diff --git a/Images.xcassets/Avatar/Contents.json b/Images.xcassets/Avatar/Contents.json new file mode 100644 index 0000000000..38f0c81fc2 --- /dev/null +++ b/Images.xcassets/Avatar/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "provides-namespace" : true + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat/Input/Text/IconSend.imageset/Contents.json b/Images.xcassets/Avatar/SavedMessagesIcon.imageset/Contents.json similarity index 72% rename from Images.xcassets/Chat/Input/Text/IconSend.imageset/Contents.json rename to Images.xcassets/Avatar/SavedMessagesIcon.imageset/Contents.json index 64d86a74a3..97518aad3a 100644 --- a/Images.xcassets/Chat/Input/Text/IconSend.imageset/Contents.json +++ b/Images.xcassets/Avatar/SavedMessagesIcon.imageset/Contents.json @@ -6,12 +6,12 @@ }, { "idiom" : "universal", - "filename" : "ModernConversationSend@2x.png", + "filename" : "SavedMessagesIcon@2x.png", "scale" : "2x" }, { "idiom" : "universal", - "filename" : "ModernConversationSend@3x.png", + "filename" : "SavedMessagesIcon@3x.png", "scale" : "3x" } ], diff --git a/Images.xcassets/Avatar/SavedMessagesIcon.imageset/SavedMessagesIcon@2x.png b/Images.xcassets/Avatar/SavedMessagesIcon.imageset/SavedMessagesIcon@2x.png new file mode 100644 index 0000000000..de460c24ee Binary files /dev/null and b/Images.xcassets/Avatar/SavedMessagesIcon.imageset/SavedMessagesIcon@2x.png differ diff --git a/Images.xcassets/Avatar/SavedMessagesIcon.imageset/SavedMessagesIcon@3x.png b/Images.xcassets/Avatar/SavedMessagesIcon.imageset/SavedMessagesIcon@3x.png new file mode 100644 index 0000000000..96a7dca662 Binary files /dev/null and b/Images.xcassets/Avatar/SavedMessagesIcon.imageset/SavedMessagesIcon@3x.png differ diff --git a/Images.xcassets/Chat List/NavigationShare.imageset/ActionsWhiteIcon@2x.png b/Images.xcassets/Chat List/NavigationShare.imageset/ActionsWhiteIcon@2x.png new file mode 100644 index 0000000000..2ad26719c3 Binary files /dev/null and b/Images.xcassets/Chat List/NavigationShare.imageset/ActionsWhiteIcon@2x.png differ diff --git a/Images.xcassets/Chat List/NavigationShare.imageset/ActionsWhiteIcon@3x.png b/Images.xcassets/Chat List/NavigationShare.imageset/ActionsWhiteIcon@3x.png new file mode 100644 index 0000000000..98dc6d8d62 Binary files /dev/null and b/Images.xcassets/Chat List/NavigationShare.imageset/ActionsWhiteIcon@3x.png differ diff --git a/Images.xcassets/Chat List/NavigationShare.imageset/Contents.json b/Images.xcassets/Chat List/NavigationShare.imageset/Contents.json index f8f827e40b..f799c88673 100644 --- a/Images.xcassets/Chat List/NavigationShare.imageset/Contents.json +++ b/Images.xcassets/Chat List/NavigationShare.imageset/Contents.json @@ -6,10 +6,12 @@ }, { "idiom" : "universal", + "filename" : "ActionsWhiteIcon@2x.png", "scale" : "2x" }, { "idiom" : "universal", + "filename" : "ActionsWhiteIcon@3x.png", "scale" : "3x" } ], diff --git a/Images.xcassets/Chat List/RevealActionGroupIcon.imageset/Contents.json b/Images.xcassets/Chat List/RevealActionGroupIcon.imageset/Contents.json new file mode 100644 index 0000000000..9e532d18a9 --- /dev/null +++ b/Images.xcassets/Chat List/RevealActionGroupIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_groupchannel.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat List/RevealActionGroupIcon.imageset/ic_groupchannel.pdf b/Images.xcassets/Chat List/RevealActionGroupIcon.imageset/ic_groupchannel.pdf new file mode 100644 index 0000000000..0663b38e9a Binary files /dev/null and b/Images.xcassets/Chat List/RevealActionGroupIcon.imageset/ic_groupchannel.pdf differ diff --git a/Images.xcassets/Chat List/RevealActionUngroupIcon.imageset/Contents.json b/Images.xcassets/Chat List/RevealActionUngroupIcon.imageset/Contents.json new file mode 100644 index 0000000000..5f2bb49ac1 --- /dev/null +++ b/Images.xcassets/Chat List/RevealActionUngroupIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_ungroupchannel.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat List/RevealActionUngroupIcon.imageset/ic_ungroupchannel.pdf b/Images.xcassets/Chat List/RevealActionUngroupIcon.imageset/ic_ungroupchannel.pdf new file mode 100644 index 0000000000..517381f5ea Binary files /dev/null and b/Images.xcassets/Chat List/RevealActionUngroupIcon.imageset/ic_ungroupchannel.pdf differ diff --git a/Images.xcassets/Chat List/Tabs/IconCallsSelected.imageset/Contents.json b/Images.xcassets/Chat List/SearchIcon.imageset/Contents.json similarity index 71% rename from Images.xcassets/Chat List/Tabs/IconCallsSelected.imageset/Contents.json rename to Images.xcassets/Chat List/SearchIcon.imageset/Contents.json index 16a7f1ac7f..5e19672ebc 100644 --- a/Images.xcassets/Chat List/Tabs/IconCallsSelected.imageset/Contents.json +++ b/Images.xcassets/Chat List/SearchIcon.imageset/Contents.json @@ -6,12 +6,12 @@ }, { "idiom" : "universal", - "filename" : "TabIconCalls_Highlighted@2x.png", + "filename" : "PanelSearchIcon@2x.png", "scale" : "2x" }, { "idiom" : "universal", - "filename" : "TabIconCalls_Highlighted@3x.png", + "filename" : "PanelSearchIcon@3x.png", "scale" : "3x" } ], diff --git a/Images.xcassets/Chat List/SearchIcon.imageset/PanelSearchIcon@2x.png b/Images.xcassets/Chat List/SearchIcon.imageset/PanelSearchIcon@2x.png new file mode 100644 index 0000000000..d0560d61db Binary files /dev/null and b/Images.xcassets/Chat List/SearchIcon.imageset/PanelSearchIcon@2x.png differ diff --git a/Images.xcassets/Chat List/SearchIcon.imageset/PanelSearchIcon@3x.png b/Images.xcassets/Chat List/SearchIcon.imageset/PanelSearchIcon@3x.png new file mode 100644 index 0000000000..7f389383bf Binary files /dev/null and b/Images.xcassets/Chat List/SearchIcon.imageset/PanelSearchIcon@3x.png differ diff --git a/Images.xcassets/Chat List/Tabs/IconCalls.imageset/Contents.json b/Images.xcassets/Chat List/Tabs/IconCalls.imageset/Contents.json index 14ee7b04c8..9afc9c4e5d 100644 --- a/Images.xcassets/Chat List/Tabs/IconCalls.imageset/Contents.json +++ b/Images.xcassets/Chat List/Tabs/IconCalls.imageset/Contents.json @@ -2,17 +2,7 @@ "images" : [ { "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "TabIconCalls@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "TabIconCalls@3x.png", - "scale" : "3x" + "filename" : "ic_calls.pdf" } ], "info" : { diff --git a/Images.xcassets/Chat List/Tabs/IconCalls.imageset/TabIconCalls@2x.png b/Images.xcassets/Chat List/Tabs/IconCalls.imageset/TabIconCalls@2x.png deleted file mode 100644 index fc1df7e1b3..0000000000 Binary files a/Images.xcassets/Chat List/Tabs/IconCalls.imageset/TabIconCalls@2x.png and /dev/null differ diff --git a/Images.xcassets/Chat List/Tabs/IconCalls.imageset/TabIconCalls@3x.png b/Images.xcassets/Chat List/Tabs/IconCalls.imageset/TabIconCalls@3x.png deleted file mode 100644 index 995cb58ef1..0000000000 Binary files a/Images.xcassets/Chat List/Tabs/IconCalls.imageset/TabIconCalls@3x.png and /dev/null differ diff --git a/Images.xcassets/Chat List/Tabs/IconCalls.imageset/ic_calls.pdf b/Images.xcassets/Chat List/Tabs/IconCalls.imageset/ic_calls.pdf new file mode 100644 index 0000000000..ce2dca8981 Binary files /dev/null and b/Images.xcassets/Chat List/Tabs/IconCalls.imageset/ic_calls.pdf differ diff --git a/Images.xcassets/Chat List/Tabs/IconCallsSelected.imageset/TabIconCalls_Highlighted@2x.png b/Images.xcassets/Chat List/Tabs/IconCallsSelected.imageset/TabIconCalls_Highlighted@2x.png deleted file mode 100644 index 4270a1212d..0000000000 Binary files a/Images.xcassets/Chat List/Tabs/IconCallsSelected.imageset/TabIconCalls_Highlighted@2x.png and /dev/null differ diff --git a/Images.xcassets/Chat List/Tabs/IconCallsSelected.imageset/TabIconCalls_Highlighted@3x.png b/Images.xcassets/Chat List/Tabs/IconCallsSelected.imageset/TabIconCalls_Highlighted@3x.png deleted file mode 100644 index 9e1be91dd9..0000000000 Binary files a/Images.xcassets/Chat List/Tabs/IconCallsSelected.imageset/TabIconCalls_Highlighted@3x.png and /dev/null differ diff --git a/Images.xcassets/Chat List/Tabs/IconChats.imageset/Contents.json b/Images.xcassets/Chat List/Tabs/IconChats.imageset/Contents.json index 67b8fd9f58..cb092b491e 100644 --- a/Images.xcassets/Chat List/Tabs/IconChats.imageset/Contents.json +++ b/Images.xcassets/Chat List/Tabs/IconChats.imageset/Contents.json @@ -2,17 +2,7 @@ "images" : [ { "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "TabIconMessages@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "TabIconMessages@3x.png", - "scale" : "3x" + "filename" : "ic_chats.pdf" } ], "info" : { diff --git a/Images.xcassets/Chat List/Tabs/IconChats.imageset/TabIconMessages@2x.png b/Images.xcassets/Chat List/Tabs/IconChats.imageset/TabIconMessages@2x.png deleted file mode 100644 index ca04fededf..0000000000 Binary files a/Images.xcassets/Chat List/Tabs/IconChats.imageset/TabIconMessages@2x.png and /dev/null differ diff --git a/Images.xcassets/Chat List/Tabs/IconChats.imageset/TabIconMessages@3x.png b/Images.xcassets/Chat List/Tabs/IconChats.imageset/TabIconMessages@3x.png deleted file mode 100644 index 887f6d649a..0000000000 Binary files a/Images.xcassets/Chat List/Tabs/IconChats.imageset/TabIconMessages@3x.png and /dev/null differ diff --git a/Images.xcassets/Chat List/Tabs/IconChats.imageset/ic_chats.pdf b/Images.xcassets/Chat List/Tabs/IconChats.imageset/ic_chats.pdf new file mode 100644 index 0000000000..7023f0492d Binary files /dev/null and b/Images.xcassets/Chat List/Tabs/IconChats.imageset/ic_chats.pdf differ diff --git a/Images.xcassets/Chat List/Tabs/IconChatsSelected.imageset/TabIconMessages_Highlighted@2x.png b/Images.xcassets/Chat List/Tabs/IconChatsSelected.imageset/TabIconMessages_Highlighted@2x.png deleted file mode 100644 index 024445c9b0..0000000000 Binary files a/Images.xcassets/Chat List/Tabs/IconChatsSelected.imageset/TabIconMessages_Highlighted@2x.png and /dev/null differ diff --git a/Images.xcassets/Chat List/Tabs/IconChatsSelected.imageset/TabIconMessages_Highlighted@3x.png b/Images.xcassets/Chat List/Tabs/IconChatsSelected.imageset/TabIconMessages_Highlighted@3x.png deleted file mode 100644 index c935785bb9..0000000000 Binary files a/Images.xcassets/Chat List/Tabs/IconChatsSelected.imageset/TabIconMessages_Highlighted@3x.png and /dev/null differ diff --git a/Images.xcassets/Chat List/Tabs/IconContacts.imageset/Contents.json b/Images.xcassets/Chat List/Tabs/IconContacts.imageset/Contents.json index d9cd705b50..006f82fb71 100644 --- a/Images.xcassets/Chat List/Tabs/IconContacts.imageset/Contents.json +++ b/Images.xcassets/Chat List/Tabs/IconContacts.imageset/Contents.json @@ -2,17 +2,7 @@ "images" : [ { "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "TabIconContacts@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "TabIconContacts@3x.png", - "scale" : "3x" + "filename" : "ic_contacts.pdf" } ], "info" : { diff --git a/Images.xcassets/Chat List/Tabs/IconContacts.imageset/TabIconContacts@2x.png b/Images.xcassets/Chat List/Tabs/IconContacts.imageset/TabIconContacts@2x.png deleted file mode 100644 index ff709a9dc6..0000000000 Binary files a/Images.xcassets/Chat List/Tabs/IconContacts.imageset/TabIconContacts@2x.png and /dev/null differ diff --git a/Images.xcassets/Chat List/Tabs/IconContacts.imageset/TabIconContacts@3x.png b/Images.xcassets/Chat List/Tabs/IconContacts.imageset/TabIconContacts@3x.png deleted file mode 100644 index 0bd8bc85d8..0000000000 Binary files a/Images.xcassets/Chat List/Tabs/IconContacts.imageset/TabIconContacts@3x.png and /dev/null differ diff --git a/Images.xcassets/Chat List/Tabs/IconContacts.imageset/ic_contacts.pdf b/Images.xcassets/Chat List/Tabs/IconContacts.imageset/ic_contacts.pdf new file mode 100644 index 0000000000..1110b53707 Binary files /dev/null and b/Images.xcassets/Chat List/Tabs/IconContacts.imageset/ic_contacts.pdf differ diff --git a/Images.xcassets/Chat List/Tabs/IconContactsSelected.imageset/TabIconContacts_Highlighted@2x.png b/Images.xcassets/Chat List/Tabs/IconContactsSelected.imageset/TabIconContacts_Highlighted@2x.png deleted file mode 100644 index b7e573a3f5..0000000000 Binary files a/Images.xcassets/Chat List/Tabs/IconContactsSelected.imageset/TabIconContacts_Highlighted@2x.png and /dev/null differ diff --git a/Images.xcassets/Chat List/Tabs/IconContactsSelected.imageset/TabIconContacts_Highlighted@3x.png b/Images.xcassets/Chat List/Tabs/IconContactsSelected.imageset/TabIconContacts_Highlighted@3x.png deleted file mode 100644 index 439091bc50..0000000000 Binary files a/Images.xcassets/Chat List/Tabs/IconContactsSelected.imageset/TabIconContacts_Highlighted@3x.png and /dev/null differ diff --git a/Images.xcassets/Chat List/Tabs/IconSettings.imageset/Contents.json b/Images.xcassets/Chat List/Tabs/IconSettings.imageset/Contents.json index 1102b437dc..261d9ff3a9 100644 --- a/Images.xcassets/Chat List/Tabs/IconSettings.imageset/Contents.json +++ b/Images.xcassets/Chat List/Tabs/IconSettings.imageset/Contents.json @@ -2,17 +2,7 @@ "images" : [ { "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "TabIconSettings@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "TabIconSettings@3x.png", - "scale" : "3x" + "filename" : "ic_settings.pdf" } ], "info" : { diff --git a/Images.xcassets/Chat List/Tabs/IconSettings.imageset/TabIconSettings@2x.png b/Images.xcassets/Chat List/Tabs/IconSettings.imageset/TabIconSettings@2x.png deleted file mode 100644 index 61e3ad0d9b..0000000000 Binary files a/Images.xcassets/Chat List/Tabs/IconSettings.imageset/TabIconSettings@2x.png and /dev/null differ diff --git a/Images.xcassets/Chat List/Tabs/IconSettings.imageset/TabIconSettings@3x.png b/Images.xcassets/Chat List/Tabs/IconSettings.imageset/TabIconSettings@3x.png deleted file mode 100644 index 4a6d24a1ff..0000000000 Binary files a/Images.xcassets/Chat List/Tabs/IconSettings.imageset/TabIconSettings@3x.png and /dev/null differ diff --git a/Images.xcassets/Chat List/Tabs/IconSettings.imageset/ic_settings.pdf b/Images.xcassets/Chat List/Tabs/IconSettings.imageset/ic_settings.pdf new file mode 100644 index 0000000000..b5a3c4fb91 Binary files /dev/null and b/Images.xcassets/Chat List/Tabs/IconSettings.imageset/ic_settings.pdf differ diff --git a/Images.xcassets/Chat List/Tabs/IconSettingsSelected.imageset/TabIconSettings_Highlighted@2x.png b/Images.xcassets/Chat List/Tabs/IconSettingsSelected.imageset/TabIconSettings_Highlighted@2x.png deleted file mode 100644 index a734866a7a..0000000000 Binary files a/Images.xcassets/Chat List/Tabs/IconSettingsSelected.imageset/TabIconSettings_Highlighted@2x.png and /dev/null differ diff --git a/Images.xcassets/Chat List/Tabs/IconSettingsSelected.imageset/TabIconSettings_Highlighted@3x.png b/Images.xcassets/Chat List/Tabs/IconSettingsSelected.imageset/TabIconSettings_Highlighted@3x.png deleted file mode 100644 index 06bb4b120d..0000000000 Binary files a/Images.xcassets/Chat List/Tabs/IconSettingsSelected.imageset/TabIconSettings_Highlighted@3x.png and /dev/null differ diff --git a/Images.xcassets/Chat/Input/Media/SavedStickersTabIcon.imageset/Contents.json b/Images.xcassets/Chat/Input/Media/SavedStickersTabIcon.imageset/Contents.json index 0c077faf04..e46f4e6129 100644 --- a/Images.xcassets/Chat/Input/Media/SavedStickersTabIcon.imageset/Contents.json +++ b/Images.xcassets/Chat/Input/Media/SavedStickersTabIcon.imageset/Contents.json @@ -6,12 +6,12 @@ }, { "idiom" : "universal", - "filename" : "ic_favestickerstab@2x.png", + "filename" : "StickerKeyboardFavoriteTab@2x.png", "scale" : "2x" }, { "idiom" : "universal", - "filename" : "ic_favestickerstab@3x.png", + "filename" : "StickerKeyboardFavoriteTab@3x.png", "scale" : "3x" } ], diff --git a/Images.xcassets/Chat/Input/Media/SavedStickersTabIcon.imageset/ic_favestickerstab@2x.png b/Images.xcassets/Chat/Input/Media/SavedStickersTabIcon.imageset/StickerKeyboardFavoriteTab@2x.png similarity index 100% rename from Images.xcassets/Chat/Input/Media/SavedStickersTabIcon.imageset/ic_favestickerstab@2x.png rename to Images.xcassets/Chat/Input/Media/SavedStickersTabIcon.imageset/StickerKeyboardFavoriteTab@2x.png diff --git a/Images.xcassets/Chat/Input/Media/SavedStickersTabIcon.imageset/ic_favestickerstab@3x.png b/Images.xcassets/Chat/Input/Media/SavedStickersTabIcon.imageset/StickerKeyboardFavoriteTab@3x.png similarity index 100% rename from Images.xcassets/Chat/Input/Media/SavedStickersTabIcon.imageset/ic_favestickerstab@3x.png rename to Images.xcassets/Chat/Input/Media/SavedStickersTabIcon.imageset/StickerKeyboardFavoriteTab@3x.png diff --git a/Images.xcassets/Chat List/Tabs/IconChatsSelected.imageset/Contents.json b/Images.xcassets/Chat/Input/Media/SettingsIcon.imageset/Contents.json similarity index 70% rename from Images.xcassets/Chat List/Tabs/IconChatsSelected.imageset/Contents.json rename to Images.xcassets/Chat/Input/Media/SettingsIcon.imageset/Contents.json index aab44bc924..ef6daa8e54 100644 --- a/Images.xcassets/Chat List/Tabs/IconChatsSelected.imageset/Contents.json +++ b/Images.xcassets/Chat/Input/Media/SettingsIcon.imageset/Contents.json @@ -6,12 +6,12 @@ }, { "idiom" : "universal", - "filename" : "TabIconMessages_Highlighted@2x.png", + "filename" : "StickerKeyboardSettingsIcon@2x.png", "scale" : "2x" }, { "idiom" : "universal", - "filename" : "TabIconMessages_Highlighted@3x.png", + "filename" : "StickerKeyboardSettingsIcon@3x.png", "scale" : "3x" } ], diff --git a/Images.xcassets/Chat/Input/Media/SettingsIcon.imageset/StickerKeyboardSettingsIcon@2x.png b/Images.xcassets/Chat/Input/Media/SettingsIcon.imageset/StickerKeyboardSettingsIcon@2x.png new file mode 100644 index 0000000000..d0dc89b9ff Binary files /dev/null and b/Images.xcassets/Chat/Input/Media/SettingsIcon.imageset/StickerKeyboardSettingsIcon@2x.png differ diff --git a/Images.xcassets/Chat/Input/Media/SettingsIcon.imageset/StickerKeyboardSettingsIcon@3x.png b/Images.xcassets/Chat/Input/Media/SettingsIcon.imageset/StickerKeyboardSettingsIcon@3x.png new file mode 100644 index 0000000000..54b04299e5 Binary files /dev/null and b/Images.xcassets/Chat/Input/Media/SettingsIcon.imageset/StickerKeyboardSettingsIcon@3x.png differ diff --git a/Images.xcassets/Chat List/Tabs/IconContactsSelected.imageset/Contents.json b/Images.xcassets/Chat/Input/Media/TrendingIcon.imageset/Contents.json similarity index 70% rename from Images.xcassets/Chat List/Tabs/IconContactsSelected.imageset/Contents.json rename to Images.xcassets/Chat/Input/Media/TrendingIcon.imageset/Contents.json index 38dfa084e4..52ee5360d8 100644 --- a/Images.xcassets/Chat List/Tabs/IconContactsSelected.imageset/Contents.json +++ b/Images.xcassets/Chat/Input/Media/TrendingIcon.imageset/Contents.json @@ -6,12 +6,12 @@ }, { "idiom" : "universal", - "filename" : "TabIconContacts_Highlighted@2x.png", + "filename" : "StickerKeyboardTrendingIcon@2x.png", "scale" : "2x" }, { "idiom" : "universal", - "filename" : "TabIconContacts_Highlighted@3x.png", + "filename" : "StickerKeyboardTrendingIcon@3x.png", "scale" : "3x" } ], diff --git a/Images.xcassets/Chat/Input/Media/TrendingIcon.imageset/StickerKeyboardTrendingIcon@2x.png b/Images.xcassets/Chat/Input/Media/TrendingIcon.imageset/StickerKeyboardTrendingIcon@2x.png new file mode 100644 index 0000000000..67de340019 Binary files /dev/null and b/Images.xcassets/Chat/Input/Media/TrendingIcon.imageset/StickerKeyboardTrendingIcon@2x.png differ diff --git a/Images.xcassets/Chat/Input/Media/TrendingIcon.imageset/StickerKeyboardTrendingIcon@3x.png b/Images.xcassets/Chat/Input/Media/TrendingIcon.imageset/StickerKeyboardTrendingIcon@3x.png new file mode 100644 index 0000000000..ed14979353 Binary files /dev/null and b/Images.xcassets/Chat/Input/Media/TrendingIcon.imageset/StickerKeyboardTrendingIcon@3x.png differ diff --git a/Images.xcassets/Chat/Input/Search/Calendar.imageset/ConversationSearchCalendar@2x.png b/Images.xcassets/Chat/Input/Search/Calendar.imageset/ConversationSearchCalendar@2x.png index a9e4237a05..bb77e0e168 100644 Binary files a/Images.xcassets/Chat/Input/Search/Calendar.imageset/ConversationSearchCalendar@2x.png and b/Images.xcassets/Chat/Input/Search/Calendar.imageset/ConversationSearchCalendar@2x.png differ diff --git a/Images.xcassets/Chat/Input/Search/Calendar.imageset/ConversationSearchCalendar@3x.png b/Images.xcassets/Chat/Input/Search/Calendar.imageset/ConversationSearchCalendar@3x.png index b916a15653..f8cb42ce61 100644 Binary files a/Images.xcassets/Chat/Input/Search/Calendar.imageset/ConversationSearchCalendar@3x.png and b/Images.xcassets/Chat/Input/Search/Calendar.imageset/ConversationSearchCalendar@3x.png differ diff --git a/Images.xcassets/Chat List/Tabs/IconSettingsSelected.imageset/Contents.json b/Images.xcassets/Chat/Input/Search/Members.imageset/Contents.json similarity index 70% rename from Images.xcassets/Chat List/Tabs/IconSettingsSelected.imageset/Contents.json rename to Images.xcassets/Chat/Input/Search/Members.imageset/Contents.json index 8036cc64b9..2fd9e5003a 100644 --- a/Images.xcassets/Chat List/Tabs/IconSettingsSelected.imageset/Contents.json +++ b/Images.xcassets/Chat/Input/Search/Members.imageset/Contents.json @@ -6,12 +6,12 @@ }, { "idiom" : "universal", - "filename" : "TabIconSettings_Highlighted@2x.png", + "filename" : "ConversationSearchUser@2x.png", "scale" : "2x" }, { "idiom" : "universal", - "filename" : "TabIconSettings_Highlighted@3x.png", + "filename" : "ConversationSearchUser@3x.png", "scale" : "3x" } ], diff --git a/Images.xcassets/Chat/Input/Search/Members.imageset/ConversationSearchUser@2x.png b/Images.xcassets/Chat/Input/Search/Members.imageset/ConversationSearchUser@2x.png new file mode 100644 index 0000000000..d0f73084f0 Binary files /dev/null and b/Images.xcassets/Chat/Input/Search/Members.imageset/ConversationSearchUser@2x.png differ diff --git a/Images.xcassets/Chat/Input/Search/Members.imageset/ConversationSearchUser@3x.png b/Images.xcassets/Chat/Input/Search/Members.imageset/ConversationSearchUser@3x.png new file mode 100644 index 0000000000..d7bc7d4c3a Binary files /dev/null and b/Images.xcassets/Chat/Input/Search/Members.imageset/ConversationSearchUser@3x.png differ diff --git a/Images.xcassets/Chat/Input/Text/IconSend.imageset/ModernConversationSend@2x.png b/Images.xcassets/Chat/Input/Text/IconSend.imageset/ModernConversationSend@2x.png deleted file mode 100644 index f31a1118cd..0000000000 Binary files a/Images.xcassets/Chat/Input/Text/IconSend.imageset/ModernConversationSend@2x.png and /dev/null differ diff --git a/Images.xcassets/Chat/Input/Text/IconSend.imageset/ModernConversationSend@3x.png b/Images.xcassets/Chat/Input/Text/IconSend.imageset/ModernConversationSend@3x.png deleted file mode 100644 index f74122a57e..0000000000 Binary files a/Images.xcassets/Chat/Input/Text/IconSend.imageset/ModernConversationSend@3x.png and /dev/null differ diff --git a/Images.xcassets/Chat/Message/LocationPin.imageset/Contents.json b/Images.xcassets/Chat/Message/LocationPin.imageset/Contents.json new file mode 100644 index 0000000000..ac254df0ac --- /dev/null +++ b/Images.xcassets/Chat/Message/LocationPin.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LocationMessagePinSmallBackground@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LocationMessagePinSmallBackground@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat/Message/LocationPin.imageset/LocationMessagePinSmallBackground@2x.png b/Images.xcassets/Chat/Message/LocationPin.imageset/LocationMessagePinSmallBackground@2x.png new file mode 100644 index 0000000000..29fc09ef11 Binary files /dev/null and b/Images.xcassets/Chat/Message/LocationPin.imageset/LocationMessagePinSmallBackground@2x.png differ diff --git a/Images.xcassets/Chat/Message/LocationPin.imageset/LocationMessagePinSmallBackground@3x.png b/Images.xcassets/Chat/Message/LocationPin.imageset/LocationMessagePinSmallBackground@3x.png new file mode 100644 index 0000000000..beba81b271 Binary files /dev/null and b/Images.xcassets/Chat/Message/LocationPin.imageset/LocationMessagePinSmallBackground@3x.png differ diff --git a/Images.xcassets/Chat/Message/LocationPinBackground.imageset/Contents.json b/Images.xcassets/Chat/Message/LocationPinBackground.imageset/Contents.json new file mode 100644 index 0000000000..bc0aabaf92 --- /dev/null +++ b/Images.xcassets/Chat/Message/LocationPinBackground.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LocationMessagePinBackground@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LocationMessagePinBackground@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat/Message/LocationPinBackground.imageset/LocationMessagePinBackground@2x.png b/Images.xcassets/Chat/Message/LocationPinBackground.imageset/LocationMessagePinBackground@2x.png new file mode 100644 index 0000000000..35384f6e1f Binary files /dev/null and b/Images.xcassets/Chat/Message/LocationPinBackground.imageset/LocationMessagePinBackground@2x.png differ diff --git a/Images.xcassets/Chat/Message/LocationPinBackground.imageset/LocationMessagePinBackground@3x.png b/Images.xcassets/Chat/Message/LocationPinBackground.imageset/LocationMessagePinBackground@3x.png new file mode 100644 index 0000000000..5eeda02fa0 Binary files /dev/null and b/Images.xcassets/Chat/Message/LocationPinBackground.imageset/LocationMessagePinBackground@3x.png differ diff --git a/Images.xcassets/Chat/Message/LocationPinForeground.imageset/Contents.json b/Images.xcassets/Chat/Message/LocationPinForeground.imageset/Contents.json new file mode 100644 index 0000000000..ad219d9cdd --- /dev/null +++ b/Images.xcassets/Chat/Message/LocationPinForeground.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LocationMessagePinIcon@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LocationMessagePinIcon@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat/Message/LocationPinForeground.imageset/LocationMessagePinIcon@2x.png b/Images.xcassets/Chat/Message/LocationPinForeground.imageset/LocationMessagePinIcon@2x.png new file mode 100644 index 0000000000..cff797ee18 Binary files /dev/null and b/Images.xcassets/Chat/Message/LocationPinForeground.imageset/LocationMessagePinIcon@2x.png differ diff --git a/Images.xcassets/Chat/Message/LocationPinForeground.imageset/LocationMessagePinIcon@3x.png b/Images.xcassets/Chat/Message/LocationPinForeground.imageset/LocationMessagePinIcon@3x.png new file mode 100644 index 0000000000..2c7603bb7d Binary files /dev/null and b/Images.xcassets/Chat/Message/LocationPinForeground.imageset/LocationMessagePinIcon@3x.png differ diff --git a/Images.xcassets/Chat/Message/LocationPinShadow.imageset/Contents.json b/Images.xcassets/Chat/Message/LocationPinShadow.imageset/Contents.json new file mode 100644 index 0000000000..1e10396b34 --- /dev/null +++ b/Images.xcassets/Chat/Message/LocationPinShadow.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LocationMessagePinShadow@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LocationMessagePinShadow@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat/Message/LocationPinShadow.imageset/LocationMessagePinShadow@2x.png b/Images.xcassets/Chat/Message/LocationPinShadow.imageset/LocationMessagePinShadow@2x.png new file mode 100644 index 0000000000..709eb48ffe Binary files /dev/null and b/Images.xcassets/Chat/Message/LocationPinShadow.imageset/LocationMessagePinShadow@2x.png differ diff --git a/Images.xcassets/Chat/Message/LocationPinShadow.imageset/LocationMessagePinShadow@3x.png b/Images.xcassets/Chat/Message/LocationPinShadow.imageset/LocationMessagePinShadow@3x.png new file mode 100644 index 0000000000..38bf099fe4 Binary files /dev/null and b/Images.xcassets/Chat/Message/LocationPinShadow.imageset/LocationMessagePinShadow@3x.png differ diff --git a/Images.xcassets/Chat/Message/NavigateToMessageIcon.imageset/Contents.json b/Images.xcassets/Chat/Message/NavigateToMessageIcon.imageset/Contents.json new file mode 100644 index 0000000000..99b1738905 --- /dev/null +++ b/Images.xcassets/Chat/Message/NavigateToMessageIcon.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ConversationGoToIcon@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "ConversationGoToIcon@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat/Message/NavigateToMessageIcon.imageset/ConversationGoToIcon@2x.png b/Images.xcassets/Chat/Message/NavigateToMessageIcon.imageset/ConversationGoToIcon@2x.png new file mode 100644 index 0000000000..1ad8e920a9 Binary files /dev/null and b/Images.xcassets/Chat/Message/NavigateToMessageIcon.imageset/ConversationGoToIcon@2x.png differ diff --git a/Images.xcassets/Chat/Message/NavigateToMessageIcon.imageset/ConversationGoToIcon@3x.png b/Images.xcassets/Chat/Message/NavigateToMessageIcon.imageset/ConversationGoToIcon@3x.png new file mode 100644 index 0000000000..62ad51a96b Binary files /dev/null and b/Images.xcassets/Chat/Message/NavigateToMessageIcon.imageset/ConversationGoToIcon@3x.png differ diff --git a/Images.xcassets/Contact List/AddMemberIcon.imageset/Contents.json b/Images.xcassets/Contact List/AddMemberIcon.imageset/Contents.json new file mode 100644 index 0000000000..1c05986955 --- /dev/null +++ b/Images.xcassets/Contact List/AddMemberIcon.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ModernContactListAddMemberIcon@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "ModernContactListAddMemberIcon@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Contact List/AddMemberIcon.imageset/ModernContactListAddMemberIcon@2x.png b/Images.xcassets/Contact List/AddMemberIcon.imageset/ModernContactListAddMemberIcon@2x.png new file mode 100644 index 0000000000..39fd43d8a3 Binary files /dev/null and b/Images.xcassets/Contact List/AddMemberIcon.imageset/ModernContactListAddMemberIcon@2x.png differ diff --git a/Images.xcassets/Contact List/AddMemberIcon.imageset/ModernContactListAddMemberIcon@3x.png b/Images.xcassets/Contact List/AddMemberIcon.imageset/ModernContactListAddMemberIcon@3x.png new file mode 100644 index 0000000000..8b46153dd8 Binary files /dev/null and b/Images.xcassets/Contact List/AddMemberIcon.imageset/ModernContactListAddMemberIcon@3x.png differ diff --git a/Images.xcassets/Contact List/InviteActionIcon.imageset/Contents.json b/Images.xcassets/Contact List/InviteActionIcon.imageset/Contents.json new file mode 100644 index 0000000000..de83dc2139 --- /dev/null +++ b/Images.xcassets/Contact List/InviteActionIcon.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ModernContactListInviteIcon@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "ModernContactListInviteIcon@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Contact List/InviteActionIcon.imageset/ModernContactListInviteIcon@2x.png b/Images.xcassets/Contact List/InviteActionIcon.imageset/ModernContactListInviteIcon@2x.png new file mode 100644 index 0000000000..4175b2c4e2 Binary files /dev/null and b/Images.xcassets/Contact List/InviteActionIcon.imageset/ModernContactListInviteIcon@2x.png differ diff --git a/Images.xcassets/Contact List/InviteActionIcon.imageset/ModernContactListInviteIcon@3x.png b/Images.xcassets/Contact List/InviteActionIcon.imageset/ModernContactListInviteIcon@3x.png new file mode 100644 index 0000000000..8c149a3ebd Binary files /dev/null and b/Images.xcassets/Contact List/InviteActionIcon.imageset/ModernContactListInviteIcon@3x.png differ diff --git a/Images.xcassets/Contact List/LinkActionIcon.imageset/Contents.json b/Images.xcassets/Contact List/LinkActionIcon.imageset/Contents.json new file mode 100644 index 0000000000..1d8c48f119 --- /dev/null +++ b/Images.xcassets/Contact List/LinkActionIcon.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ModernContactListInviteFriendsIcon@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "ModernContactListInviteFriendsIcon@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Contact List/LinkActionIcon.imageset/ModernContactListInviteFriendsIcon@2x.png b/Images.xcassets/Contact List/LinkActionIcon.imageset/ModernContactListInviteFriendsIcon@2x.png new file mode 100644 index 0000000000..699598d62c Binary files /dev/null and b/Images.xcassets/Contact List/LinkActionIcon.imageset/ModernContactListInviteFriendsIcon@2x.png differ diff --git a/Images.xcassets/Contact List/LinkActionIcon.imageset/ModernContactListInviteFriendsIcon@3x.png b/Images.xcassets/Contact List/LinkActionIcon.imageset/ModernContactListInviteFriendsIcon@3x.png new file mode 100644 index 0000000000..ddafd80782 Binary files /dev/null and b/Images.xcassets/Contact List/LinkActionIcon.imageset/ModernContactListInviteFriendsIcon@3x.png differ diff --git a/Images.xcassets/GlobalMusicPlayer/AlbumArtPlaceholder.imageset/Contents.json b/Images.xcassets/GlobalMusicPlayer/AlbumArtPlaceholder.imageset/Contents.json new file mode 100644 index 0000000000..aeac20352b --- /dev/null +++ b/Images.xcassets/GlobalMusicPlayer/AlbumArtPlaceholder.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "MusicPlayerAlbumArtPlaceholder@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "MusicPlayerAlbumArtPlaceholder@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/GlobalMusicPlayer/AlbumArtPlaceholder.imageset/MusicPlayerAlbumArtPlaceholder@2x.png b/Images.xcassets/GlobalMusicPlayer/AlbumArtPlaceholder.imageset/MusicPlayerAlbumArtPlaceholder@2x.png new file mode 100644 index 0000000000..9c5d8ac147 Binary files /dev/null and b/Images.xcassets/GlobalMusicPlayer/AlbumArtPlaceholder.imageset/MusicPlayerAlbumArtPlaceholder@2x.png differ diff --git a/Images.xcassets/GlobalMusicPlayer/AlbumArtPlaceholder.imageset/MusicPlayerAlbumArtPlaceholder@3x.png b/Images.xcassets/GlobalMusicPlayer/AlbumArtPlaceholder.imageset/MusicPlayerAlbumArtPlaceholder@3x.png new file mode 100644 index 0000000000..82389186a6 Binary files /dev/null and b/Images.xcassets/GlobalMusicPlayer/AlbumArtPlaceholder.imageset/MusicPlayerAlbumArtPlaceholder@3x.png differ diff --git a/Images.xcassets/GlobalMusicPlayer/CollapseArrow.imageset/Contents.json b/Images.xcassets/GlobalMusicPlayer/CollapseArrow.imageset/Contents.json new file mode 100644 index 0000000000..18119da5de --- /dev/null +++ b/Images.xcassets/GlobalMusicPlayer/CollapseArrow.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "MusicPlayerArrow@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "MusicPlayerArrow@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/GlobalMusicPlayer/CollapseArrow.imageset/MusicPlayerArrow@2x.png b/Images.xcassets/GlobalMusicPlayer/CollapseArrow.imageset/MusicPlayerArrow@2x.png new file mode 100644 index 0000000000..3f0dec5b36 Binary files /dev/null and b/Images.xcassets/GlobalMusicPlayer/CollapseArrow.imageset/MusicPlayerArrow@2x.png differ diff --git a/Images.xcassets/GlobalMusicPlayer/CollapseArrow.imageset/MusicPlayerArrow@3x.png b/Images.xcassets/GlobalMusicPlayer/CollapseArrow.imageset/MusicPlayerArrow@3x.png new file mode 100644 index 0000000000..bbcf8d71a3 Binary files /dev/null and b/Images.xcassets/GlobalMusicPlayer/CollapseArrow.imageset/MusicPlayerArrow@3x.png differ diff --git a/Images.xcassets/GlobalMusicPlayer/Next.imageset/MusicPlayerControlForward@2x.png b/Images.xcassets/GlobalMusicPlayer/Next.imageset/MusicPlayerControlForward@2x.png index ccffb3478c..03bf295608 100644 Binary files a/Images.xcassets/GlobalMusicPlayer/Next.imageset/MusicPlayerControlForward@2x.png and b/Images.xcassets/GlobalMusicPlayer/Next.imageset/MusicPlayerControlForward@2x.png differ diff --git a/Images.xcassets/GlobalMusicPlayer/Next.imageset/MusicPlayerControlForward@3x.png b/Images.xcassets/GlobalMusicPlayer/Next.imageset/MusicPlayerControlForward@3x.png index a700a7c62b..e616776ed6 100644 Binary files a/Images.xcassets/GlobalMusicPlayer/Next.imageset/MusicPlayerControlForward@3x.png and b/Images.xcassets/GlobalMusicPlayer/Next.imageset/MusicPlayerControlForward@3x.png differ diff --git a/Images.xcassets/GlobalMusicPlayer/OrderRandom.imageset/Contents.json b/Images.xcassets/GlobalMusicPlayer/OrderRandom.imageset/Contents.json new file mode 100644 index 0000000000..a76057444b --- /dev/null +++ b/Images.xcassets/GlobalMusicPlayer/OrderRandom.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "MusicPlayerControlShuffle@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "MusicPlayerControlShuffle@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/GlobalMusicPlayer/OrderRandom.imageset/MusicPlayerControlShuffle@2x.png b/Images.xcassets/GlobalMusicPlayer/OrderRandom.imageset/MusicPlayerControlShuffle@2x.png new file mode 100644 index 0000000000..7f73b91622 Binary files /dev/null and b/Images.xcassets/GlobalMusicPlayer/OrderRandom.imageset/MusicPlayerControlShuffle@2x.png differ diff --git a/Images.xcassets/GlobalMusicPlayer/OrderRandom.imageset/MusicPlayerControlShuffle@3x.png b/Images.xcassets/GlobalMusicPlayer/OrderRandom.imageset/MusicPlayerControlShuffle@3x.png new file mode 100644 index 0000000000..facc408289 Binary files /dev/null and b/Images.xcassets/GlobalMusicPlayer/OrderRandom.imageset/MusicPlayerControlShuffle@3x.png differ diff --git a/Images.xcassets/GlobalMusicPlayer/OrderReverse.imageset/Contents.json b/Images.xcassets/GlobalMusicPlayer/OrderReverse.imageset/Contents.json new file mode 100644 index 0000000000..f840cc796b --- /dev/null +++ b/Images.xcassets/GlobalMusicPlayer/OrderReverse.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "MusicPlayerControlReverse@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "MusicPlayerControlReverse@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/GlobalMusicPlayer/OrderReverse.imageset/MusicPlayerControlReverse@2x.png b/Images.xcassets/GlobalMusicPlayer/OrderReverse.imageset/MusicPlayerControlReverse@2x.png new file mode 100644 index 0000000000..17b7afeaeb Binary files /dev/null and b/Images.xcassets/GlobalMusicPlayer/OrderReverse.imageset/MusicPlayerControlReverse@2x.png differ diff --git a/Images.xcassets/GlobalMusicPlayer/OrderReverse.imageset/MusicPlayerControlReverse@3x.png b/Images.xcassets/GlobalMusicPlayer/OrderReverse.imageset/MusicPlayerControlReverse@3x.png new file mode 100644 index 0000000000..e2006eb4f5 Binary files /dev/null and b/Images.xcassets/GlobalMusicPlayer/OrderReverse.imageset/MusicPlayerControlReverse@3x.png differ diff --git a/Images.xcassets/GlobalMusicPlayer/Pause.imageset/MusicPlayerControlPause@2x.png b/Images.xcassets/GlobalMusicPlayer/Pause.imageset/MusicPlayerControlPause@2x.png index 8730f240f9..d211fca050 100644 Binary files a/Images.xcassets/GlobalMusicPlayer/Pause.imageset/MusicPlayerControlPause@2x.png and b/Images.xcassets/GlobalMusicPlayer/Pause.imageset/MusicPlayerControlPause@2x.png differ diff --git a/Images.xcassets/GlobalMusicPlayer/Pause.imageset/MusicPlayerControlPause@3x.png b/Images.xcassets/GlobalMusicPlayer/Pause.imageset/MusicPlayerControlPause@3x.png index 222823abf3..26c4e50b11 100644 Binary files a/Images.xcassets/GlobalMusicPlayer/Pause.imageset/MusicPlayerControlPause@3x.png and b/Images.xcassets/GlobalMusicPlayer/Pause.imageset/MusicPlayerControlPause@3x.png differ diff --git a/Images.xcassets/GlobalMusicPlayer/Play.imageset/MusicPlayerControlPlay@2x.png b/Images.xcassets/GlobalMusicPlayer/Play.imageset/MusicPlayerControlPlay@2x.png index ad21af44a3..d7edb29921 100644 Binary files a/Images.xcassets/GlobalMusicPlayer/Play.imageset/MusicPlayerControlPlay@2x.png and b/Images.xcassets/GlobalMusicPlayer/Play.imageset/MusicPlayerControlPlay@2x.png differ diff --git a/Images.xcassets/GlobalMusicPlayer/Play.imageset/MusicPlayerControlPlay@3x.png b/Images.xcassets/GlobalMusicPlayer/Play.imageset/MusicPlayerControlPlay@3x.png index f791f38566..08f093f0fc 100644 Binary files a/Images.xcassets/GlobalMusicPlayer/Play.imageset/MusicPlayerControlPlay@3x.png and b/Images.xcassets/GlobalMusicPlayer/Play.imageset/MusicPlayerControlPlay@3x.png differ diff --git a/Images.xcassets/GlobalMusicPlayer/Previous.imageset/MusicPlayerControlBack@2x.png b/Images.xcassets/GlobalMusicPlayer/Previous.imageset/MusicPlayerControlBack@2x.png index 6ee078287f..d4fbb89351 100644 Binary files a/Images.xcassets/GlobalMusicPlayer/Previous.imageset/MusicPlayerControlBack@2x.png and b/Images.xcassets/GlobalMusicPlayer/Previous.imageset/MusicPlayerControlBack@2x.png differ diff --git a/Images.xcassets/GlobalMusicPlayer/Previous.imageset/MusicPlayerControlBack@3x.png b/Images.xcassets/GlobalMusicPlayer/Previous.imageset/MusicPlayerControlBack@3x.png index 839258b11d..3591c6c3c2 100644 Binary files a/Images.xcassets/GlobalMusicPlayer/Previous.imageset/MusicPlayerControlBack@3x.png and b/Images.xcassets/GlobalMusicPlayer/Previous.imageset/MusicPlayerControlBack@3x.png differ diff --git a/Images.xcassets/GlobalMusicPlayer/Repeat.imageset/Contents.json b/Images.xcassets/GlobalMusicPlayer/Repeat.imageset/Contents.json index 6984e9a73c..211465d7b5 100644 --- a/Images.xcassets/GlobalMusicPlayer/Repeat.imageset/Contents.json +++ b/Images.xcassets/GlobalMusicPlayer/Repeat.imageset/Contents.json @@ -11,6 +11,7 @@ }, { "idiom" : "universal", + "filename" : "MusicPlayerControlRepeat@3x.png", "scale" : "3x" } ], diff --git a/Images.xcassets/GlobalMusicPlayer/Repeat.imageset/MusicPlayerControlRepeat@2x.png b/Images.xcassets/GlobalMusicPlayer/Repeat.imageset/MusicPlayerControlRepeat@2x.png index 45d2b384f8..b527fa27f5 100644 Binary files a/Images.xcassets/GlobalMusicPlayer/Repeat.imageset/MusicPlayerControlRepeat@2x.png and b/Images.xcassets/GlobalMusicPlayer/Repeat.imageset/MusicPlayerControlRepeat@2x.png differ diff --git a/Images.xcassets/GlobalMusicPlayer/Repeat.imageset/MusicPlayerControlRepeat@3x.png b/Images.xcassets/GlobalMusicPlayer/Repeat.imageset/MusicPlayerControlRepeat@3x.png new file mode 100644 index 0000000000..e7b4c93b63 Binary files /dev/null and b/Images.xcassets/GlobalMusicPlayer/Repeat.imageset/MusicPlayerControlRepeat@3x.png differ diff --git a/Images.xcassets/GlobalMusicPlayer/RepeatOne.imageset/Contents.json b/Images.xcassets/GlobalMusicPlayer/RepeatOne.imageset/Contents.json index 5d67c8da2e..350a4f66cc 100644 --- a/Images.xcassets/GlobalMusicPlayer/RepeatOne.imageset/Contents.json +++ b/Images.xcassets/GlobalMusicPlayer/RepeatOne.imageset/Contents.json @@ -11,6 +11,7 @@ }, { "idiom" : "universal", + "filename" : "MusicPlayerControlRepeatOne@3x.png", "scale" : "3x" } ], diff --git a/Images.xcassets/GlobalMusicPlayer/RepeatOne.imageset/MusicPlayerControlRepeatOne@2x.png b/Images.xcassets/GlobalMusicPlayer/RepeatOne.imageset/MusicPlayerControlRepeatOne@2x.png index 47716a776f..b4f9caa534 100644 Binary files a/Images.xcassets/GlobalMusicPlayer/RepeatOne.imageset/MusicPlayerControlRepeatOne@2x.png and b/Images.xcassets/GlobalMusicPlayer/RepeatOne.imageset/MusicPlayerControlRepeatOne@2x.png differ diff --git a/Images.xcassets/GlobalMusicPlayer/RepeatOne.imageset/MusicPlayerControlRepeatOne@3x.png b/Images.xcassets/GlobalMusicPlayer/RepeatOne.imageset/MusicPlayerControlRepeatOne@3x.png new file mode 100644 index 0000000000..9792970b89 Binary files /dev/null and b/Images.xcassets/GlobalMusicPlayer/RepeatOne.imageset/MusicPlayerControlRepeatOne@3x.png differ diff --git a/Images.xcassets/GlobalMusicPlayer/Shuffle.imageset/Contents.json b/Images.xcassets/GlobalMusicPlayer/Shuffle.imageset/Contents.json index 1f16dcd26e..a76057444b 100644 --- a/Images.xcassets/GlobalMusicPlayer/Shuffle.imageset/Contents.json +++ b/Images.xcassets/GlobalMusicPlayer/Shuffle.imageset/Contents.json @@ -11,6 +11,7 @@ }, { "idiom" : "universal", + "filename" : "MusicPlayerControlShuffle@3x.png", "scale" : "3x" } ], diff --git a/Images.xcassets/GlobalMusicPlayer/Shuffle.imageset/MusicPlayerControlShuffle@2x.png b/Images.xcassets/GlobalMusicPlayer/Shuffle.imageset/MusicPlayerControlShuffle@2x.png index 5baf93d20e..7f73b91622 100644 Binary files a/Images.xcassets/GlobalMusicPlayer/Shuffle.imageset/MusicPlayerControlShuffle@2x.png and b/Images.xcassets/GlobalMusicPlayer/Shuffle.imageset/MusicPlayerControlShuffle@2x.png differ diff --git a/Images.xcassets/GlobalMusicPlayer/Shuffle.imageset/MusicPlayerControlShuffle@3x.png b/Images.xcassets/GlobalMusicPlayer/Shuffle.imageset/MusicPlayerControlShuffle@3x.png new file mode 100644 index 0000000000..facc408289 Binary files /dev/null and b/Images.xcassets/GlobalMusicPlayer/Shuffle.imageset/MusicPlayerControlShuffle@3x.png differ diff --git a/Images.xcassets/Settings/MenuIcons/Appearance.imageset/Contents.json b/Images.xcassets/Settings/MenuIcons/Appearance.imageset/Contents.json new file mode 100644 index 0000000000..d2881ee973 --- /dev/null +++ b/Images.xcassets/Settings/MenuIcons/Appearance.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_theme@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "ic_theme@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Settings/MenuIcons/Appearance.imageset/ic_theme@2x.png b/Images.xcassets/Settings/MenuIcons/Appearance.imageset/ic_theme@2x.png new file mode 100644 index 0000000000..4132369e5c Binary files /dev/null and b/Images.xcassets/Settings/MenuIcons/Appearance.imageset/ic_theme@2x.png differ diff --git a/Images.xcassets/Settings/MenuIcons/Appearance.imageset/ic_theme@3x.png b/Images.xcassets/Settings/MenuIcons/Appearance.imageset/ic_theme@3x.png new file mode 100644 index 0000000000..73ed287bed Binary files /dev/null and b/Images.xcassets/Settings/MenuIcons/Appearance.imageset/ic_theme@3x.png differ diff --git a/Images.xcassets/Settings/MenuIcons/Contents.json b/Images.xcassets/Settings/MenuIcons/Contents.json new file mode 100644 index 0000000000..38f0c81fc2 --- /dev/null +++ b/Images.xcassets/Settings/MenuIcons/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "provides-namespace" : true + } +} \ No newline at end of file diff --git a/Images.xcassets/Settings/MenuIcons/DataAndStorage.imageset/Contents.json b/Images.xcassets/Settings/MenuIcons/DataAndStorage.imageset/Contents.json new file mode 100644 index 0000000000..1c472e29d7 --- /dev/null +++ b/Images.xcassets/Settings/MenuIcons/DataAndStorage.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_data@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "ic_data@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Settings/MenuIcons/DataAndStorage.imageset/ic_data@2x.png b/Images.xcassets/Settings/MenuIcons/DataAndStorage.imageset/ic_data@2x.png new file mode 100644 index 0000000000..e80173af04 Binary files /dev/null and b/Images.xcassets/Settings/MenuIcons/DataAndStorage.imageset/ic_data@2x.png differ diff --git a/Images.xcassets/Settings/MenuIcons/DataAndStorage.imageset/ic_data@3x.png b/Images.xcassets/Settings/MenuIcons/DataAndStorage.imageset/ic_data@3x.png new file mode 100644 index 0000000000..66d9c308d6 Binary files /dev/null and b/Images.xcassets/Settings/MenuIcons/DataAndStorage.imageset/ic_data@3x.png differ diff --git a/Images.xcassets/Settings/MenuIcons/Faq.imageset/Contents.json b/Images.xcassets/Settings/MenuIcons/Faq.imageset/Contents.json new file mode 100644 index 0000000000..0475ec9367 --- /dev/null +++ b/Images.xcassets/Settings/MenuIcons/Faq.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_faq@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "ic_faq@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Settings/MenuIcons/Faq.imageset/ic_faq@2x.png b/Images.xcassets/Settings/MenuIcons/Faq.imageset/ic_faq@2x.png new file mode 100644 index 0000000000..795f5b6edf Binary files /dev/null and b/Images.xcassets/Settings/MenuIcons/Faq.imageset/ic_faq@2x.png differ diff --git a/Images.xcassets/Settings/MenuIcons/Faq.imageset/ic_faq@3x.png b/Images.xcassets/Settings/MenuIcons/Faq.imageset/ic_faq@3x.png new file mode 100644 index 0000000000..9aa0ed26cf Binary files /dev/null and b/Images.xcassets/Settings/MenuIcons/Faq.imageset/ic_faq@3x.png differ diff --git a/Images.xcassets/Settings/MenuIcons/Language.imageset/Contents.json b/Images.xcassets/Settings/MenuIcons/Language.imageset/Contents.json new file mode 100644 index 0000000000..875a865f5a --- /dev/null +++ b/Images.xcassets/Settings/MenuIcons/Language.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_language@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "ic_language@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Settings/MenuIcons/Language.imageset/ic_language@2x.png b/Images.xcassets/Settings/MenuIcons/Language.imageset/ic_language@2x.png new file mode 100644 index 0000000000..171bee5563 Binary files /dev/null and b/Images.xcassets/Settings/MenuIcons/Language.imageset/ic_language@2x.png differ diff --git a/Images.xcassets/Settings/MenuIcons/Language.imageset/ic_language@3x.png b/Images.xcassets/Settings/MenuIcons/Language.imageset/ic_language@3x.png new file mode 100644 index 0000000000..d9283f4f9e Binary files /dev/null and b/Images.xcassets/Settings/MenuIcons/Language.imageset/ic_language@3x.png differ diff --git a/Images.xcassets/Settings/MenuIcons/Notifications.imageset/Contents.json b/Images.xcassets/Settings/MenuIcons/Notifications.imageset/Contents.json new file mode 100644 index 0000000000..d12a7aea6f --- /dev/null +++ b/Images.xcassets/Settings/MenuIcons/Notifications.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_notifications@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "ic_notifications@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Settings/MenuIcons/Notifications.imageset/ic_notifications@2x.png b/Images.xcassets/Settings/MenuIcons/Notifications.imageset/ic_notifications@2x.png new file mode 100644 index 0000000000..2393036ea6 Binary files /dev/null and b/Images.xcassets/Settings/MenuIcons/Notifications.imageset/ic_notifications@2x.png differ diff --git a/Images.xcassets/Settings/MenuIcons/Notifications.imageset/ic_notifications@3x.png b/Images.xcassets/Settings/MenuIcons/Notifications.imageset/ic_notifications@3x.png new file mode 100644 index 0000000000..e37163f477 Binary files /dev/null and b/Images.xcassets/Settings/MenuIcons/Notifications.imageset/ic_notifications@3x.png differ diff --git a/Images.xcassets/Settings/MenuIcons/RecentCalls.imageset/Contents.json b/Images.xcassets/Settings/MenuIcons/RecentCalls.imageset/Contents.json new file mode 100644 index 0000000000..b1d8b94571 --- /dev/null +++ b/Images.xcassets/Settings/MenuIcons/RecentCalls.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_recentcalls@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "ic_recentcalls@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Settings/MenuIcons/RecentCalls.imageset/ic_recentcalls@2x.png b/Images.xcassets/Settings/MenuIcons/RecentCalls.imageset/ic_recentcalls@2x.png new file mode 100644 index 0000000000..c3d98063d2 Binary files /dev/null and b/Images.xcassets/Settings/MenuIcons/RecentCalls.imageset/ic_recentcalls@2x.png differ diff --git a/Images.xcassets/Settings/MenuIcons/RecentCalls.imageset/ic_recentcalls@3x.png b/Images.xcassets/Settings/MenuIcons/RecentCalls.imageset/ic_recentcalls@3x.png new file mode 100644 index 0000000000..50694ef7b9 Binary files /dev/null and b/Images.xcassets/Settings/MenuIcons/RecentCalls.imageset/ic_recentcalls@3x.png differ diff --git a/Images.xcassets/Settings/MenuIcons/SavedMessages.imageset/Contents.json b/Images.xcassets/Settings/MenuIcons/SavedMessages.imageset/Contents.json new file mode 100644 index 0000000000..f7f87484be --- /dev/null +++ b/Images.xcassets/Settings/MenuIcons/SavedMessages.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_savedmessages@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "ic_savedmessages@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Settings/MenuIcons/SavedMessages.imageset/ic_savedmessages@2x.png b/Images.xcassets/Settings/MenuIcons/SavedMessages.imageset/ic_savedmessages@2x.png new file mode 100644 index 0000000000..d3a840e68e Binary files /dev/null and b/Images.xcassets/Settings/MenuIcons/SavedMessages.imageset/ic_savedmessages@2x.png differ diff --git a/Images.xcassets/Settings/MenuIcons/SavedMessages.imageset/ic_savedmessages@3x.png b/Images.xcassets/Settings/MenuIcons/SavedMessages.imageset/ic_savedmessages@3x.png new file mode 100644 index 0000000000..d79f6680bd Binary files /dev/null and b/Images.xcassets/Settings/MenuIcons/SavedMessages.imageset/ic_savedmessages@3x.png differ diff --git a/Images.xcassets/Settings/MenuIcons/Security.imageset/Contents.json b/Images.xcassets/Settings/MenuIcons/Security.imageset/Contents.json new file mode 100644 index 0000000000..91894c8550 --- /dev/null +++ b/Images.xcassets/Settings/MenuIcons/Security.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_security@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "ic_security@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Settings/MenuIcons/Security.imageset/ic_security@2x.png b/Images.xcassets/Settings/MenuIcons/Security.imageset/ic_security@2x.png new file mode 100644 index 0000000000..6a2520959a Binary files /dev/null and b/Images.xcassets/Settings/MenuIcons/Security.imageset/ic_security@2x.png differ diff --git a/Images.xcassets/Settings/MenuIcons/Security.imageset/ic_security@3x.png b/Images.xcassets/Settings/MenuIcons/Security.imageset/ic_security@3x.png new file mode 100644 index 0000000000..76d4f8ac8d Binary files /dev/null and b/Images.xcassets/Settings/MenuIcons/Security.imageset/ic_security@3x.png differ diff --git a/Images.xcassets/Settings/MenuIcons/Stickers.imageset/Contents.json b/Images.xcassets/Settings/MenuIcons/Stickers.imageset/Contents.json new file mode 100644 index 0000000000..0f98e01e85 --- /dev/null +++ b/Images.xcassets/Settings/MenuIcons/Stickers.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_stickers@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "ic_stickers@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Settings/MenuIcons/Stickers.imageset/ic_stickers@2x.png b/Images.xcassets/Settings/MenuIcons/Stickers.imageset/ic_stickers@2x.png new file mode 100644 index 0000000000..d91a6ee87a Binary files /dev/null and b/Images.xcassets/Settings/MenuIcons/Stickers.imageset/ic_stickers@2x.png differ diff --git a/Images.xcassets/Settings/MenuIcons/Stickers.imageset/ic_stickers@3x.png b/Images.xcassets/Settings/MenuIcons/Stickers.imageset/ic_stickers@3x.png new file mode 100644 index 0000000000..e267f02a27 Binary files /dev/null and b/Images.xcassets/Settings/MenuIcons/Stickers.imageset/ic_stickers@3x.png differ diff --git a/Images.xcassets/Settings/MenuIcons/Support.imageset/Contents.json b/Images.xcassets/Settings/MenuIcons/Support.imageset/Contents.json new file mode 100644 index 0000000000..76198ef138 --- /dev/null +++ b/Images.xcassets/Settings/MenuIcons/Support.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_ask@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "ic_ask@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Settings/MenuIcons/Support.imageset/ic_ask@2x.png b/Images.xcassets/Settings/MenuIcons/Support.imageset/ic_ask@2x.png new file mode 100644 index 0000000000..fc07b165c2 Binary files /dev/null and b/Images.xcassets/Settings/MenuIcons/Support.imageset/ic_ask@2x.png differ diff --git a/Images.xcassets/Settings/MenuIcons/Support.imageset/ic_ask@3x.png b/Images.xcassets/Settings/MenuIcons/Support.imageset/ic_ask@3x.png new file mode 100644 index 0000000000..642a5cfdf6 Binary files /dev/null and b/Images.xcassets/Settings/MenuIcons/Support.imageset/ic_ask@3x.png differ diff --git a/TelegramUI.xcodeproj/project.pbxproj b/TelegramUI.xcodeproj/project.pbxproj index ee1330a58a..29f33359e1 100644 --- a/TelegramUI.xcodeproj/project.pbxproj +++ b/TelegramUI.xcodeproj/project.pbxproj @@ -23,6 +23,8 @@ D01776BA1F1D704F0044446D /* RadialStatusIconContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01776B91F1D704F0044446D /* RadialStatusIconContentNode.swift */; }; D01776BC1F1E21AF0044446D /* RadialStatusBackgroundNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01776BB1F1E21AF0044446D /* RadialStatusBackgroundNode.swift */; }; D01776BE1F1E76920044446D /* PeerMediaCollectionSectionsNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01776BD1F1E76920044446D /* PeerMediaCollectionSectionsNode.swift */; }; + D018477E1FFBC01E00075256 /* TimestampStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D018477D1FFBC01E00075256 /* TimestampStrings.swift */; }; + D01847801FFBD12E00075256 /* ChatListPresentationData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D018477F1FFBD12E00075256 /* ChatListPresentationData.swift */; }; D01A21AF1F39EA2E00DDA104 /* InstantPageTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01A21AE1F39EA2E00DDA104 /* InstantPageTheme.swift */; }; D01A21B11F3A050E00DDA104 /* InstantPageNavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01A21B01F3A050E00DDA104 /* InstantPageNavigationBar.swift */; }; D01BAA181ECC8E0000295217 /* CallListController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01BAA171ECC8E0000295217 /* CallListController.swift */; }; @@ -33,12 +35,31 @@ D01BAA221ECE076100295217 /* CallListCallItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01BAA211ECE076100295217 /* CallListCallItem.swift */; }; D01BAA241ECE173200295217 /* PresentationResourcesCallList.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01BAA231ECE173200295217 /* PresentationResourcesCallList.swift */; }; D01BAA581ED3283D00295217 /* AddFormatToStringWithRanges.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01BAA571ED3283D00295217 /* AddFormatToStringWithRanges.swift */; }; + D01C06AF1FBB461E001561AB /* JoinLinkPreviewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01C06AE1FBB461E001561AB /* JoinLinkPreviewController.swift */; }; + D01C06B11FBB4643001561AB /* JoinLinkPreviewControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01C06B01FBB4643001561AB /* JoinLinkPreviewControllerNode.swift */; }; + D01C06B31FBB49A5001561AB /* JoinLinkPreviewPeerContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01C06B21FBB49A5001561AB /* JoinLinkPreviewPeerContentNode.swift */; }; + D01C06B51FBB7720001561AB /* ChatMediaInputSettingsItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01C06B41FBB7720001561AB /* ChatMediaInputSettingsItem.swift */; }; + D01C06BA1FBBB076001561AB /* ItemListSelectableControlNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01C06B91FBBB076001561AB /* ItemListSelectableControlNode.swift */; }; + D01C06BC1FBBB0D8001561AB /* CheckNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01C06BB1FBBB0D8001561AB /* CheckNode.swift */; }; + D01C06BE1FBCAF06001561AB /* ChatMessageBubbleMosaicLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01C06BD1FBCAF06001561AB /* ChatMessageBubbleMosaicLayout.swift */; }; + D01C06C01FBF118A001561AB /* MessageUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01C06BF1FBF118A001561AB /* MessageUtils.swift */; }; D01C7F001EF9D45B008305F1 /* DeviceContactsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01C7EFF1EF9D45B008305F1 /* DeviceContactsManager.swift */; }; D01C99781F4F382C00DCFAF6 /* InstantPageSettingsItemTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01C99771F4F382C00DCFAF6 /* InstantPageSettingsItemTheme.swift */; }; + D0208AD51FA33D14001F0D5F /* RaiseToListenActivator.h in Headers */ = {isa = PBXBuildFile; fileRef = D0208AD31FA33D14001F0D5F /* RaiseToListenActivator.h */; }; + D0208AD61FA33D14001F0D5F /* RaiseToListenActivator.m in Sources */ = {isa = PBXBuildFile; fileRef = D0208AD41FA33D14001F0D5F /* RaiseToListenActivator.m */; }; + D0208AD91FA34017001F0D5F /* DeviceProximityManager.h in Headers */ = {isa = PBXBuildFile; fileRef = D0208AD71FA34017001F0D5F /* DeviceProximityManager.h */; }; + D0208ADA1FA34017001F0D5F /* DeviceProximityManager.m in Sources */ = {isa = PBXBuildFile; fileRef = D0208AD81FA34017001F0D5F /* DeviceProximityManager.m */; }; + D0208ADC1FA346A4001F0D5F /* RaiseToListen.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0208ADB1FA346A4001F0D5F /* RaiseToListen.swift */; }; + D020A9DA1FEAE675008C66F7 /* OverlayPlayerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D020A9D91FEAE675008C66F7 /* OverlayPlayerController.swift */; }; + D020A9DC1FEAE6E7008C66F7 /* OverlayPlayerControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D020A9DB1FEAE6E7008C66F7 /* OverlayPlayerControllerNode.swift */; }; D025A4231F79344500563950 /* FetchManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D025A4221F79344500563950 /* FetchManager.swift */; }; D025A4261F79428E00563950 /* FetchManagerLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D025A4251F79428E00563950 /* FetchManagerLocation.swift */; }; D02660941F34CE5C000E2DC5 /* LegacyLocationVenueIconDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02660931F34CE5C000E2DC5 /* LegacyLocationVenueIconDataSource.swift */; }; + D02F4AE91FCF370B004DFBAE /* ChatMessageInteractiveMediaBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02F4AE81FCF370B004DFBAE /* ChatMessageInteractiveMediaBadge.swift */; }; + D02F4AF01FD4C46D004DFBAE /* SystemVideoContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02F4AEF1FD4C46D004DFBAE /* SystemVideoContent.swift */; }; D033C60B1F0D306E0044EABA /* TelegramVideoNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D033C60A1F0D306E0044EABA /* TelegramVideoNode.swift */; }; + D0430B001FF4570500A35ADD /* WebController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0430AFF1FF4570500A35ADD /* WebController.swift */; }; + D0430B021FF4584100A35ADD /* WebControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0430B011FF4584100A35ADD /* WebControllerNode.swift */; }; D0471B491EFD59170074D609 /* BotCheckoutControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0471B481EFD59170074D609 /* BotCheckoutControllerNode.swift */; }; D0471B4B1EFD64AC0074D609 /* BotCheckoutHeaderItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0471B4A1EFD64AC0074D609 /* BotCheckoutHeaderItem.swift */; }; D0471B4F1EFD84600074D609 /* BotCheckoutPriceItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0471B4E1EFD84600074D609 /* BotCheckoutPriceItem.swift */; }; @@ -65,10 +86,19 @@ D04B4D111EEA04D400711AF6 /* MapResources.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04B4D101EEA04D400711AF6 /* MapResources.swift */; }; D04B4D131EEA0A6500711AF6 /* ChatMessageMapBubbleContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04B4D121EEA0A6500711AF6 /* ChatMessageMapBubbleContentNode.swift */; }; D04B4D661EEA993A00711AF6 /* LegacyLocationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04B4D651EEA993A00711AF6 /* LegacyLocationController.swift */; }; - D053B4351F19299100E2D58A /* ChatMessageItemContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = D053B4341F19299000E2D58A /* ChatMessageItemContent.swift */; }; + D04ECD721FFBF22B00DE9029 /* OpenUrl.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04ECD711FFBF22B00DE9029 /* OpenUrl.swift */; }; D053B4371F1A9CA000E2D58A /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D053B4361F1A9CA000E2D58A /* WebKit.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; D05677511F4CA0C2001B723E /* InstantPagePeerReferenceItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05677501F4CA0C2001B723E /* InstantPagePeerReferenceItem.swift */; }; D05677531F4CA0D0001B723E /* InstantPagePeerReferenceNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05677521F4CA0D0001B723E /* InstantPagePeerReferenceNode.swift */; }; + D056CD701FF147B000880D28 /* IconButtonNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D056CD6F1FF147B000880D28 /* IconButtonNode.swift */; }; + D056CD721FF1569800880D28 /* MusicPlaybackSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D056CD711FF1569800880D28 /* MusicPlaybackSettings.swift */; }; + D056CD741FF2996B00880D28 /* ExternalMusicAlbumArtResources.swift in Sources */ = {isa = PBXBuildFile; fileRef = D056CD731FF2996B00880D28 /* ExternalMusicAlbumArtResources.swift */; }; + D056CD761FF2A30900880D28 /* ChatSwipeToReplyRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D056CD751FF2A30900880D28 /* ChatSwipeToReplyRecognizer.swift */; }; + D056CD781FF2A6EE00880D28 /* ChatMessageSwipeToReplyNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D056CD771FF2A6EE00880D28 /* ChatMessageSwipeToReplyNode.swift */; }; + D056CD7A1FF3CC2A00880D28 /* ListMessagePlaybackOverlayNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D056CD791FF3CC2A00880D28 /* ListMessagePlaybackOverlayNode.swift */; }; + D056CD7C1FF3E92C00880D28 /* DirectionalPanGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D056CD7B1FF3E92C00880D28 /* DirectionalPanGestureRecognizer.swift */; }; + D057C5402004215B00990762 /* Lottie.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D057C5412004215B00990762 /* Lottie.framework */; }; + D057C5452004235000990762 /* mute.json in Resources */ = {isa = PBXBuildFile; fileRef = D057C5422004226C00990762 /* mute.json */; }; D0642EFC1F3E1E7B00792790 /* ChatHistoryNavigationButtons.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0642EFB1F3E1E7B00792790 /* ChatHistoryNavigationButtons.swift */; }; D064EF871F69A06F00AC0398 /* MessageContentKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = D064EF861F69A06F00AC0398 /* MessageContentKind.swift */; }; D0684A041F6C3AD50059F570 /* ChatListTypingNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0684A031F6C3AD50059F570 /* ChatListTypingNode.swift */; }; @@ -79,6 +109,7 @@ D06BEC8C1F65E30A0035A545 /* WebEmbedVideoContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06BEC8B1F65E30A0035A545 /* WebEmbedVideoContent.swift */; }; D06E0F8E1F79ABFB003CF3DD /* ChatLoadingNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06E0F8D1F79ABFB003CF3DD /* ChatLoadingNode.swift */; }; D06F1EA41F6C0A5D00FE8B74 /* ChatHistorySearchContainerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06F1EA31F6C0A5D00FE8B74 /* ChatHistorySearchContainerNode.swift */; }; + D073D2DB1FB61DA9009E1DA2 /* CallListSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D073D2DA1FB61DA9009E1DA2 /* CallListSettings.swift */; }; D0754D1E1EEDDF6200884F6E /* ChatMessageAttachedContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0754D1D1EEDDF6200884F6E /* ChatMessageAttachedContentNode.swift */; }; D0754D201EEDEBA000884F6E /* ChatMessageGameBubbleContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0754D1F1EEDEBA000884F6E /* ChatMessageGameBubbleContentNode.swift */; }; D0754D221EEDF89900884F6E /* ChatMessageInvoiceBubbleContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0754D211EEDF89900884F6E /* ChatMessageInvoiceBubbleContentNode.swift */; }; @@ -97,27 +128,57 @@ D087BFB31F748752003FD209 /* ShareControllerRecentPeersGridItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D087BFB21F748752003FD209 /* ShareControllerRecentPeersGridItem.swift */; }; D08803C51F6064CF00DD7951 /* TelegramUI.h in Headers */ = {isa = PBXBuildFile; fileRef = D0FC40821D5B8E7400261D9D /* TelegramUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; D089F78A1F4E0C14000E934D /* InstantPagePresentationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D089F7891F4E0C14000E934D /* InstantPagePresentationSettings.swift */; }; + D08BDF641FA37BEA009D08E1 /* ChatRecordingPreviewInputPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08BDF631FA37BEA009D08E1 /* ChatRecordingPreviewInputPanelNode.swift */; }; + D08BDF661FA8CB10009D08E1 /* EditSettingsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08BDF651FA8CB10009D08E1 /* EditSettingsController.swift */; }; + D091C7A41F8EBB1E00D7DE13 /* ChatPresentationData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D091C7A31F8EBB1E00D7DE13 /* ChatPresentationData.swift */; }; + D091C7A61F8ECEA300D7DE13 /* SettingsThemeWallpaperNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D091C7A51F8ECEA300D7DE13 /* SettingsThemeWallpaperNode.swift */; }; + D09250041FE5363D003F693F /* ExperimentalSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09250031FE5363D003F693F /* ExperimentalSettings.swift */; }; + D09250061FE5371D003F693F /* GlobalExperimentalSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09250051FE5371D003F693F /* GlobalExperimentalSettings.swift */; }; + D0943AF61FDAAE7E001522CC /* MultipleAvatarsNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0943AF51FDAAE7E001522CC /* MultipleAvatarsNode.swift */; }; + D0943AFE1FDAE454001522CC /* ChatMultipleAvatarsNavigationNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0943AFD1FDAE454001522CC /* ChatMultipleAvatarsNavigationNode.swift */; }; + D0943B001FDAE852001522CC /* ChatFeedNavigationInputPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0943AFF1FDAE852001522CC /* ChatFeedNavigationInputPanelNode.swift */; }; + D0943B051FDDFDA0001522CC /* OverlayInstantVideoNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0943B041FDDFDA0001522CC /* OverlayInstantVideoNode.swift */; }; + D0943B071FDEC529001522CC /* InstantVideoRadialStatusNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0943B061FDEC528001522CC /* InstantVideoRadialStatusNode.swift */; }; + D0943B081FDEEF27001522CC /* TGLogWrapper.h in Headers */ = {isa = PBXBuildFile; fileRef = D0EC6B821EB9F42D00EBF1C3 /* TGLogWrapper.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0943B0B1FDEF56E001522CC /* DarwinSpecific.mm in Sources */ = {isa = PBXBuildFile; fileRef = D0943B091FDEF56D001522CC /* DarwinSpecific.mm */; }; + D0943B0C1FDEF56E001522CC /* DarwinSpecific.h in Headers */ = {isa = PBXBuildFile; fileRef = D0943B0A1FDEF56E001522CC /* DarwinSpecific.h */; }; D099D74D1EEFEE1500A3128C /* GameController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D099D74C1EEFEE1500A3128C /* GameController.swift */; }; D099D74F1EEFEE6A00A3128C /* GameControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D099D74E1EEFEE6A00A3128C /* GameControllerNode.swift */; }; D099D7511EEFF91E00A3128C /* GameControllerTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D099D7501EEFF91E00A3128C /* GameControllerTitleView.swift */; }; + D09D886F1F86C11F00BEB4C9 /* AuthorizationTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09D886E1F86C11F00BEB4C9 /* AuthorizationTheme.swift */; }; + D09D88711F86D36700BEB4C9 /* CountryList.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09D88701F86D36700BEB4C9 /* CountryList.swift */; }; + D09D88731F86D56B00BEB4C9 /* AuthorizationLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09D88721F86D56B00BEB4C9 /* AuthorizationLayout.swift */; }; D09E637C1F0E7C28003444CD /* SharedMediaPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09E637B1F0E7C28003444CD /* SharedMediaPlayer.swift */; }; D09E637F1F0E8C9F003444CD /* PeerMessagesMediaPlaylist.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09E637E1F0E8C9F003444CD /* PeerMessagesMediaPlaylist.swift */; }; D09E63A21F0FA723003444CD /* EmbedVideoNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09E63A11F0FA723003444CD /* EmbedVideoNode.swift */; }; - D09E63A41F0FAB91003444CD /* EmbedGalleryVideoItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09E63A31F0FAB91003444CD /* EmbedGalleryVideoItem.swift */; }; D09E63AA1F0FC681003444CD /* PictureInPictureVideoControlsNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09E63A91F0FC681003444CD /* PictureInPictureVideoControlsNode.swift */; }; D09E63B01F1010FE003444CD /* Contacts.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D09E63AF1F1010FE003444CD /* Contacts.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; D09E63B21F11289A003444CD /* PassKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D09E63B11F11289A003444CD /* PassKit.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; + D0A24D281F92C27100584D24 /* DefaultDarkAccentPresentationTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A24D271F92C27100584D24 /* DefaultDarkAccentPresentationTheme.swift */; }; + D0A723541FC3B40E0094D167 /* RadialCheckContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A723531FC3B40E0094D167 /* RadialCheckContentNode.swift */; }; D0A8BBA11F61EE83000F03FD /* UniversalVideoCalleryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A8BBA01F61EE83000F03FD /* UniversalVideoCalleryItem.swift */; }; D0AA29AE1F72770D00C050AC /* ChatListItemStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0AA29AD1F72770D00C050AC /* ChatListItemStrings.swift */; }; + D0AA840C1FEB2BA3005C6E91 /* OverlayPlayerControlsNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0AA840B1FEB2BA3005C6E91 /* OverlayPlayerControlsNode.swift */; }; D0ACCB1A1EC5E0C20079D8BF /* CallControllerKeyPreviewNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ACCB191EC5E0C20079D8BF /* CallControllerKeyPreviewNode.swift */; }; D0ACCB1C1EC5FF4B0079D8BF /* ChatMessageCallBubbleContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ACCB1B1EC5FF4B0079D8BF /* ChatMessageCallBubbleContentNode.swift */; }; + D0AD02E81FFFDE5F00C1DCFF /* ChatMessageLiveLocationTimerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0AD02E71FFFDE5F00C1DCFF /* ChatMessageLiveLocationTimerNode.swift */; }; + D0AD02EA1FFFEBEF00C1DCFF /* ChatMessageLiveLocationTextNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0AD02E91FFFEBEF00C1DCFF /* ChatMessageLiveLocationTextNode.swift */; }; + D0AD02EC20000D0100C1DCFF /* ChatMessageLiveLocationPositionNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0AD02EB20000D0100C1DCFF /* ChatMessageLiveLocationPositionNode.swift */; }; + D0AF323A1FB1D8D60097362B /* ChatOverlayNavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0AF32391FB1D8D60097362B /* ChatOverlayNavigationBar.swift */; }; D0AF7C461ED84BC500CD8E0F /* LanguageSelectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0AF7C451ED84BC500CD8E0F /* LanguageSelectionController.swift */; }; D0AF7C4A1ED84CE000CD8E0F /* LanguageSelectionControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0AF7C491ED84CE000CD8E0F /* LanguageSelectionControllerNode.swift */; }; D0AFCC791F4C8D2C000720C6 /* InstantPageSlideshowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0AFCC781F4C8D2C000720C6 /* InstantPageSlideshowItem.swift */; }; D0AFCC7B1F4C8D39000720C6 /* InstantPageSlideshowItemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0AFCC7A1F4C8D39000720C6 /* InstantPageSlideshowItemNode.swift */; }; + D0B37C5C1F8D22AE004252DF /* ThemeSettingsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B37C5B1F8D22AE004252DF /* ThemeSettingsController.swift */; }; + D0B37C5E1F8D26A8004252DF /* ThemeSettingsChatPreviewItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B37C5D1F8D26A8004252DF /* ThemeSettingsChatPreviewItem.swift */; }; + D0B37C601F8D286E004252DF /* ThemeSettingsFontSizeItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B37C5F1F8D286E004252DF /* ThemeSettingsFontSizeItem.swift */; }; D0B4AF861EC111FA00D51FF6 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D0AB0BBA1D6719B5002C78E7 /* Images.xcassets */; }; D0B4AF881EC112EE00D51FF6 /* CallKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0B4AF871EC112ED00D51FF6 /* CallKit.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; D0B4AF8B1EC1133600D51FF6 /* CallKitIntergation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B4AF8A1EC1133600D51FF6 /* CallKitIntergation.swift */; }; + D0B85C1C1FF6F76000E795B4 /* AuthorizationSequencePasswordRecoveryController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B85C1B1FF6F76000E795B4 /* AuthorizationSequencePasswordRecoveryController.swift */; }; + D0B85C1E1FF6F76600E795B4 /* AuthorizationSequencePasswordRecoveryControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B85C1D1FF6F76600E795B4 /* AuthorizationSequencePasswordRecoveryControllerNode.swift */; }; + D0B85C211FF70BEC00E795B4 /* AuthorizationSequenceAwaitingAccountResetControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B85C201FF70BEC00E795B4 /* AuthorizationSequenceAwaitingAccountResetControllerNode.swift */; }; + D0B85C231FF70BF400E795B4 /* AuthorizationSequenceAwaitingAccountResetController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B85C221FF70BF400E795B4 /* AuthorizationSequenceAwaitingAccountResetController.swift */; }; D0BDB09B1F79C658002ABF2F /* SaveToCameraRoll.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BDB09A1F79C658002ABF2F /* SaveToCameraRoll.swift */; }; D0C0B5901EDB505E000F4D2C /* ActivityIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C0B58F1EDB505E000F4D2C /* ActivityIndicator.swift */; }; D0C0B5921EDC5A3B000F4D2C /* LinkHighlightingNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C0B5911EDC5A3B000F4D2C /* LinkHighlightingNode.swift */; }; @@ -126,12 +187,23 @@ D0C0B5B11EE1C421000F4D2C /* ChatDateSelectionSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C0B5B01EE1C421000F4D2C /* ChatDateSelectionSheet.swift */; }; D0C0B5B71EE1DEF1000F4D2C /* ThemeGridControllerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C0B5B61EE1DEF1000F4D2C /* ThemeGridControllerItem.swift */; }; D0C12A1D1F33A85600B3F66D /* ChatWallpaperBuiltin0.jpg in Resources */ = {isa = PBXBuildFile; fileRef = D0C12A1B1F33964900B3F66D /* ChatWallpaperBuiltin0.jpg */; }; + D0C12EB01F9A8D1300600BB2 /* ListMessageDateHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C12EAF1F9A8D1300600BB2 /* ListMessageDateHeader.swift */; }; + D0C26D571FDF2388004ABF18 /* OpenChatMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C26D561FDF2388004ABF18 /* OpenChatMessage.swift */; }; + D0C26D5E1FDF49E7004ABF18 /* DateFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C26D5D1FDF49E7004ABF18 /* DateFormat.swift */; }; D0C27B3B1F4B453700A4E170 /* InstantPagePlayableVideoItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C27B3A1F4B453700A4E170 /* InstantPagePlayableVideoItem.swift */; }; D0C27B3D1F4B454800A4E170 /* InstantPagePlayableVideoNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C27B3C1F4B454800A4E170 /* InstantPagePlayableVideoNode.swift */; }; + D0C44B641FC64D0500227BE0 /* SwipeToDismissGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C44B631FC64D0500227BE0 /* SwipeToDismissGestureRecognizer.swift */; }; D0CE67941F7DB45100FFB557 /* ChatMessageContactBubbleContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CE67931F7DB45100FFB557 /* ChatMessageContactBubbleContentNode.swift */; }; D0CE8CE51F6F354400AA2DB0 /* ChatTextInputAccessoryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CE8CE41F6F354400AA2DB0 /* ChatTextInputAccessoryItem.swift */; }; D0CE8CE71F6F35A300AA2DB0 /* ChatTextInputPanelState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CE8CE61F6F35A300AA2DB0 /* ChatTextInputPanelState.swift */; }; D0CE8CEC1F6FCCA300AA2DB0 /* TransformImageArguments.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CE8CEB1F6FCCA300AA2DB0 /* TransformImageArguments.swift */; }; + D0CFBB861FD715E700B65C0D /* LegacyHTTPOperationImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CFBB851FD715E700B65C0D /* LegacyHTTPOperationImpl.swift */; }; + D0CFBB911FD881A600B65C0D /* AudioRecordningToneData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CFBB901FD881A600B65C0D /* AudioRecordningToneData.swift */; }; + D0CFBB951FD8B05000B65C0D /* OverlayInstantVideoDecoration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CFBB941FD8B05000B65C0D /* OverlayInstantVideoDecoration.swift */; }; + D0CFBB971FD8B0F700B65C0D /* ChatBubbleInstantVideoDecoration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CFBB961FD8B0F700B65C0D /* ChatBubbleInstantVideoDecoration.swift */; }; + D0D4345C1F97CEAA00CC1806 /* ProxySettingsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D4345B1F97CEAA00CC1806 /* ProxySettingsController.swift */; }; + D0DE66061F9A51E200EF4AE9 /* GalleryHiddenMediaManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DE66051F9A51E200EF4AE9 /* GalleryHiddenMediaManager.swift */; }; + D0DFD5E21FCE2BA50039B3B1 /* CalculatingCacheSizeItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DFD5E11FCE2BA50039B3B1 /* CalculatingCacheSizeItem.swift */; }; D0E266FD1F66706500BFC79F /* ChatBubbleVideoDecoration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E266FC1F66706500BFC79F /* ChatBubbleVideoDecoration.swift */; }; D0E9B9E81EFEFB9500F079A4 /* BotPaymentDisclosureItemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E9B9E71EFEFB9500F079A4 /* BotPaymentDisclosureItemNode.swift */; }; D0E9B9EA1F00853C00F079A4 /* PhoneCountries.txt in Resources */ = {isa = PBXBuildFile; fileRef = D0E9B9E91F00853C00F079A4 /* PhoneCountries.txt */; }; @@ -506,7 +578,6 @@ D0EC6D621EB9F58800EBF1C3 /* ContactListNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00C7CD81E36B2DB0080C3D5 /* ContactListNode.swift */; }; D0EC6D631EB9F58800EBF1C3 /* ContactListActionItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D087751F1E3F595000A97350 /* ContactListActionItem.swift */; }; D0EC6D641EB9F58800EBF1C3 /* ContactsPeerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E6F1D6B8C340046BCD6 /* ContactsPeerItem.swift */; }; - D0EC6D651EB9F58800EBF1C3 /* ContactsVCardItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E721D6B8C340046BCD6 /* ContactsVCardItem.swift */; }; D0EC6D661EB9F58800EBF1C3 /* ContactsSectionHeaderAccessoryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E711D6B8C340046BCD6 /* ContactsSectionHeaderAccessoryItem.swift */; }; D0EC6D671EB9F58800EBF1C3 /* ContactListNameIndexHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08775131E3F4A7700A97350 /* ContactListNameIndexHeader.swift */; }; D0EC6D681EB9F58800EBF1C3 /* AuthorizationSequenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D049EAF21E44DE2500A2CD3A /* AuthorizationSequenceController.swift */; }; @@ -651,7 +722,6 @@ D0EC6DF61EB9F58900EBF1C3 /* PeerMediaCollectionControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B7F8E71D8A1F5F0045D939 /* PeerMediaCollectionControllerNode.swift */; }; D0EC6DF81EB9F58900EBF1C3 /* PeerMediaCollectionInterfaceState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DE77221D932043002B8809 /* PeerMediaCollectionInterfaceState.swift */; }; D0EC6DF91EB9F58900EBF1C3 /* PeerMediaCollectionInterfaceStateButtons.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DE77241D93225E002B8809 /* PeerMediaCollectionInterfaceStateButtons.swift */; }; - D0EC6DFA1EB9F58900EBF1C3 /* PeerMediaCollectionModeSelectionNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DE772A1D932E16002B8809 /* PeerMediaCollectionModeSelectionNode.swift */; }; D0EC6DFB1EB9F58900EBF1C3 /* AvatarGalleryController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0575AF91EA0FDA7006F2541 /* AvatarGalleryController.swift */; }; D0EC6DFC1EB9F58900EBF1C3 /* GalleryController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E501D6B8BDA0046BCD6 /* GalleryController.swift */; }; D0EC6DFD1EB9F58900EBF1C3 /* GalleryControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E511D6B8BDA0046BCD6 /* GalleryControllerNode.swift */; }; @@ -666,11 +736,9 @@ D0EC6E061EB9F58900EBF1C3 /* ChatDocumentGalleryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E5B1D6B8BF90046BCD6 /* ChatDocumentGalleryItem.swift */; }; D0EC6E071EB9F58900EBF1C3 /* ChatHoleGalleryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E5C1D6B8BF90046BCD6 /* ChatHoleGalleryItem.swift */; }; D0EC6E081EB9F58900EBF1C3 /* ChatImageGalleryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E5D1D6B8BF90046BCD6 /* ChatImageGalleryItem.swift */; }; - D0EC6E091EB9F58900EBF1C3 /* ChatVideoGalleryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E5E1D6B8BF90046BCD6 /* ChatVideoGalleryItem.swift */; }; D0EC6E0A1EB9F58900EBF1C3 /* ChatVideoGalleryItemScrubberView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E5F1D6B8BF90046BCD6 /* ChatVideoGalleryItemScrubberView.swift */; }; D0EC6E0B1EB9F58900EBF1C3 /* ZoomableContentGalleryItemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E601D6B8BF90046BCD6 /* ZoomableContentGalleryItemNode.swift */; }; D0EC6E0C1EB9F58900EBF1C3 /* ChatItemGalleryFooterContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D042C6891E8DAAB000C863B0 /* ChatItemGalleryFooterContentNode.swift */; }; - D0EC6E0D1EB9F58900EBF1C3 /* ChatItemGalleryItemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D042C68B1E8DAB7100C863B0 /* ChatItemGalleryItemNode.swift */; }; D0EC6E0E1EB9F58900EBF1C3 /* PeerAvatarImageGalleryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0575AFB1EA104A6006F2541 /* PeerAvatarImageGalleryItem.swift */; }; D0EC6E0F1EB9F58900EBF1C3 /* MapInputController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E681D6B8C160046BCD6 /* MapInputController.swift */; }; D0EC6E101EB9F58900EBF1C3 /* MapInputControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E691D6B8C160046BCD6 /* MapInputControllerNode.swift */; }; @@ -766,8 +834,6 @@ D0EC6E6C1EB9F58900EBF1C3 /* FeaturedStickerPacksController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E23DD71E805E2600B9B6D2 /* FeaturedStickerPacksController.swift */; }; D0EC6E6D1EB9F58900EBF1C3 /* ItemListStickerPackItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04791661E79A22000F18979 /* ItemListStickerPackItem.swift */; }; D0EC6E6E1EB9F58900EBF1C3 /* ArhivedStickerPacksController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E23DDC1E8081A200B9B6D2 /* ArhivedStickerPacksController.swift */; }; - D0EC6E6F1EB9F58900EBF1C3 /* SettingsThemesItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05BFB4D1EA96C5000909D38 /* SettingsThemesItem.swift */; }; - D0EC6E701EB9F58900EBF1C3 /* SettingsThemeWallpaperNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05BFB501EA96EDA00909D38 /* SettingsThemeWallpaperNode.swift */; }; D0EC6E711EB9F58900EBF1C3 /* ThemeGalleryController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05174A41EAA456600A1BF36 /* ThemeGalleryController.swift */; }; D0EC6E721EB9F58900EBF1C3 /* ThemeGalleryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05174A81EAA46E000A1BF36 /* ThemeGalleryItem.swift */; }; D0EC6E731EB9F58900EBF1C3 /* ThemeGalleryToolbarNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05174AA1EAA5B4700A1BF36 /* ThemeGalleryToolbarNode.swift */; }; @@ -919,6 +985,8 @@ D0F67FF21EE6B915000E5906 /* ChannelMembersSearchControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F67FF11EE6B915000E5906 /* ChannelMembersSearchControllerNode.swift */; }; D0F67FF41EE6C10F000E5906 /* ChannelMembersSearchContainerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F67FF31EE6C10F000E5906 /* ChannelMembersSearchContainerNode.swift */; }; D0F6800A1EE750EE000E5906 /* ChannelBannedMemberController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F680091EE750EE000E5906 /* ChannelBannedMemberController.swift */; }; + D0F9720F1FFE4BD5002595C8 /* notification.caf in Resources */ = {isa = PBXBuildFile; fileRef = D0C50E431E93FCD200F62E39 /* notification.caf */; }; + D0F972101FFE4BD5002595C8 /* MessageSent.caf in Resources */ = {isa = PBXBuildFile; fileRef = D073CE621DCBBE5D007511FD /* MessageSent.caf */; }; D0FB87B21F7C4C19004DE005 /* FetchMediaUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FB87B11F7C4C19004DE005 /* FetchMediaUtils.swift */; }; D0FC408E1D5B8E7500261D9D /* TelegramUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FC408D1D5B8E7500261D9D /* TelegramUITests.swift */; }; D0FC4FBB1F751E8900B7443F /* SelectablePeerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FC4FBA1F751E8900B7443F /* SelectablePeerNode.swift */; }; @@ -991,6 +1059,8 @@ D0177B7F1DFAE18500A5083A /* MediaPlayerTimeTextNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaPlayerTimeTextNode.swift; sourceTree = ""; }; D0177B811DFAEA5400A5083A /* MediaNavigationAccessoryItemListNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaNavigationAccessoryItemListNode.swift; sourceTree = ""; }; D0177B831DFB095000A5083A /* FileMediaResourceStatus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileMediaResourceStatus.swift; sourceTree = ""; }; + D018477D1FFBC01E00075256 /* TimestampStrings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimestampStrings.swift; sourceTree = ""; }; + D018477F1FFBD12E00075256 /* ChatListPresentationData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListPresentationData.swift; sourceTree = ""; }; D018D3311E6460B300C5E089 /* ChatUnblockInputPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatUnblockInputPanelNode.swift; sourceTree = ""; }; D018D3341E6489EC00C5E089 /* CreateChannelController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateChannelController.swift; sourceTree = ""; }; D01A21AE1F39EA2E00DDA104 /* InstantPageTheme.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPageTheme.swift; sourceTree = ""; }; @@ -1011,6 +1081,14 @@ D01BAA211ECE076100295217 /* CallListCallItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallListCallItem.swift; sourceTree = ""; }; D01BAA231ECE173200295217 /* PresentationResourcesCallList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresentationResourcesCallList.swift; sourceTree = ""; }; D01BAA571ED3283D00295217 /* AddFormatToStringWithRanges.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddFormatToStringWithRanges.swift; sourceTree = ""; }; + D01C06AE1FBB461E001561AB /* JoinLinkPreviewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinLinkPreviewController.swift; sourceTree = ""; }; + D01C06B01FBB4643001561AB /* JoinLinkPreviewControllerNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinLinkPreviewControllerNode.swift; sourceTree = ""; }; + D01C06B21FBB49A5001561AB /* JoinLinkPreviewPeerContentNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinLinkPreviewPeerContentNode.swift; sourceTree = ""; }; + D01C06B41FBB7720001561AB /* ChatMediaInputSettingsItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMediaInputSettingsItem.swift; sourceTree = ""; }; + D01C06B91FBBB076001561AB /* ItemListSelectableControlNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemListSelectableControlNode.swift; sourceTree = ""; }; + D01C06BB1FBBB0D8001561AB /* CheckNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckNode.swift; sourceTree = ""; }; + D01C06BD1FBCAF06001561AB /* ChatMessageBubbleMosaicLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageBubbleMosaicLayout.swift; sourceTree = ""; }; + D01C06BF1FBF118A001561AB /* MessageUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageUtils.swift; sourceTree = ""; }; D01C2AA01E758F90001F6F9A /* NavigateToChatController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigateToChatController.swift; sourceTree = ""; }; D01C2AAA1E75E010001F6F9A /* TwoStepVerificationUnlockController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TwoStepVerificationUnlockController.swift; sourceTree = ""; }; D01C2AAC1E768404001F6F9A /* Markdown.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Markdown.swift; sourceTree = ""; }; @@ -1018,6 +1096,13 @@ D01C99771F4F382C00DCFAF6 /* InstantPageSettingsItemTheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageSettingsItemTheme.swift; sourceTree = ""; }; D01D6BFB1E42AB3C006151C6 /* EmojiUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmojiUtils.swift; sourceTree = ""; }; D01F66121DE8903300345CBE /* ChatTextInputMediaRecordingButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatTextInputMediaRecordingButton.swift; sourceTree = ""; }; + D0208AD31FA33D14001F0D5F /* RaiseToListenActivator.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RaiseToListenActivator.h; sourceTree = ""; }; + D0208AD41FA33D14001F0D5F /* RaiseToListenActivator.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RaiseToListenActivator.m; sourceTree = ""; }; + D0208AD71FA34017001F0D5F /* DeviceProximityManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DeviceProximityManager.h; sourceTree = ""; }; + D0208AD81FA34017001F0D5F /* DeviceProximityManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DeviceProximityManager.m; sourceTree = ""; }; + D0208ADB1FA346A4001F0D5F /* RaiseToListen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RaiseToListen.swift; sourceTree = ""; }; + D020A9D91FEAE675008C66F7 /* OverlayPlayerController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlayPlayerController.swift; sourceTree = ""; }; + D020A9DB1FEAE6E7008C66F7 /* OverlayPlayerControllerNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlayPlayerControllerNode.swift; sourceTree = ""; }; D0215D371E040F53001A0B1E /* InstantPageNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPageNode.swift; sourceTree = ""; }; D0215D391E041003001A0B1E /* InstantPageLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPageLayout.swift; sourceTree = ""; }; D0215D3B1E041014001A0B1E /* InstantPageItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPageItem.swift; sourceTree = ""; }; @@ -1065,6 +1150,8 @@ D02958011D6F0D5F00360E5E /* TapLongTapOrDoubleTapGestureRecognizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TapLongTapOrDoubleTapGestureRecognizer.swift; sourceTree = ""; }; D02BE0701D91814C000889C2 /* ChatHistoryGridNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatHistoryGridNode.swift; sourceTree = ""; }; D02BE0761D9190EF000889C2 /* GridMessageItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridMessageItem.swift; sourceTree = ""; }; + D02F4AE81FCF370B004DFBAE /* ChatMessageInteractiveMediaBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageInteractiveMediaBadge.swift; sourceTree = ""; }; + D02F4AEF1FD4C46D004DFBAE /* SystemVideoContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemVideoContent.swift; sourceTree = ""; }; D03120F51DA534C1006A2A60 /* ItemListActionItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListActionItem.swift; sourceTree = ""; }; D033C60A1F0D306E0044EABA /* TelegramVideoNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelegramVideoNode.swift; sourceTree = ""; }; D033FEAA1E61BFC100644997 /* GroupAdminsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupAdminsController.swift; sourceTree = ""; }; @@ -1083,7 +1170,8 @@ D042C6851E8DA69D00C863B0 /* GalleryFooterContentNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GalleryFooterContentNode.swift; sourceTree = ""; }; D042C6871E8DA8C800C863B0 /* GalleryControllerPresentationState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GalleryControllerPresentationState.swift; sourceTree = ""; }; D042C6891E8DAAB000C863B0 /* ChatItemGalleryFooterContentNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatItemGalleryFooterContentNode.swift; sourceTree = ""; }; - D042C68B1E8DAB7100C863B0 /* ChatItemGalleryItemNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatItemGalleryItemNode.swift; sourceTree = ""; }; + D0430AFF1FF4570500A35ADD /* WebController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebController.swift; sourceTree = ""; }; + D0430B011FF4584100A35ADD /* WebControllerNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebControllerNode.swift; sourceTree = ""; }; D04662801E68BA64006FAFC4 /* TransformOutgoingMessageMedia.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransformOutgoingMessageMedia.swift; sourceTree = ""; }; D0471B481EFD59170074D609 /* BotCheckoutControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BotCheckoutControllerNode.swift; sourceTree = ""; }; D0471B4A1EFD64AC0074D609 /* BotCheckoutHeaderItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BotCheckoutHeaderItem.swift; sourceTree = ""; }; @@ -1188,6 +1276,7 @@ D04BB3271E48797500650E93 /* RMRootViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RMRootViewController.m; sourceTree = ""; }; D04BB3281E48797500650E93 /* texture_helper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = texture_helper.h; sourceTree = ""; }; D04BB3291E48797500650E93 /* texture_helper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = texture_helper.m; sourceTree = ""; }; + D04ECD711FFBF22B00DE9029 /* OpenUrl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenUrl.swift; sourceTree = ""; }; D050F2121E48B61500988324 /* PhoneInputNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhoneInputNode.swift; sourceTree = ""; }; D050F2151E48D9E000988324 /* AuthorizationSequenceCountrySelectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationSequenceCountrySelectionController.swift; sourceTree = ""; }; D050F2171E48D9EA00988324 /* AuthorizationSequenceCountrySelectionControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationSequenceCountrySelectionControllerNode.swift; sourceTree = ""; }; @@ -1204,7 +1293,6 @@ D0528E621E65BECA00E2FEF5 /* UserInfoController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserInfoController.swift; sourceTree = ""; }; D0528E671E65CB2C00E2FEF5 /* UsernameSetupController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UsernameSetupController.swift; sourceTree = ""; }; D0528E6C1E65DE3B00E2FEF5 /* WebpagePreviewAccessoryPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebpagePreviewAccessoryPanelNode.swift; sourceTree = ""; }; - D053B4341F19299000E2D58A /* ChatMessageItemContent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageItemContent.swift; sourceTree = ""; }; D053B4361F1A9CA000E2D58A /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; }; D0561DDE1E56FE8200E6B9E9 /* ItemListSingleLineInputItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListSingleLineInputItem.swift; sourceTree = ""; }; D0561DE01E57153000E6B9E9 /* ItemListActivityTextItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListActivityTextItem.swift; sourceTree = ""; }; @@ -1214,12 +1302,22 @@ D05677521F4CA0D0001B723E /* InstantPagePeerReferenceNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPagePeerReferenceNode.swift; sourceTree = ""; }; D0568AAC1DF198130022E7DA /* AudioWaveformNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioWaveformNode.swift; sourceTree = ""; }; D0568AAE1DF1B3920022E7DA /* HapticFeedback.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HapticFeedback.swift; sourceTree = ""; }; + D056CD6F1FF147B000880D28 /* IconButtonNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconButtonNode.swift; sourceTree = ""; }; + D056CD711FF1569800880D28 /* MusicPlaybackSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MusicPlaybackSettings.swift; sourceTree = ""; }; + D056CD731FF2996B00880D28 /* ExternalMusicAlbumArtResources.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExternalMusicAlbumArtResources.swift; sourceTree = ""; }; + D056CD751FF2A30900880D28 /* ChatSwipeToReplyRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatSwipeToReplyRecognizer.swift; sourceTree = ""; }; + D056CD771FF2A6EE00880D28 /* ChatMessageSwipeToReplyNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageSwipeToReplyNode.swift; sourceTree = ""; }; + D056CD791FF3CC2A00880D28 /* ListMessagePlaybackOverlayNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListMessagePlaybackOverlayNode.swift; sourceTree = ""; }; + D056CD7B1FF3E92C00880D28 /* DirectionalPanGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectionalPanGestureRecognizer.swift; sourceTree = ""; }; D0575AEA1E9FD579006F2541 /* ChatListTitleLockView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListTitleLockView.swift; sourceTree = ""; }; D0575AEC1E9FF1AD006F2541 /* ChatMediaInputTrendingPane.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMediaInputTrendingPane.swift; sourceTree = ""; }; D0575AEE1E9FF881006F2541 /* ChatMediaInputTrendingItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMediaInputTrendingItem.swift; sourceTree = ""; }; D0575AF61EA0ED4F006F2541 /* ChatMessageInstantVideoItemNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageInstantVideoItemNode.swift; sourceTree = ""; }; D0575AF91EA0FDA7006F2541 /* AvatarGalleryController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarGalleryController.swift; sourceTree = ""; }; D0575AFB1EA104A6006F2541 /* PeerAvatarImageGalleryItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerAvatarImageGalleryItem.swift; sourceTree = ""; }; + D057C52B2004202900990762 /* libLottie.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libLottie.a; sourceTree = BUILT_PRODUCTS_DIR; }; + D057C5412004215B00990762 /* Lottie.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Lottie.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D057C5422004226C00990762 /* mute.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; name = mute.json; path = TelegramUI/Resources/mute.json; sourceTree = ""; }; D05811931DD5F9380057C769 /* TelegramApplicationContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelegramApplicationContext.swift; sourceTree = ""; }; D058E0CE1E8AD57300A442DE /* VideoPlayerProxy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoPlayerProxy.swift; sourceTree = ""; }; D05A32DB1E6EFCC2002760B4 /* NumericFormat.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NumericFormat.swift; sourceTree = ""; }; @@ -1229,8 +1327,6 @@ D05A32ED1E6F25A0002760B4 /* ItemListRecentSessionItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListRecentSessionItem.swift; sourceTree = ""; }; D05B724C1E720393000BD3AD /* SelectivePrivacySettingsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectivePrivacySettingsController.swift; sourceTree = ""; }; D05B724F1E720597000BD3AD /* PresentationData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresentationData.swift; sourceTree = ""; }; - D05BFB4D1EA96C5000909D38 /* SettingsThemesItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsThemesItem.swift; sourceTree = ""; }; - D05BFB501EA96EDA00909D38 /* SettingsThemeWallpaperNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsThemeWallpaperNode.swift; sourceTree = ""; }; D05BFB5E1EAA22F900909D38 /* PresentationResourceKey.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresentationResourceKey.swift; sourceTree = ""; }; D05BFB601EAA27E200909D38 /* Wallpapers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Wallpapers.swift; sourceTree = ""; }; D0613FC71E5F8AB100202CDB /* ChannelInfoController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelInfoController.swift; sourceTree = ""; }; @@ -1260,6 +1356,7 @@ D073CE621DCBBE5D007511FD /* MessageSent.caf */ = {isa = PBXFileReference; lastKnownFileType = file; name = MessageSent.caf; path = TelegramUI/Sounds/MessageSent.caf; sourceTree = ""; }; D073CE641DCBC26B007511FD /* ServiceSoundManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServiceSoundManager.swift; sourceTree = ""; }; D073CE701DCBF23F007511FD /* DeclareEncodables.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeclareEncodables.swift; sourceTree = ""; }; + D073D2DA1FB61DA9009E1DA2 /* CallListSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallListSettings.swift; sourceTree = ""; }; D0754D1D1EEDDF6200884F6E /* ChatMessageAttachedContentNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageAttachedContentNode.swift; sourceTree = ""; }; D0754D1F1EEDEBA000884F6E /* ChatMessageGameBubbleContentNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageGameBubbleContentNode.swift; sourceTree = ""; }; D0754D211EEDF89900884F6E /* ChatMessageInvoiceBubbleContentNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageInvoiceBubbleContentNode.swift; sourceTree = ""; }; @@ -1306,6 +1403,8 @@ D087BFB01F745483003FD209 /* ShareSearchBarNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareSearchBarNode.swift; sourceTree = ""; }; D087BFB21F748752003FD209 /* ShareControllerRecentPeersGridItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareControllerRecentPeersGridItem.swift; sourceTree = ""; }; D089F7891F4E0C14000E934D /* InstantPagePresentationSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPagePresentationSettings.swift; sourceTree = ""; }; + D08BDF631FA37BEA009D08E1 /* ChatRecordingPreviewInputPanelNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatRecordingPreviewInputPanelNode.swift; sourceTree = ""; }; + D08BDF651FA8CB10009D08E1 /* EditSettingsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditSettingsController.swift; sourceTree = ""; }; D08C367E1DB66A820064C744 /* ChatMediaInputPanelEntries.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMediaInputPanelEntries.swift; sourceTree = ""; }; D08C36801DB66AAC0064C744 /* ChatMediaInputGridEntries.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMediaInputGridEntries.swift; sourceTree = ""; }; D08C36821DB66AD40064C744 /* ChatMediaInputStickerGridItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMediaInputStickerGridItem.swift; sourceTree = ""; }; @@ -1314,6 +1413,17 @@ D08D452B1D5E340300A7428A /* Postbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Postbox.framework; path = "../../../../Library/Developer/Xcode/DerivedData/Telegram-iOS-diblohvjozhgaifjcniwdlixlilx/Build/Products/Debug-iphonesimulator/Postbox.framework"; sourceTree = ""; }; D08D452C1D5E340300A7428A /* SwiftSignalKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftSignalKit.framework; path = "../../../../Library/Developer/Xcode/DerivedData/Telegram-iOS-diblohvjozhgaifjcniwdlixlilx/Build/Products/Debug-iphonesimulator/SwiftSignalKit.framework"; sourceTree = ""; }; D08D452D1D5E340300A7428A /* TelegramCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = TelegramCore.framework; path = "../../../../Library/Developer/Xcode/DerivedData/Telegram-iOS-diblohvjozhgaifjcniwdlixlilx/Build/Products/Debug-iphonesimulator/TelegramCore.framework"; sourceTree = ""; }; + D091C7A31F8EBB1E00D7DE13 /* ChatPresentationData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatPresentationData.swift; sourceTree = ""; }; + D091C7A51F8ECEA300D7DE13 /* SettingsThemeWallpaperNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsThemeWallpaperNode.swift; sourceTree = ""; }; + D09250031FE5363D003F693F /* ExperimentalSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExperimentalSettings.swift; sourceTree = ""; }; + D09250051FE5371D003F693F /* GlobalExperimentalSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalExperimentalSettings.swift; sourceTree = ""; }; + D0943AF51FDAAE7E001522CC /* MultipleAvatarsNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultipleAvatarsNode.swift; sourceTree = ""; }; + D0943AFD1FDAE454001522CC /* ChatMultipleAvatarsNavigationNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMultipleAvatarsNavigationNode.swift; sourceTree = ""; }; + D0943AFF1FDAE852001522CC /* ChatFeedNavigationInputPanelNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatFeedNavigationInputPanelNode.swift; sourceTree = ""; }; + D0943B041FDDFDA0001522CC /* OverlayInstantVideoNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlayInstantVideoNode.swift; sourceTree = ""; }; + D0943B061FDEC528001522CC /* InstantVideoRadialStatusNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantVideoRadialStatusNode.swift; sourceTree = ""; }; + D0943B091FDEF56D001522CC /* DarwinSpecific.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = DarwinSpecific.mm; sourceTree = ""; }; + D0943B0A1FDEF56E001522CC /* DarwinSpecific.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DarwinSpecific.h; sourceTree = ""; }; D096A4611EA681A90000A7AE /* PresentationsResourceCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresentationsResourceCache.swift; sourceTree = ""; }; D096A4631EA683C90000A7AE /* PresentationTheme.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresentationTheme.swift; sourceTree = ""; }; D096A47A1EA6A2F00000A7AE /* PresentationStrings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresentationStrings.swift; sourceTree = ""; }; @@ -1328,19 +1438,24 @@ D099EA2C1DE76782001AF5A8 /* PeerMessageManagedMediaId.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerMessageManagedMediaId.swift; sourceTree = ""; }; D099EA2E1DE775BB001AF5A8 /* ChatContextResultManagedMediaId.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatContextResultManagedMediaId.swift; sourceTree = ""; }; D09AEFD31E5BAF67005C1A8B /* ItemListTextEmptyStateItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListTextEmptyStateItem.swift; sourceTree = ""; }; + D09D886E1F86C11F00BEB4C9 /* AuthorizationTheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorizationTheme.swift; sourceTree = ""; }; + D09D88701F86D36700BEB4C9 /* CountryList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountryList.swift; sourceTree = ""; }; + D09D88721F86D56B00BEB4C9 /* AuthorizationLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorizationLayout.swift; sourceTree = ""; }; D09E637B1F0E7C28003444CD /* SharedMediaPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SharedMediaPlayer.swift; sourceTree = ""; }; D09E637E1F0E8C9F003444CD /* PeerMessagesMediaPlaylist.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerMessagesMediaPlaylist.swift; sourceTree = ""; }; D09E63A11F0FA723003444CD /* EmbedVideoNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmbedVideoNode.swift; sourceTree = ""; }; - D09E63A31F0FAB91003444CD /* EmbedGalleryVideoItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmbedGalleryVideoItem.swift; sourceTree = ""; }; D09E63A91F0FC681003444CD /* PictureInPictureVideoControlsNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PictureInPictureVideoControlsNode.swift; sourceTree = ""; }; D09E63AF1F1010FE003444CD /* Contacts.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Contacts.framework; path = System/Library/Frameworks/Contacts.framework; sourceTree = SDKROOT; }; D09E63B11F11289A003444CD /* PassKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PassKit.framework; path = System/Library/Frameworks/PassKit.framework; sourceTree = SDKROOT; }; D0A11BF91E7836C20081CE03 /* ChangePhoneNumberIntroController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChangePhoneNumberIntroController.swift; sourceTree = ""; }; D0A11BFB1E7840750081CE03 /* ChangePhoneNumberController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChangePhoneNumberController.swift; sourceTree = ""; }; D0A11BFD1E7840A50081CE03 /* ChangePhoneNumberControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChangePhoneNumberControllerNode.swift; sourceTree = ""; }; + D0A24D271F92C27100584D24 /* DefaultDarkAccentPresentationTheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultDarkAccentPresentationTheme.swift; sourceTree = ""; }; + D0A723531FC3B40E0094D167 /* RadialCheckContentNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadialCheckContentNode.swift; sourceTree = ""; }; D0A749961E3AA25200AD786E /* NotificationSoundSelection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationSoundSelection.swift; sourceTree = ""; }; D0A8BBA01F61EE83000F03FD /* UniversalVideoCalleryItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UniversalVideoCalleryItem.swift; sourceTree = ""; }; D0AA29AD1F72770D00C050AC /* ChatListItemStrings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListItemStrings.swift; sourceTree = ""; }; + D0AA840B1FEB2BA3005C6E91 /* OverlayPlayerControlsNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlayPlayerControlsNode.swift; sourceTree = ""; }; D0AB0BB01D6718DA002C78E7 /* libiconv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libiconv.tbd; path = usr/lib/libiconv.tbd; sourceTree = SDKROOT; }; D0AB0BB21D6718EB002C78E7 /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; }; D0AB0BB41D6718F1002C78E7 /* CoreMedia.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMedia.framework; path = System/Library/Frameworks/CoreMedia.framework; sourceTree = SDKROOT; }; @@ -1349,10 +1464,17 @@ D0AB0BBA1D6719B5002C78E7 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; D0ACCB191EC5E0C20079D8BF /* CallControllerKeyPreviewNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallControllerKeyPreviewNode.swift; sourceTree = ""; }; D0ACCB1B1EC5FF4B0079D8BF /* ChatMessageCallBubbleContentNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageCallBubbleContentNode.swift; sourceTree = ""; }; + D0AD02E71FFFDE5F00C1DCFF /* ChatMessageLiveLocationTimerNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageLiveLocationTimerNode.swift; sourceTree = ""; }; + D0AD02E91FFFEBEF00C1DCFF /* ChatMessageLiveLocationTextNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageLiveLocationTextNode.swift; sourceTree = ""; }; + D0AD02EB20000D0100C1DCFF /* ChatMessageLiveLocationPositionNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageLiveLocationPositionNode.swift; sourceTree = ""; }; + D0AF32391FB1D8D60097362B /* ChatOverlayNavigationBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatOverlayNavigationBar.swift; sourceTree = ""; }; D0AF7C451ED84BC500CD8E0F /* LanguageSelectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LanguageSelectionController.swift; sourceTree = ""; }; D0AF7C491ED84CE000CD8E0F /* LanguageSelectionControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LanguageSelectionControllerNode.swift; sourceTree = ""; }; D0AFCC781F4C8D2C000720C6 /* InstantPageSlideshowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageSlideshowItem.swift; sourceTree = ""; }; D0AFCC7A1F4C8D39000720C6 /* InstantPageSlideshowItemNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageSlideshowItemNode.swift; sourceTree = ""; }; + D0B37C5B1F8D22AE004252DF /* ThemeSettingsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeSettingsController.swift; sourceTree = ""; }; + D0B37C5D1F8D26A8004252DF /* ThemeSettingsChatPreviewItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeSettingsChatPreviewItem.swift; sourceTree = ""; }; + D0B37C5F1F8D286E004252DF /* ThemeSettingsFontSizeItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeSettingsFontSizeItem.swift; sourceTree = ""; }; D0B417C21D7DE54E004562A4 /* ChatPresentationInterfaceState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatPresentationInterfaceState.swift; sourceTree = ""; }; D0B4AF871EC112ED00D51FF6 /* CallKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CallKit.framework; path = System/Library/Frameworks/CallKit.framework; sourceTree = SDKROOT; }; D0B4AF8A1EC1133600D51FF6 /* CallKitIntergation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallKitIntergation.swift; sourceTree = ""; }; @@ -1364,6 +1486,10 @@ D0B843DA1DAAB138005F29E1 /* ItemListPeerActionItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListPeerActionItem.swift; sourceTree = ""; }; D0B844551DAC3AEE005F29E1 /* PresenceStrings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresenceStrings.swift; sourceTree = ""; }; D0B844571DAC44E8005F29E1 /* PeerPresenceStatusManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerPresenceStatusManager.swift; sourceTree = ""; }; + D0B85C1B1FF6F76000E795B4 /* AuthorizationSequencePasswordRecoveryController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorizationSequencePasswordRecoveryController.swift; sourceTree = ""; }; + D0B85C1D1FF6F76600E795B4 /* AuthorizationSequencePasswordRecoveryControllerNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorizationSequencePasswordRecoveryControllerNode.swift; sourceTree = ""; }; + D0B85C201FF70BEC00E795B4 /* AuthorizationSequenceAwaitingAccountResetControllerNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorizationSequenceAwaitingAccountResetControllerNode.swift; sourceTree = ""; }; + D0B85C221FF70BF400E795B4 /* AuthorizationSequenceAwaitingAccountResetController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorizationSequenceAwaitingAccountResetController.swift; sourceTree = ""; }; D0B98E7E1E575D2C008084B1 /* ChannelBlacklistController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelBlacklistController.swift; sourceTree = ""; }; D0BA6F821D784C520034826E /* ChatInputPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInputPanelNode.swift; sourceTree = ""; }; D0BA6F841D784ECD0034826E /* ChatInterfaceStateInputPanels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInterfaceStateInputPanels.swift; sourceTree = ""; }; @@ -1382,8 +1508,12 @@ D0C0B5B01EE1C421000F4D2C /* ChatDateSelectionSheet.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatDateSelectionSheet.swift; sourceTree = ""; }; D0C0B5B61EE1DEF1000F4D2C /* ThemeGridControllerItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThemeGridControllerItem.swift; sourceTree = ""; }; D0C12A1B1F33964900B3F66D /* ChatWallpaperBuiltin0.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; name = ChatWallpaperBuiltin0.jpg; path = TelegramUI/Resources/ChatWallpaperBuiltin0.jpg; sourceTree = ""; }; + D0C12EAF1F9A8D1300600BB2 /* ListMessageDateHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListMessageDateHeader.swift; sourceTree = ""; }; + D0C26D561FDF2388004ABF18 /* OpenChatMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenChatMessage.swift; sourceTree = ""; }; + D0C26D5D1FDF49E7004ABF18 /* DateFormat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateFormat.swift; sourceTree = ""; }; D0C27B3A1F4B453700A4E170 /* InstantPagePlayableVideoItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPagePlayableVideoItem.swift; sourceTree = ""; }; D0C27B3C1F4B454800A4E170 /* InstantPagePlayableVideoNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPagePlayableVideoNode.swift; sourceTree = ""; }; + D0C44B631FC64D0500227BE0 /* SwipeToDismissGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeToDismissGestureRecognizer.swift; sourceTree = ""; }; D0C48F431E81D5110075317D /* ChatEmptyItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatEmptyItem.swift; sourceTree = ""; }; D0C50DE81E93A07900F62E39 /* libtgvoip.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = libtgvoip.framework; path = "../libtgvoip/build/Debug-iphoneos/libtgvoip.framework"; sourceTree = ""; }; D0C50E281E93A33700F62E39 /* VoipDynamic.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = VoipDynamic.framework; path = "../../../../Library/Developer/Xcode/DerivedData/Telegram-iOS-diblohvjozhgaifjcniwdlixlilx/Build/Products/Debug-iphoneos/VoipDynamic.framework"; sourceTree = ""; }; @@ -1401,6 +1531,10 @@ D0CE8CE41F6F354400AA2DB0 /* ChatTextInputAccessoryItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatTextInputAccessoryItem.swift; sourceTree = ""; }; D0CE8CE61F6F35A300AA2DB0 /* ChatTextInputPanelState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatTextInputPanelState.swift; sourceTree = ""; }; D0CE8CEB1F6FCCA300AA2DB0 /* TransformImageArguments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransformImageArguments.swift; sourceTree = ""; }; + D0CFBB851FD715E700B65C0D /* LegacyHTTPOperationImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyHTTPOperationImpl.swift; sourceTree = ""; }; + D0CFBB901FD881A600B65C0D /* AudioRecordningToneData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecordningToneData.swift; sourceTree = ""; }; + D0CFBB941FD8B05000B65C0D /* OverlayInstantVideoDecoration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlayInstantVideoDecoration.swift; sourceTree = ""; }; + D0CFBB961FD8B0F700B65C0D /* ChatBubbleInstantVideoDecoration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatBubbleInstantVideoDecoration.swift; sourceTree = ""; }; D0D03AE21DECACB700220C46 /* ManagedAudioSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedAudioSession.swift; sourceTree = ""; }; D0D03AE41DECAE8900220C46 /* ManagedAudioRecorder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedAudioRecorder.swift; sourceTree = ""; }; D0D03AE81DECB0FE00220C46 /* diag_range.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = diag_range.c; sourceTree = ""; }; @@ -1436,6 +1570,7 @@ D0D2686B1D788F8200C422DA /* ChatTitleAccessoryPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatTitleAccessoryPanelNode.swift; sourceTree = ""; }; D0D2686D1D7898A900C422DA /* ChatMessageSelectionNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageSelectionNode.swift; sourceTree = ""; }; D0D268991D79CF9F00C422DA /* ChatPanelInterfaceInteraction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatPanelInterfaceInteraction.swift; sourceTree = ""; }; + D0D4345B1F97CEAA00CC1806 /* ProxySettingsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxySettingsController.swift; sourceTree = ""; }; D0D748051E7AF63800F4B1F6 /* StickerPackPreviewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StickerPackPreviewController.swift; sourceTree = ""; }; D0D748071E7AF64400F4B1F6 /* StickerPackPreviewControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StickerPackPreviewControllerNode.swift; sourceTree = ""; }; D0D7480E1E7B1BD600F4B1F6 /* StickerPackPreviewGridItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StickerPackPreviewGridItem.swift; sourceTree = ""; }; @@ -1445,13 +1580,13 @@ D0DC35451DE35805000195EB /* MentionChatInputPanelItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MentionChatInputPanelItem.swift; sourceTree = ""; }; D0DC35491DE366CD000195EB /* CommandChatInputContextPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CommandChatInputContextPanelNode.swift; sourceTree = ""; }; D0DC354B1DE366DE000195EB /* CommandChatInputPanelItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CommandChatInputPanelItem.swift; sourceTree = ""; }; + D0DE66051F9A51E200EF4AE9 /* GalleryHiddenMediaManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryHiddenMediaManager.swift; sourceTree = ""; }; D0DE76F61D91BA3D002B8809 /* GridHoleItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridHoleItem.swift; sourceTree = ""; }; D0DE76FF1D92F1EB002B8809 /* ChatTitleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatTitleView.swift; sourceTree = ""; }; D0DE77221D932043002B8809 /* PeerMediaCollectionInterfaceState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerMediaCollectionInterfaceState.swift; sourceTree = ""; }; D0DE77241D93225E002B8809 /* PeerMediaCollectionInterfaceStateButtons.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerMediaCollectionInterfaceStateButtons.swift; sourceTree = ""; }; D0DE77261D932627002B8809 /* ChatHistoryNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatHistoryNode.swift; sourceTree = ""; }; D0DE77281D932923002B8809 /* GridMessageSelectionNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridMessageSelectionNode.swift; sourceTree = ""; }; - D0DE772A1D932E16002B8809 /* PeerMediaCollectionModeSelectionNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerMediaCollectionModeSelectionNode.swift; sourceTree = ""; }; D0DE772F1D934DEF002B8809 /* ListMessageItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListMessageItem.swift; sourceTree = ""; }; D0DE77311D940295002B8809 /* ListMessageFileItemNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListMessageFileItemNode.swift; sourceTree = ""; }; D0DF0C941D81B063008AEB01 /* ChatInterfaceStateContextMenus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInterfaceStateContextMenus.swift; sourceTree = ""; }; @@ -1461,6 +1596,7 @@ D0DF0C9D1D82141F008AEB01 /* ChatInterfaceInputContexts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInterfaceInputContexts.swift; sourceTree = ""; }; D0DF0CA01D821B28008AEB01 /* HashtagChatInputPanelItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HashtagChatInputPanelItem.swift; sourceTree = ""; }; D0DF0CA31D82BCD0008AEB01 /* MentionChatInputContextPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MentionChatInputContextPanelNode.swift; sourceTree = ""; }; + D0DFD5E11FCE2BA50039B3B1 /* CalculatingCacheSizeItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalculatingCacheSizeItem.swift; sourceTree = ""; }; D0E23DD71E805E2600B9B6D2 /* FeaturedStickerPacksController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeaturedStickerPacksController.swift; sourceTree = ""; }; D0E23DDC1E8081A200B9B6D2 /* ArhivedStickerPacksController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArhivedStickerPacksController.swift; sourceTree = ""; }; D0E266FC1F66706500BFC79F /* ChatBubbleVideoDecoration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatBubbleVideoDecoration.swift; sourceTree = ""; }; @@ -2023,7 +2159,6 @@ D0F69E5B1D6B8BF90046BCD6 /* ChatDocumentGalleryItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatDocumentGalleryItem.swift; sourceTree = ""; }; D0F69E5C1D6B8BF90046BCD6 /* ChatHoleGalleryItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatHoleGalleryItem.swift; sourceTree = ""; }; D0F69E5D1D6B8BF90046BCD6 /* ChatImageGalleryItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatImageGalleryItem.swift; sourceTree = ""; }; - D0F69E5E1D6B8BF90046BCD6 /* ChatVideoGalleryItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatVideoGalleryItem.swift; sourceTree = ""; }; D0F69E5F1D6B8BF90046BCD6 /* ChatVideoGalleryItemScrubberView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatVideoGalleryItemScrubberView.swift; sourceTree = ""; }; D0F69E601D6B8BF90046BCD6 /* ZoomableContentGalleryItemNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZoomableContentGalleryItemNode.swift; sourceTree = ""; }; D0F69E681D6B8C160046BCD6 /* MapInputController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MapInputController.swift; sourceTree = ""; }; @@ -2033,7 +2168,6 @@ D0F69E6F1D6B8C340046BCD6 /* ContactsPeerItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactsPeerItem.swift; sourceTree = ""; }; D0F69E701D6B8C340046BCD6 /* ContactsSearchContainerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactsSearchContainerNode.swift; sourceTree = ""; }; D0F69E711D6B8C340046BCD6 /* ContactsSectionHeaderAccessoryItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactsSectionHeaderAccessoryItem.swift; sourceTree = ""; }; - D0F69E721D6B8C340046BCD6 /* ContactsVCardItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactsVCardItem.swift; sourceTree = ""; }; D0F69E7F1D6B8C850046BCD6 /* FastBlur.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FastBlur.h; sourceTree = ""; }; D0F69E801D6B8C850046BCD6 /* FastBlur.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FastBlur.m; sourceTree = ""; }; D0F69E811D6B8C850046BCD6 /* FFMpegSwResample.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FFMpegSwResample.h; sourceTree = ""; }; @@ -2084,6 +2218,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + D057C5402004215B00990762 /* Lottie.framework in Frameworks */, D07BCBFE1F2B792300ED97AA /* LegacyComponents.framework in Frameworks */, D053B4371F1A9CA000E2D58A /* WebKit.framework in Frameworks */, D09E63B21F11289A003444CD /* PassKit.framework in Frameworks */, @@ -2170,6 +2305,7 @@ D01776BB1F1E21AF0044446D /* RadialStatusBackgroundNode.swift */, D01776B41F1D6CCC0044446D /* RadialStatusContentNode.swift */, D01776B71F1D6FB30044446D /* RadialProgressContentNode.swift */, + D0A723531FC3B40E0094D167 /* RadialCheckContentNode.swift */, D01776B91F1D704F0044446D /* RadialStatusIconContentNode.swift */, ); name = "Radial Status"; @@ -2208,6 +2344,16 @@ name = "Call List"; sourceTree = ""; }; + D01C06AD1FBB45ED001561AB /* Join Link Preview */ = { + isa = PBXGroup; + children = ( + D01C06AE1FBB461E001561AB /* JoinLinkPreviewController.swift */, + D01C06B01FBB4643001561AB /* JoinLinkPreviewControllerNode.swift */, + D01C06B21FBB49A5001561AB /* JoinLinkPreviewPeerContentNode.swift */, + ); + name = "Join Link Preview"; + sourceTree = ""; + }; D01C7EFE1EF9D434008305F1 /* Device Contacts */ = { isa = PBXGroup; children = ( @@ -2216,6 +2362,16 @@ name = "Device Contacts"; sourceTree = ""; }; + D020A9D81FEAE611008C66F7 /* Player */ = { + isa = PBXGroup; + children = ( + D020A9D91FEAE675008C66F7 /* OverlayPlayerController.swift */, + D020A9DB1FEAE6E7008C66F7 /* OverlayPlayerControllerNode.swift */, + D0AA840B1FEB2BA3005C6E91 /* OverlayPlayerControlsNode.swift */, + ); + name = Player; + sourceTree = ""; + }; D021E0CC1DB4132E00C6B04F /* Input Nodes */ = { isa = PBXGroup; children = ( @@ -2237,6 +2393,7 @@ D049EAE51E44AD5600A2CD3A /* ChatMediaInputMetaSectionItemNode.swift */, D002A0DC1E9CD52A00A81812 /* ChatMediaInputRecentGifsItem.swift */, D0575AEE1E9FF881006F2541 /* ChatMediaInputTrendingItem.swift */, + D01C06B41FBB7720001561AB /* ChatMediaInputSettingsItem.swift */, D021E0E41DB55D0A00C6B04F /* ChatMediaInputStickerPackItem.swift */, D08C36821DB66AD40064C744 /* ChatMediaInputStickerGridItem.swift */, D0F02CDA1E981D240065DEE2 /* MultiplexedSoftwareVideoNode.swift */, @@ -2301,16 +2458,19 @@ name = "Accessory Panels"; sourceTree = ""; }; - D0471B471EFD4E920074D609 /* Payment */ = { + D0430AFE1FF456F400A35ADD /* Web */ = { isa = PBXGroup; children = ( + D0430AFF1FF4570500A35ADD /* WebController.swift */, + D0430B011FF4584100A35ADD /* WebControllerNode.swift */, ); - name = Payment; + name = Web; sourceTree = ""; }; D0471B521EFD8EBC0074D609 /* Resources */ = { isa = PBXGroup; children = ( + D057C5422004226C00990762 /* mute.json */, D0C12A1B1F33964900B3F66D /* ChatWallpaperBuiltin0.jpg */, D0E9BA681F056F4C00F079A4 /* Stripe */, D0E9B9E91F00853C00F079A4 /* PhoneCountries.txt */, @@ -2324,12 +2484,17 @@ children = ( D0477D1A1F617E5800412B44 /* UniversalVideoNode.swift */, D06BEC761F62F68B0035A545 /* OverlayUniversalVideoNode.swift */, + D0943B041FDDFDA0001522CC /* OverlayInstantVideoNode.swift */, D0477D1C1F617E8900412B44 /* NativeVideoContent.swift */, D06BEC8B1F65E30A0035A545 /* WebEmbedVideoContent.swift */, + D02F4AEF1FD4C46D004DFBAE /* SystemVideoContent.swift */, D0477D1E1F619E0700412B44 /* GalleryVideoDecoration.swift */, D06BEC891F6597A80035A545 /* OverlayVideoDecoration.swift */, D0E266FC1F66706500BFC79F /* ChatBubbleVideoDecoration.swift */, D0477D201F61A47600412B44 /* UniversalVideoContentManager.swift */, + D0CFBB941FD8B05000B65C0D /* OverlayInstantVideoDecoration.swift */, + D0CFBB961FD8B0F700B65C0D /* ChatBubbleInstantVideoDecoration.swift */, + D0943B061FDEC528001522CC /* InstantVideoRadialStatusNode.swift */, ); name = Video; sourceTree = ""; @@ -2507,6 +2672,7 @@ D050F2141E48D9C200988324 /* Country Selection */ = { isa = PBXGroup; children = ( + D09D88701F86D36700BEB4C9 /* CountryList.swift */, D050F2151E48D9E000988324 /* AuthorizationSequenceCountrySelectionController.swift */, D050F2171E48D9EA00988324 /* AuthorizationSequenceCountrySelectionControllerNode.swift */, ); @@ -2540,14 +2706,16 @@ D05BFB4F1EA96EC100909D38 /* Themes */ = { isa = PBXGroup; children = ( - D05BFB4D1EA96C5000909D38 /* SettingsThemesItem.swift */, - D05BFB501EA96EDA00909D38 /* SettingsThemeWallpaperNode.swift */, D05174A41EAA456600A1BF36 /* ThemeGalleryController.swift */, D05174A81EAA46E000A1BF36 /* ThemeGalleryItem.swift */, D05174AA1EAA5B4700A1BF36 /* ThemeGalleryToolbarNode.swift */, D0EC6B351EB88D0A00EBF1C3 /* ThemeGridController.swift */, + D091C7A51F8ECEA300D7DE13 /* SettingsThemeWallpaperNode.swift */, D0EC6B371EB88D1600EBF1C3 /* ThemeGridControllerNode.swift */, D0C0B5B61EE1DEF1000F4D2C /* ThemeGridControllerItem.swift */, + D0B37C5B1F8D22AE004252DF /* ThemeSettingsController.swift */, + D0B37C5D1F8D26A8004252DF /* ThemeSettingsChatPreviewItem.swift */, + D0B37C5F1F8D286E004252DF /* ThemeSettingsFontSizeItem.swift */, ); name = Themes; sourceTree = ""; @@ -2642,6 +2810,7 @@ D0D03B221DECB1AD00220C46 /* TGDataItem.m */, D0EB41FE1F30ED4F00838FE6 /* LegacyImageProcessors.h */, D0EB41FF1F30ED4F00838FE6 /* LegacyImageProcessors.m */, + D0CFBB851FD715E700B65C0D /* LegacyHTTPOperationImpl.swift */, ); name = "Legacy Components"; sourceTree = ""; @@ -2701,6 +2870,7 @@ D07CFF7C1DCA273400761F81 /* ChatListViewTransition.swift */, D07CFF7E1DCA308500761F81 /* ChatListNodeLocation.swift */, D0684A031F6C3AD50059F570 /* ChatListTypingNode.swift */, + D018477F1FFBD12E00075256 /* ChatListPresentationData.swift */, ); name = "Chat List Node"; sourceTree = ""; @@ -2715,6 +2885,9 @@ D0223A931EA5442C00211D94 /* VoiceCallSettings.swift */, D010C2C91EA7A59F00F41B96 /* PresentationThemeSettings.swift */, D089F7891F4E0C14000E934D /* InstantPagePresentationSettings.swift */, + D073D2DA1FB61DA9009E1DA2 /* CallListSettings.swift */, + D09250031FE5363D003F693F /* ExperimentalSettings.swift */, + D056CD711FF1569800880D28 /* MusicPlaybackSettings.swift */, ); name = Settings; sourceTree = ""; @@ -2725,7 +2898,6 @@ D00C7CD81E36B2DB0080C3D5 /* ContactListNode.swift */, D087751F1E3F595000A97350 /* ContactListActionItem.swift */, D0F69E6F1D6B8C340046BCD6 /* ContactsPeerItem.swift */, - D0F69E721D6B8C340046BCD6 /* ContactsVCardItem.swift */, D0F69E711D6B8C340046BCD6 /* ContactsSectionHeaderAccessoryItem.swift */, D08775131E3F4A7700A97350 /* ContactListNameIndexHeader.swift */, ); @@ -2754,6 +2926,8 @@ D08D45281D5E340200A7428A /* Frameworks */ = { isa = PBXGroup; children = ( + D057C5412004215B00990762 /* Lottie.framework */, + D057C52B2004202900990762 /* libLottie.a */, D07BCBFD1F2B792300ED97AA /* LegacyComponents.framework */, D053B4361F1A9CA000E2D58A /* WebKit.framework */, D09E63B11F11289A003444CD /* PassKit.framework */, @@ -2799,6 +2973,7 @@ D096A4631EA683C90000A7AE /* PresentationTheme.swift */, D010C2CB1EA7D74800F41B96 /* DefaultPresentationTheme.swift */, D05174BF1EAE3AD400A1BF36 /* DefaultDarkPresentationTheme.swift */, + D0A24D271F92C27100584D24 /* DefaultDarkAccentPresentationTheme.swift */, D010C2CD1EA7DDD600F41B96 /* DefaultPresentationStrings.swift */, D05BFB601EAA27E200909D38 /* Wallpapers.swift */, D06FFBA71EAFAC4F00CB53D4 /* PresentationThemeEssentialGraphics.swift */, @@ -2871,13 +3046,30 @@ D0B7F8E71D8A1F5F0045D939 /* PeerMediaCollectionControllerNode.swift */, D0DE77221D932043002B8809 /* PeerMediaCollectionInterfaceState.swift */, D0DE77241D93225E002B8809 /* PeerMediaCollectionInterfaceStateButtons.swift */, - D0DE772A1D932E16002B8809 /* PeerMediaCollectionModeSelectionNode.swift */, D01776BD1F1E76920044446D /* PeerMediaCollectionSectionsNode.swift */, D0EB5ADE1F798033004E89B6 /* PeerMediaCollectionEmptyNode.swift */, ); name = "Peer Media Collection"; sourceTree = ""; }; + D0B85C1A1FF6F74C00E795B4 /* Password Recovery */ = { + isa = PBXGroup; + children = ( + D0B85C1B1FF6F76000E795B4 /* AuthorizationSequencePasswordRecoveryController.swift */, + D0B85C1D1FF6F76600E795B4 /* AuthorizationSequencePasswordRecoveryControllerNode.swift */, + ); + name = "Password Recovery"; + sourceTree = ""; + }; + D0B85C1F1FF6F96D00E795B4 /* Awaiting Account Reset */ = { + isa = PBXGroup; + children = ( + D0B85C221FF70BF400E795B4 /* AuthorizationSequenceAwaitingAccountResetController.swift */, + D0B85C201FF70BEC00E795B4 /* AuthorizationSequenceAwaitingAccountResetControllerNode.swift */, + ); + name = "Awaiting Account Reset"; + sourceTree = ""; + }; D0BA6F811D784C3A0034826E /* Input Panels */ = { isa = PBXGroup; children = ( @@ -2890,6 +3082,9 @@ D0528E551E65750600E2FEF5 /* SecretChatHandshakeStatusInputPanelNode.swift */, D0528E571E65773300E2FEF5 /* DeleteChatInputPanelNode.swift */, D0C0B59E1EE082F5000F4D2C /* ChatSearchInputPanelNode.swift */, + D08BDF631FA37BEA009D08E1 /* ChatRecordingPreviewInputPanelNode.swift */, + D0943AFF1FDAE852001522CC /* ChatFeedNavigationInputPanelNode.swift */, + D0D2686A1D788F6600C422DA /* Title Accessory Panels */, ); name = "Input Panels"; sourceTree = ""; @@ -2950,6 +3145,8 @@ D0223A951EA54D0D00211D94 /* VoiceCallDataSavingController.swift */, D0223A9D1EA5732300211D94 /* NetworkUsageStatsController.swift */, D0FA35001EA6127000E56FFA /* StorageUsageController.swift */, + D0D4345B1F97CEAA00CC1806 /* ProxySettingsController.swift */, + D0DFD5E11FCE2BA50039B3B1 /* CalculatingCacheSizeItem.swift */, ); name = "Data and Storage"; sourceTree = ""; @@ -3090,6 +3287,8 @@ D0DE77311D940295002B8809 /* ListMessageFileItemNode.swift */, D07A7DA21D957671005BCD27 /* ListMessageSnippetItemNode.swift */, D02383831DDFA22C004018B6 /* ListMessageHoleItem.swift */, + D0C12EAF1F9A8D1300600BB2 /* ListMessageDateHeader.swift */, + D056CD791FF3CC2A00880D28 /* ListMessagePlaybackOverlayNode.swift */, ); name = List; sourceTree = ""; @@ -3163,6 +3362,7 @@ D021E0A81E3AACA200AF709C /* ItemListEditableItem.swift */, D021E0AA1E3B9E2700AF709C /* ItemListRevealOptionsNode.swift */, D08774F71E3DE7BF00A97350 /* ItemListEditableDeleteControlNode.swift */, + D01C06B91FBBB076001561AB /* ItemListSelectableControlNode.swift */, D0561DDE1E56FE8200E6B9E9 /* ItemListSingleLineInputItem.swift */, D0561DE51E57424700E6B9E9 /* ItemListMultilineTextItem.swift */, D0E305AE1E5BA8E000D7A3A2 /* ItemListLoadingIndicatorEmptyStateItem.swift */, @@ -3389,6 +3589,8 @@ D0EC6B771EB9F42D00EBF1C3 /* darwin */ = { isa = PBXGroup; children = ( + D0943B0A1FDEF56E001522CC /* DarwinSpecific.h */, + D0943B091FDEF56D001522CC /* DarwinSpecific.mm */, D0EC6B781EB9F42D00EBF1C3 /* AudioInputAudioUnit.cpp */, D0EC6B791EB9F42D00EBF1C3 /* AudioInputAudioUnit.h */, D0EC6B7C1EB9F42D00EBF1C3 /* AudioOutputAudioUnit.cpp */, @@ -4015,13 +4217,13 @@ name = "Peer Info"; sourceTree = ""; }; - D0F53BF51E79592300117362 /* Sign In */ = { + D0F53BF51E79592300117362 /* Sign Up */ = { isa = PBXGroup; children = ( D0F53BF61E79593500117362 /* AuthorizationSequenceSignUpController.swift */, D0F53BF81E79593F00117362 /* AuthorizationSequenceSignUpControllerNode.swift */, ); - name = "Sign In"; + name = "Sign Up"; sourceTree = ""; }; D0F69CCE1D6B87950046BCD6 /* Files */ = { @@ -4034,15 +4236,16 @@ D0F69DBB1D6B88330046BCD6 /* Media */ = { isa = PBXGroup; children = ( + D0F69CD61D6B87D30046BCD6 /* MediaManager.swift */, D099EA261DE765DB001AF5A8 /* ManagedMediaId.swift */, D099EA2C1DE76782001AF5A8 /* PeerMessageManagedMediaId.swift */, D080B27E1F4C7C6000AA3847 /* InstantPageManagedMediaId.swift */, D099EA2E1DE775BB001AF5A8 /* ChatContextResultManagedMediaId.swift */, D0F02CD81E97ED080065DEE2 /* RecentGifManagedMediaId.swift */, D099EA281DE76655001AF5A8 /* ManagedVideoNode.swift */, - D0F69CD61D6B87D30046BCD6 /* MediaManager.swift */, D0D03AE21DECACB700220C46 /* ManagedAudioSession.swift */, D0D03AE41DECAE8900220C46 /* ManagedAudioRecorder.swift */, + D0CFBB901FD881A600B65C0D /* AudioRecordningToneData.swift */, D0736F201DF41CFD00F2C02A /* ManagedAudioPlaylistPlayer.swift */, D0D03B2B1DED9B8900220C46 /* AudioWaveform.swift */, D00ADFDC1EBB73C200873D2E /* OverlayMediaManager.swift */, @@ -4109,6 +4312,8 @@ children = ( D0F69CFB1D6B87D30046BCD6 /* TouchDownGestureRecognizer.swift */, D02958011D6F0D5F00360E5E /* TapLongTapOrDoubleTapGestureRecognizer.swift */, + D0C44B631FC64D0500227BE0 /* SwipeToDismissGestureRecognizer.swift */, + D056CD7B1FF3E92C00880D28 /* DirectionalPanGestureRecognizer.swift */, ); name = Gestures; sourceTree = ""; @@ -4126,6 +4331,7 @@ D0F69DC21D6B89DA0046BCD6 /* TextNode.swift */, D0F69DC01D6B89D30046BCD6 /* ListSectionHeaderNode.swift */, D0F69DF71D6B8A880046BCD6 /* AvatarNode.swift */, + D0943AF51FDAAE7E001522CC /* MultipleAvatarsNode.swift */, D00219051DDD1C9E00BE708A /* ImageContainingNode.swift */, D0568AAC1DF198130022E7DA /* AudioWaveformNode.swift */, D0BC38621E3F9EFA0044D6FE /* EditableTokenListNode.swift */, @@ -4134,6 +4340,8 @@ D0C0B58F1EDB505E000F4D2C /* ActivityIndicator.swift */, D0C0B5911EDC5A3B000F4D2C /* LinkHighlightingNode.swift */, D0FC4FBA1F751E8900B7443F /* SelectablePeerNode.swift */, + D01C06BB1FBBB0D8001561AB /* CheckNode.swift */, + D056CD6F1FF147B000880D28 /* IconButtonNode.swift */, ); name = Nodes; sourceTree = ""; @@ -4160,7 +4368,6 @@ D0F69DE61D6B8A4E0046BCD6 /* Controllers */ = { isa = PBXGroup; children = ( - D0471B471EFD4E920074D609 /* Payment */, D0F69DE71D6B8A590046BCD6 /* Authorization */, D05174C11EAE582A00A1BF36 /* Root */, D0F69DF61D6B8A720046BCD6 /* Chat List */, @@ -4179,6 +4386,7 @@ D01BAA161ECC8DED00295217 /* Call List */, D0F69E791D6B8C3B0046BCD6 /* Settings */, D0C50E361E93CAF200F62E39 /* Notifications */, + D0430AFE1FF456F400A35ADD /* Web */, ); name = Controllers; sourceTree = ""; @@ -4186,13 +4394,17 @@ D0F69DE71D6B8A590046BCD6 /* Authorization */ = { isa = PBXGroup; children = ( + D09D886E1F86C11F00BEB4C9 /* AuthorizationTheme.swift */, + D09D88721F86D56B00BEB4C9 /* AuthorizationLayout.swift */, D049EAF21E44DE2500A2CD3A /* AuthorizationSequenceController.swift */, D04BB2B61E44E5BB00650E93 /* Splash */, D050F2141E48D9C200988324 /* Country Selection */, D04BB2B71E44E5CB00650E93 /* Phone Entry */, D04BB2BC1E44FD1300650E93 /* Code Entry */, D04BB2C11E45016800650E93 /* Password Entry */, - D0F53BF51E79592300117362 /* Sign In */, + D0B85C1A1FF6F74C00E795B4 /* Password Recovery */, + D0B85C1F1FF6F96D00E795B4 /* Awaiting Account Reset */, + D0F53BF51E79592300117362 /* Sign Up */, ); name = Authorization; sourceTree = ""; @@ -4238,6 +4450,7 @@ D0F69E111D6B8ACF0046BCD6 /* ChatHistoryEntry.swift */, D0F69E121D6B8ACF0046BCD6 /* ChatHistoryLocation.swift */, D0D268681D78865300C422DA /* ChatAvatarNavigationNode.swift */, + D0943AFD1FDAE454001522CC /* ChatMultipleAvatarsNavigationNode.swift */, D0DE76FF1D92F1EB002B8809 /* ChatTitleView.swift */, D02383761DDF16B2004018B6 /* ChatControllerTitlePanelNodeContainer.swift */, D00C7CE81E379B820080C3D5 /* ChatSecretAutoremoveTimerActionSheet.swift */, @@ -4246,13 +4459,13 @@ D0EF40DE1E73100D000DFCD4 /* ChatHistoryNavigationStack.swift */, D01C2AA01E758F90001F6F9A /* NavigateToChatController.swift */, D06E0F8D1F79ABFB003CF3DD /* ChatLoadingNode.swift */, + D0AF32391FB1D8D60097362B /* ChatOverlayNavigationBar.swift */, D0F69E181D6B8AD10046BCD6 /* Items */, D03ADB461D703250005A521C /* Interface State */, D03ADB491D704427005A521C /* Accessory Panels */, D021E0CC1DB4132E00C6B04F /* Input Nodes */, D0DF0C961D81FD87008AEB01 /* Input Context Panels */, D0BA6F811D784C3A0034826E /* Input Panels */, - D0D2686A1D788F6600C422DA /* Title Accessory Panels */, D0F69E441D6B8B850046BCD6 /* History Navigation */, ); name = Chat; @@ -4272,7 +4485,6 @@ D0F69E231D6B8B030046BCD6 /* ChatMessageInteractiveFileNode.swift */, D0F69E241D6B8B030046BCD6 /* ChatMessageInteractiveMediaNode.swift */, D0F69E251D6B8B030046BCD6 /* ChatMessageItem.swift */, - D053B4341F19299000E2D58A /* ChatMessageItemContent.swift */, D0F69E261D6B8B030046BCD6 /* ChatMessageItemView.swift */, D0F69E271D6B8B030046BCD6 /* ChatMessageMediaBubbleContentNode.swift */, D04B4D121EEA0A6500711AF6 /* ChatMessageMapBubbleContentNode.swift */, @@ -4296,6 +4508,14 @@ D0C48F431E81D5110075317D /* ChatEmptyItem.swift */, D02298361E0C34E900707F91 /* ChatMessageBackground.swift */, D0754D231EEE0F4100884F6E /* ChatMessageInteractiveMediaLabelNode.swift */, + D091C7A31F8EBB1E00D7DE13 /* ChatPresentationData.swift */, + D01C06BD1FBCAF06001561AB /* ChatMessageBubbleMosaicLayout.swift */, + D02F4AE81FCF370B004DFBAE /* ChatMessageInteractiveMediaBadge.swift */, + D056CD751FF2A30900880D28 /* ChatSwipeToReplyRecognizer.swift */, + D056CD771FF2A6EE00880D28 /* ChatMessageSwipeToReplyNode.swift */, + D0AD02E71FFFDE5F00C1DCFF /* ChatMessageLiveLocationTimerNode.swift */, + D0AD02E91FFFEBEF00C1DCFF /* ChatMessageLiveLocationTextNode.swift */, + D0AD02EB20000D0100C1DCFF /* ChatMessageLiveLocationPositionNode.swift */, ); name = Items; sourceTree = ""; @@ -4333,6 +4553,8 @@ D0F69E671D6B8C030046BCD6 /* Map Input */, D07827CC1E03F32C00071108 /* Instant Page */, D0D748041E7AF62000F4B1F6 /* Stickers */, + D020A9D81FEAE611008C66F7 /* Player */, + D01C06AD1FBB45ED001561AB /* Join Link Preview */, ); name = Media; sourceTree = ""; @@ -4348,6 +4570,7 @@ D0F69E541D6B8BDA0046BCD6 /* GalleryPagerNode.swift */, D042C6801E8D9A6700C863B0 /* GalleryFooterNode.swift */, D042C6851E8DA69D00C863B0 /* GalleryFooterContentNode.swift */, + D0DE66051F9A51E200EF4AE9 /* GalleryHiddenMediaManager.swift */, D00C7CDA1E3776CA0080C3D5 /* Secret Preview */, D0F69E5A1D6B8BDD0046BCD6 /* Items */, ); @@ -4360,12 +4583,9 @@ D0F69E5B1D6B8BF90046BCD6 /* ChatDocumentGalleryItem.swift */, D0F69E5C1D6B8BF90046BCD6 /* ChatHoleGalleryItem.swift */, D0F69E5D1D6B8BF90046BCD6 /* ChatImageGalleryItem.swift */, - D0F69E5E1D6B8BF90046BCD6 /* ChatVideoGalleryItem.swift */, D0F69E5F1D6B8BF90046BCD6 /* ChatVideoGalleryItemScrubberView.swift */, - D09E63A31F0FAB91003444CD /* EmbedGalleryVideoItem.swift */, D0F69E601D6B8BF90046BCD6 /* ZoomableContentGalleryItemNode.swift */, D042C6891E8DAAB000C863B0 /* ChatItemGalleryFooterContentNode.swift */, - D042C68B1E8DAB7100C863B0 /* ChatItemGalleryItemNode.swift */, D0575AFB1EA104A6006F2541 /* PeerAvatarImageGalleryItem.swift */, D0104F291F471DA6004E4881 /* InstantImageGalleryItem.swift */, D0104F2B1F471EEB004E4881 /* InstantPageGalleryFooterContentNode.swift */, @@ -4402,6 +4622,7 @@ D05BFB4F1EA96EC100909D38 /* Themes */, D0AF7C441ED84BB000CD8E0F /* Language Selection */, D01B279A1E39386C0022A4C0 /* SettingsController.swift */, + D08BDF651FA8CB10009D08E1 /* EditSettingsController.swift */, D01B279C1E394A500022A4C0 /* NotificationsAndSounds.swift */, D0A749961E3AA25200AD786E /* NotificationSoundSelection.swift */, D0CE1BD21E51BC6100404327 /* DebugController.swift */, @@ -4433,6 +4654,10 @@ D00C7CF61E37BF680080C3D5 /* SecretChatKeyVisualization.m */, D0EAE0A11EB212DE005296C1 /* NumberPluralizationForm.h */, D0EAE0A21EB212DE005296C1 /* NumberPluralizationForm.m */, + D0208AD31FA33D14001F0D5F /* RaiseToListenActivator.h */, + D0208AD41FA33D14001F0D5F /* RaiseToListenActivator.m */, + D0208AD71FA34017001F0D5F /* DeviceProximityManager.h */, + D0208AD81FA34017001F0D5F /* DeviceProximityManager.m */, ); name = "Supporting Files"; sourceTree = ""; @@ -4471,6 +4696,13 @@ D0FE4DDB1F09AD0400E8A0B3 /* PresentationSurfaceLevels.swift */, D064EF861F69A06F00AC0398 /* MessageContentKind.swift */, D0BDB09A1F79C658002ABF2F /* SaveToCameraRoll.swift */, + D0208ADB1FA346A4001F0D5F /* RaiseToListen.swift */, + D01C06BF1FBF118A001561AB /* MessageUtils.swift */, + D0C26D5D1FDF49E7004ABF18 /* DateFormat.swift */, + D09250051FE5371D003F693F /* GlobalExperimentalSettings.swift */, + D018477D1FFBC01E00075256 /* TimestampStrings.swift */, + D0C26D561FDF2388004ABF18 /* OpenChatMessage.swift */, + D04ECD711FFBF22B00DE9029 /* OpenUrl.swift */, ); name = Utils; sourceTree = ""; @@ -4490,6 +4722,7 @@ D06E4AC31E84806300627D1D /* FetchPhotoLibraryImageResource.swift */, D04B4D101EEA04D400711AF6 /* MapResources.swift */, D0FB87B11F7C4C19004DE005 /* FetchMediaUtils.swift */, + D056CD731FF2996B00880D28 /* ExternalMusicAlbumArtResources.swift */, ); name = Resources; sourceTree = ""; @@ -4588,6 +4821,7 @@ D0E9BADE1F0574D800F079A4 /* STPBackendAPIAdapter.h in Headers */, D0E9BAD11F0573C000F079A4 /* STPToken.h in Headers */, D0E9BAE71F0574FF00F079A4 /* STPCustomer.h in Headers */, + D0208AD51FA33D14001F0D5F /* RaiseToListenActivator.h in Headers */, D0E9BAE31F0574D800F079A4 /* STPBankAccountParams.h in Headers */, D0E9BA361F05585000F079A4 /* STPPhoneNumberValidator.h in Headers */, D0E9BA511F0559DA00F079A4 /* STPImageLibrary.h in Headers */, @@ -4598,6 +4832,8 @@ D0E9BA2A1F0557A600F079A4 /* STPFormEncoder.h in Headers */, D0E9BA321F05583A00F079A4 /* STPPostalCodeValidator.h in Headers */, D0EC6FE81EBA138700EBF1C3 /* ooura_fft.h in Headers */, + D0943B081FDEEF27001522CC /* TGLogWrapper.h in Headers */, + D0943B0C1FDEF56E001522CC /* DarwinSpecific.h in Headers */, D0E9BADC1F0574D800F079A4 /* PKPayment+Stripe.h in Headers */, D0E9BA491F0559B600F079A4 /* STPPaymentMethod.h in Headers */, D08803C51F6064CF00DD7951 /* TelegramUI.h in Headers */, @@ -4606,6 +4842,7 @@ D0E9BA291F0557A600F079A4 /* STPFormEncodable.h in Headers */, D0E9BA141F05574500F079A4 /* STPCardValidationState.h in Headers */, D0E9BA461F0559A500F079A4 /* NSDictionary+Stripe.h in Headers */, + D0208AD91FA34017001F0D5F /* DeviceProximityManager.h in Headers */, D0E9BAC61F05738600F079A4 /* STPAPIClient.h in Headers */, D00ADFD91EBA2E9D00873D2E /* OngoingCallThreadLocalContext.h in Headers */, D0E9BA531F0559DA00F079A4 /* STPImageLibrary+Private.h in Headers */, @@ -4722,12 +4959,14 @@ D0E9BAB51F056F4C00F079A4 /* stp_card_visa@2x.png in Resources */, D0E9BA941F056F4C00F079A4 /* stp_card_amex_template@3x.png in Resources */, D0E9BA961F056F4C00F079A4 /* stp_card_applepay@3x.png in Resources */, + D0F9720F1FFE4BD5002595C8 /* notification.caf in Resources */, D0E9BA9A1F056F4C00F079A4 /* stp_card_cvc@3x.png in Resources */, D0E9BA921F056F4C00F079A4 /* stp_card_amex@3x.png in Resources */, D0E9BA9F1F056F4C00F079A4 /* stp_card_diners_template@2x.png in Resources */, D0E9BA9E1F056F4C00F079A4 /* stp_card_diners@3x.png in Resources */, D0B4AF861EC111FA00D51FF6 /* Images.xcassets in Resources */, D0E9BAAD1F056F4C00F079A4 /* stp_card_jcb_template@2x.png in Resources */, + D0F972101FFE4BD5002595C8 /* MessageSent.caf in Resources */, D0E9BAB71F056F4C00F079A4 /* stp_card_visa_template@2x.png in Resources */, D0E9BA951F056F4C00F079A4 /* stp_card_applepay@2x.png in Resources */, D0E9BAA01F056F4C00F079A4 /* stp_card_diners_template@3x.png in Resources */, @@ -4757,6 +4996,7 @@ D0E9BA981F056F4C00F079A4 /* stp_card_applepay_template@3x.png in Resources */, D0E9BAA51F056F4C00F079A4 /* stp_card_form_applepay@2x.png in Resources */, D0E9BAB81F056F4C00F079A4 /* stp_card_visa_template@3x.png in Resources */, + D057C5452004235000990762 /* mute.json in Resources */, D0E9BA9B1F056F4C00F079A4 /* stp_card_cvc_amex@2x.png in Resources */, D0E9BAB61F056F4C00F079A4 /* stp_card_visa@3x.png in Resources */, D0E9BAA61F056F4C00F079A4 /* stp_card_form_applepay@3x.png in Resources */, @@ -4797,6 +5037,7 @@ D0EC6CB91EB9F58800EBF1C3 /* RMLoginViewController.m in Sources */, D0E9BA631F055AD200F079A4 /* BotPaymentCardInputItemNode.swift in Sources */, D0EC6CBA1EB9F58800EBF1C3 /* RMRootViewController.m in Sources */, + D0208ADC1FA346A4001F0D5F /* RaiseToListen.swift in Sources */, D0EB41F91F30E5B700838FE6 /* LegacyPeerAvatarPlaceholderDataSource.swift in Sources */, D0EC6CBB1EB9F58800EBF1C3 /* texture_helper.m in Sources */, D0EC6CBC1EB9F58800EBF1C3 /* LegacyController.swift in Sources */, @@ -4806,15 +5047,19 @@ D0C0B5B71EE1DEF1000F4D2C /* ThemeGridControllerItem.swift in Sources */, D0EC6CBE1EB9F58800EBF1C3 /* TelegramInitializeLegacyComponents.swift in Sources */, D0EC6CBF1EB9F58800EBF1C3 /* LegacyAttachmentMenu.swift in Sources */, + D0943B001FDAE852001522CC /* ChatFeedNavigationInputPanelNode.swift in Sources */, + D0B37C601F8D286E004252DF /* ThemeSettingsFontSizeItem.swift in Sources */, D0EC6FB51EBA114200EBF1C3 /* echo_cancellation.cc in Sources */, D0EC6CC01EB9F58800EBF1C3 /* LegacyMediaPickers.swift in Sources */, D0AF7C4A1ED84CE000CD8E0F /* LanguageSelectionControllerNode.swift in Sources */, D0EC6CC11EB9F58800EBF1C3 /* LegacyCamera.swift in Sources */, D0754D1E1EEDDF6200884F6E /* ChatMessageAttachedContentNode.swift in Sources */, D0E9BAC71F05738600F079A4 /* STPAPIClient.m in Sources */, + D0CFBB911FD881A600B65C0D /* AudioRecordningToneData.swift in Sources */, D0EC6FEC1EBA182B00EBF1C3 /* aec_core_sse2.cc in Sources */, D089F78A1F4E0C14000E934D /* InstantPagePresentationSettings.swift in Sources */, D01776B51F1D6CCC0044446D /* RadialStatusContentNode.swift in Sources */, + D02F4AF01FD4C46D004DFBAE /* SystemVideoContent.swift in Sources */, D0EC6FC81EBA135100EBF1C3 /* cross_correlation.c in Sources */, D0477D1F1F619E0700412B44 /* GalleryVideoDecoration.swift in Sources */, D01C99781F4F382C00DCFAF6 /* InstantPageSettingsItemTheme.swift in Sources */, @@ -4826,6 +5071,7 @@ D0EC6FA41EBA10EA00EBF1C3 /* stringutils.cc in Sources */, D0EC6CC61EB9F58800EBF1C3 /* PresenceStrings.swift in Sources */, D0EC6CC71EB9F58800EBF1C3 /* PeerNotificationSoundStrings.swift in Sources */, + D01C06C01FBF118A001561AB /* MessageUtils.swift in Sources */, D0104F281F47171F004E4881 /* InstantPageGalleryController.swift in Sources */, D0EC6CC81EB9F58800EBF1C3 /* ProgressiveImage.swift in Sources */, D0EC6CC91EB9F58800EBF1C3 /* WebP.swift in Sources */, @@ -4837,6 +5083,7 @@ D0EC6CCC1EB9F58800EBF1C3 /* ServiceSoundManager.swift in Sources */, D0EC6FCC1EBA135100EBF1C3 /* downsample_fast.c in Sources */, D0EC6CCD1EB9F58800EBF1C3 /* DeclareEncodables.swift in Sources */, + D0CFBB951FD8B05000B65C0D /* OverlayInstantVideoDecoration.swift in Sources */, D0EC6CCE1EB9F58800EBF1C3 /* TelegramApplicationContext.swift in Sources */, D0EC6FB41EBA114200EBF1C3 /* aec_resampler.cc in Sources */, D0EC6CCF1EB9F58800EBF1C3 /* GeoLocation.swift in Sources */, @@ -4869,11 +5116,13 @@ D0A8BBA11F61EE83000F03FD /* UniversalVideoCalleryItem.swift in Sources */, D0EC6CDE1EB9F58800EBF1C3 /* CompomentsThemes.swift in Sources */, D0642EFC1F3E1E7B00792790 /* ChatHistoryNavigationButtons.swift in Sources */, + D01C06BC1FBBB0D8001561AB /* CheckNode.swift in Sources */, D0EC6CDF1EB9F58800EBF1C3 /* PresentationResourceKey.swift in Sources */, D0EC6CE01EB9F58800EBF1C3 /* PresentationResourcesRootController.swift in Sources */, D0EC6CE11EB9F58800EBF1C3 /* PresentationResourcesItemList.swift in Sources */, D0EC6CE21EB9F58800EBF1C3 /* PresentationResourcesChatList.swift in Sources */, D0EC6CE31EB9F58800EBF1C3 /* PresentationResourcesChat.swift in Sources */, + D0AA840C1FEB2BA3005C6E91 /* OverlayPlayerControlsNode.swift in Sources */, D0F67FF21EE6B915000E5906 /* ChannelMembersSearchControllerNode.swift in Sources */, D0EC6CE41EB9F58800EBF1C3 /* PresentationData.swift in Sources */, D0EC6CE51EB9F58800EBF1C3 /* PresentationStrings.swift in Sources */, @@ -4892,6 +5141,7 @@ D01BAA1E1ECC931D00295217 /* CallListNodeEntries.swift in Sources */, D0EC6FEF1EBA18E800EBF1C3 /* VoIPController.cpp in Sources */, D0EC6CED1EB9F58800EBF1C3 /* StringPluralization.swift in Sources */, + D020A9DC1FEAE6E7008C66F7 /* OverlayPlayerControllerNode.swift in Sources */, D0EC6FC61EBA135100EBF1C3 /* complex_fft.c in Sources */, D0EC6CEE1EB9F58800EBF1C3 /* InAppNotificationSettings.swift in Sources */, D0EC6CEF1EB9F58800EBF1C3 /* PresentationPasscodeSettings.swift in Sources */, @@ -4903,13 +5153,16 @@ D0EC6EB51EBA0FC100EBF1C3 /* Resampler.cpp in Sources */, D0EC6CF41EB9F58800EBF1C3 /* ManagedMediaId.swift in Sources */, D0EC6FD41EBA135100EBF1C3 /* ilbc_specific_functions.c in Sources */, + D0CFBB971FD8B0F700B65C0D /* ChatBubbleInstantVideoDecoration.swift in Sources */, D0471B601EFEB5A70074D609 /* BotPaymentTextItemNode.swift in Sources */, D0EC6EB11EBA0FBB00EBF1C3 /* OpusDecoder.cpp in Sources */, D0EC6CF51EB9F58800EBF1C3 /* PeerMessageManagedMediaId.swift in Sources */, D0E9BA521F0559DA00F079A4 /* STPImageLibrary.m in Sources */, D0EC6CF61EB9F58800EBF1C3 /* ChatContextResultManagedMediaId.swift in Sources */, + D04ECD721FFBF22B00DE9029 /* OpenUrl.swift in Sources */, D0EC6EB01EBA0FBB00EBF1C3 /* NetworkSocket.cpp in Sources */, D04B4D661EEA993A00711AF6 /* LegacyLocationController.swift in Sources */, + D056CD7A1FF3CC2A00880D28 /* ListMessagePlaybackOverlayNode.swift in Sources */, D0EC6CF71EB9F58800EBF1C3 /* RecentGifManagedMediaId.swift in Sources */, D0EC6CF81EB9F58800EBF1C3 /* ManagedVideoNode.swift in Sources */, D0ACCB1A1EC5E0C20079D8BF /* CallControllerKeyPreviewNode.swift in Sources */, @@ -4936,6 +5189,7 @@ D0EC6D091EB9F58800EBF1C3 /* info.c in Sources */, D0EC6D0A1EB9F58800EBF1C3 /* internal.c in Sources */, D0EC6D0B1EB9F58800EBF1C3 /* opusfile.c in Sources */, + D01847801FFBD12E00075256 /* ChatListPresentationData.swift in Sources */, D0B4AF8B1EC1133600D51FF6 /* CallKitIntergation.swift in Sources */, D0FFF7F61F55B82500BEBC01 /* InstantPageAudioItem.swift in Sources */, D0EC6D0C1EB9F58800EBF1C3 /* stream.c in Sources */, @@ -4952,24 +5206,28 @@ D0EC6D151EB9F58800EBF1C3 /* MediaTrackFrameBuffer.swift in Sources */, D0EC6D161EB9F58800EBF1C3 /* MediaTrackFrameDecoder.swift in Sources */, D0EC6FE61EBA135100EBF1C3 /* sqrt_of_one_minus_x_squared.c in Sources */, + D056CD701FF147B000880D28 /* IconButtonNode.swift in Sources */, D0EC6D171EB9F58800EBF1C3 /* FFMpegAudioFrameDecoder.swift in Sources */, D0EC6D181EB9F58800EBF1C3 /* FFMpegMediaFrameSource.swift in Sources */, D0EC6D191EB9F58800EBF1C3 /* FFMpegMediaFrameSourceContext.swift in Sources */, D079FCE11F05C9380038FADE /* BotReceiptControllerNode.swift in Sources */, D0EC6FF61EBA195F00EBF1C3 /* TGLogWrapper.m in Sources */, D0EC6EAD1EBA0FBB00EBF1C3 /* JitterBuffer.cpp in Sources */, - D053B4351F19299100E2D58A /* ChatMessageItemContent.swift in Sources */, D0EC6D1A1EB9F58800EBF1C3 /* FFMpegMediaFrameSourceContextHelpers.swift in Sources */, D0EC6D1B1EB9F58800EBF1C3 /* FFMpegMediaVideoFrameDecoder.swift in Sources */, + D01C06AF1FBB461E001561AB /* JoinLinkPreviewController.swift in Sources */, D0EC6D1C1EB9F58800EBF1C3 /* FFMpegMediaPassthroughVideoFrameDecoder.swift in Sources */, D0EC6D1D1EB9F58800EBF1C3 /* FFMpegPacket.swift in Sources */, + D01C06B11FBB4643001561AB /* JoinLinkPreviewControllerNode.swift in Sources */, D0EC6D1E1EB9F58800EBF1C3 /* MediaPlayerScrubbingNode.swift in Sources */, D0C0B59B1EE019E5000F4D2C /* ChatSearchNavigationContentNode.swift in Sources */, D0EC6D1F1EB9F58800EBF1C3 /* MediaPlayerTimeTextNode.swift in Sources */, D0EC6D201EB9F58800EBF1C3 /* PeerAvatar.swift in Sources */, D0EC6D211EB9F58800EBF1C3 /* FileResources.swift in Sources */, D0EC6FB31EBA114200EBF1C3 /* aec_core_neon.cc in Sources */, + D056CD781FF2A6EE00880D28 /* ChatMessageSwipeToReplyNode.swift in Sources */, D0CE67941F7DB45100FFB557 /* ChatMessageContactBubbleContentNode.swift in Sources */, + D0943AFE1FDAE454001522CC /* ChatMultipleAvatarsNavigationNode.swift in Sources */, D0EC6D221EB9F58800EBF1C3 /* PhotoResources.swift in Sources */, D0EC6FC11EBA132B00EBF1C3 /* nsx_core_neon.c in Sources */, D0EC6FA81EBA111500EBF1C3 /* sparse_fir_filter.cc in Sources */, @@ -4981,7 +5239,10 @@ D0EC6D261EB9F58800EBF1C3 /* TransformOutgoingMessageMedia.swift in Sources */, D0EC6D271EB9F58800EBF1C3 /* FetchResource.swift in Sources */, D048EA8F1F4F2A9C00188713 /* InstantPageSettingsItemNode.swift in Sources */, + D056CD721FF1569800880D28 /* MusicPlaybackSettings.swift in Sources */, D0EC6FDE1EBA135100EBF1C3 /* resample_by_2.c in Sources */, + D0A723541FC3B40E0094D167 /* RadialCheckContentNode.swift in Sources */, + D09D88731F86D56B00BEB4C9 /* AuthorizationLayout.swift in Sources */, D0EC6D281EB9F58800EBF1C3 /* MediaResources.swift in Sources */, D0E9BA671F055B5500F079A4 /* BotCheckoutNativeCardEntryControllerNode.swift in Sources */, D0EC6D291EB9F58800EBF1C3 /* FetchVideoMediaResource.swift in Sources */, @@ -5021,6 +5282,7 @@ D0EB41F71F30D4A800838FE6 /* LegacyMediaLocations.swift in Sources */, D0EC6D3B1EB9F58800EBF1C3 /* EditableTokenListNode.swift in Sources */, D0EC6D3C1EB9F58800EBF1C3 /* PhoneInputNode.swift in Sources */, + D0AD02EC20000D0100C1DCFF /* ChatMessageLiveLocationPositionNode.swift in Sources */, D0EC6D3D1EB9F58800EBF1C3 /* ProgressNavigationButtonNode.swift in Sources */, D0FFF7FF1F55C55800BEBC01 /* InstantPageMediaAudioPlaylist.swift in Sources */, D01BAA581ED3283D00295217 /* AddFormatToStringWithRanges.swift in Sources */, @@ -5046,8 +5308,10 @@ D0EC6D531EB9F58800EBF1C3 /* ChatHistoryViewForLocation.swift in Sources */, D06BB8821F58994B0084FC30 /* LegacyInstantVideoController.swift in Sources */, D0EC6D541EB9F58800EBF1C3 /* ChatHistoryEntriesForView.swift in Sources */, + D0943B051FDDFDA0001522CC /* OverlayInstantVideoNode.swift in Sources */, D0EC6D551EB9F58800EBF1C3 /* PreparedChatHistoryViewTransition.swift in Sources */, D0EB41FB1F30E75000838FE6 /* LegacyImageDownloadActor.swift in Sources */, + D0208ADA1FA34017001F0D5F /* DeviceProximityManager.m in Sources */, D0EC6D561EB9F58800EBF1C3 /* ChatHistoryNode.swift in Sources */, D0EC6FAF1EBA112600EBF1C3 /* delay_estimator_wrapper.cc in Sources */, D0EC6D571EB9F58800EBF1C3 /* ChatHistoryListNode.swift in Sources */, @@ -5056,6 +5320,7 @@ D06E0F8E1F79ABFB003CF3DD /* ChatLoadingNode.swift in Sources */, D0EC6D5A1EB9F58800EBF1C3 /* ListMessageItem.swift in Sources */, D0EC6D5B1EB9F58800EBF1C3 /* ListMessageNode.swift in Sources */, + D0DE66061F9A51E200EF4AE9 /* GalleryHiddenMediaManager.swift in Sources */, D0EC6D5C1EB9F58800EBF1C3 /* ListMessageFileItemNode.swift in Sources */, D0EC6FA91EBA111500EBF1C3 /* wav_file.cc in Sources */, D0471B561EFDB40F0074D609 /* BotCheckoutActionButton.swift in Sources */, @@ -5073,7 +5338,7 @@ D0EC6D621EB9F58800EBF1C3 /* ContactListNode.swift in Sources */, D0EC6D631EB9F58800EBF1C3 /* ContactListActionItem.swift in Sources */, D0EC6D641EB9F58800EBF1C3 /* ContactsPeerItem.swift in Sources */, - D0EC6D651EB9F58800EBF1C3 /* ContactsVCardItem.swift in Sources */, + D0B85C1E1FF6F76600E795B4 /* AuthorizationSequencePasswordRecoveryControllerNode.swift in Sources */, D00BED201F73F60F00922292 /* ShareSearchContainerNode.swift in Sources */, D0CE8CEC1F6FCCA300AA2DB0 /* TransformImageArguments.swift in Sources */, D0EC6D661EB9F58800EBF1C3 /* ContactsSectionHeaderAccessoryItem.swift in Sources */, @@ -5089,14 +5354,18 @@ D0EC6D6E1EB9F58800EBF1C3 /* AuthorizationSequencePhoneEntryControllerNode.swift in Sources */, D0EC6FD61EBA135100EBF1C3 /* lpc_to_refl_coef.c in Sources */, D0EC6FAB1EBA112600EBF1C3 /* splitting_filter.cc in Sources */, + D0B85C211FF70BEC00E795B4 /* AuthorizationSequenceAwaitingAccountResetControllerNode.swift in Sources */, D0EC6D6F1EB9F58800EBF1C3 /* AuthorizationSequenceCodeEntryController.swift in Sources */, + D0C26D5E1FDF49E7004ABF18 /* DateFormat.swift in Sources */, D0EC6D701EB9F58800EBF1C3 /* AuthorizationSequenceCodeEntryControllerNode.swift in Sources */, D0E9BA4D1F0559C700F079A4 /* NSString+Stripe_CardBrands.m in Sources */, D099D7511EEFF91E00A3128C /* GameControllerTitleView.swift in Sources */, D0EC6FBB1EBA114200EBF1C3 /* digital_agc.c in Sources */, D0EC6D711EB9F58800EBF1C3 /* AuthorizationSequencePasswordEntryController.swift in Sources */, D0EC6D721EB9F58800EBF1C3 /* AuthorizationSequencePasswordEntryControllerNode.swift in Sources */, + D09D886F1F86C11F00BEB4C9 /* AuthorizationTheme.swift in Sources */, D0EC6D731EB9F58800EBF1C3 /* AuthorizationSequenceSignUpController.swift in Sources */, + D0C12EB01F9A8D1300600BB2 /* ListMessageDateHeader.swift in Sources */, D0E9BA5D1F055A3300F079A4 /* STPBINRange.m in Sources */, D0EC6D741EB9F58800EBF1C3 /* AuthorizationSequenceSignUpControllerNode.swift in Sources */, D0EC6FE21EBA135100EBF1C3 /* spl_inl.c in Sources */, @@ -5115,12 +5384,14 @@ D0EC6D7B1EB9F58800EBF1C3 /* ChatListRecentPeersListItem.swift in Sources */, D0EC6D7C1EB9F58800EBF1C3 /* HorizontalPeerItem.swift in Sources */, D0E9BADD1F0574D800F079A4 /* PKPayment+Stripe.m in Sources */, + D0AD02E81FFFDE5F00C1DCFF /* ChatMessageLiveLocationTimerNode.swift in Sources */, D0EC6D7D1EB9F58800EBF1C3 /* ChatListSearchRecentPeersNode.swift in Sources */, D0EC6D7E1EB9F58800EBF1C3 /* ChatListSearchItemHeader.swift in Sources */, D0EC6D7F1EB9F58800EBF1C3 /* HashtagSearchController.swift in Sources */, D0EC6D801EB9F58800EBF1C3 /* HashtagSearchControllerNode.swift in Sources */, D0EC6D811EB9F58800EBF1C3 /* ChatController.swift in Sources */, D0FFF7F81F55B83600BEBC01 /* InstantPageAudioNode.swift in Sources */, + D0B37C5E1F8D26A8004252DF /* ThemeSettingsChatPreviewItem.swift in Sources */, D0EC6D821EB9F58800EBF1C3 /* ChatControllerInteraction.swift in Sources */, D0EC6D831EB9F58800EBF1C3 /* ChatControllerNode.swift in Sources */, D0E9BA231F05577700F079A4 /* STPCard.m in Sources */, @@ -5140,8 +5411,10 @@ D0FE4DE61F0BA58A00E8A0B3 /* OverlayMediaItemNode.swift in Sources */, D0EC6D901EB9F58900EBF1C3 /* ChatMessageBubbleContentNode.swift in Sources */, D0EC6D911EB9F58900EBF1C3 /* ChatMessageBubbleItemNode.swift in Sources */, + D0B85C1C1FF6F76000E795B4 /* AuthorizationSequencePasswordRecoveryController.swift in Sources */, D0471B511EFD872F0074D609 /* CurrencyFormat.swift in Sources */, D0EC6D921EB9F58900EBF1C3 /* ChatMessageDateAndStatusNode.swift in Sources */, + D01C06B31FBB49A5001561AB /* JoinLinkPreviewPeerContentNode.swift in Sources */, D0EC6D931EB9F58900EBF1C3 /* ChatMessageFileBubbleContentNode.swift in Sources */, D0EC6D941EB9F58900EBF1C3 /* ChatMessageForwardInfoNode.swift in Sources */, D0104F2C1F471EEB004E4881 /* InstantPageGalleryFooterContentNode.swift in Sources */, @@ -5154,6 +5427,8 @@ D0EC6D961EB9F58900EBF1C3 /* ChatMessageInteractiveMediaNode.swift in Sources */, D0EC6D971EB9F58900EBF1C3 /* ChatMessageItem.swift in Sources */, D0EC6D981EB9F58900EBF1C3 /* ChatMessageItemView.swift in Sources */, + D073D2DB1FB61DA9009E1DA2 /* CallListSettings.swift in Sources */, + D0430B001FF4570500A35ADD /* WebController.swift in Sources */, D0EC6D991EB9F58900EBF1C3 /* ChatMessageMediaBubbleContentNode.swift in Sources */, D0EC6D9A1EB9F58900EBF1C3 /* ChatMessageReplyInfoNode.swift in Sources */, D0FE4DE41F0AEBB900E8A0B3 /* SharedVideoContextManager.swift in Sources */, @@ -5177,15 +5452,20 @@ D0EC6EB81EBA0FD000EBF1C3 /* AudioOutputAudioUnit.cpp in Sources */, D0EC6DA61EB9F58900EBF1C3 /* ChatEmptyItem.swift in Sources */, D0E9BAE41F0574D800F079A4 /* STPBankAccountParams.m in Sources */, + D0943B0B1FDEF56E001522CC /* DarwinSpecific.mm in Sources */, D06887F01F72DEE6000AB936 /* ShareInputFieldNode.swift in Sources */, D0EC6DA71EB9F58900EBF1C3 /* ChatMessageBackground.swift in Sources */, D0F0AAE01EC1E12C005EE2A5 /* PresentationCall.swift in Sources */, D0EC6DA81EB9F58900EBF1C3 /* ChatInterfaceState.swift in Sources */, + D018477E1FFBC01E00075256 /* TimestampStrings.swift in Sources */, + D08BDF661FA8CB10009D08E1 /* EditSettingsController.swift in Sources */, D0EC6DA91EB9F58900EBF1C3 /* ChatPresentationInterfaceState.swift in Sources */, D0EC6DAA1EB9F58900EBF1C3 /* ChatPanelInterfaceInteraction.swift in Sources */, D00FF2091F4E2414006FA332 /* InstantPageSettingsNode.swift in Sources */, D0EC6DAB1EB9F58900EBF1C3 /* ChatInterfaceStateAccessoryPanels.swift in Sources */, D0EC6DAC1EB9F58900EBF1C3 /* ChatInterfaceStateInputPanels.swift in Sources */, + D056CD761FF2A30900880D28 /* ChatSwipeToReplyRecognizer.swift in Sources */, + D091C7A41F8EBB1E00D7DE13 /* ChatPresentationData.swift in Sources */, D0EB41F31F2FEAB800838FE6 /* LegacyComponentsStickers.swift in Sources */, D0EC6FAA1EBA111500EBF1C3 /* wav_header.cc in Sources */, D0EC6DAD1EB9F58900EBF1C3 /* ChatInterfaceStateNavigationButtons.swift in Sources */, @@ -5235,7 +5515,9 @@ D0754D221EEDF89900884F6E /* ChatMessageInvoiceBubbleContentNode.swift in Sources */, D0EC6FE11EBA135100EBF1C3 /* spl_init.c in Sources */, D0EC6DCB1EB9F58900EBF1C3 /* ChatMediaInputTrendingPane.swift in Sources */, + D0430B021FF4584100A35ADD /* WebControllerNode.swift in Sources */, D0EC6DCC1EB9F58900EBF1C3 /* ChatButtonKeyboardInputNode.swift in Sources */, + D0CFBB861FD715E700B65C0D /* LegacyHTTPOperationImpl.swift in Sources */, D0EC6DCD1EB9F58900EBF1C3 /* ChatInputContextPanelNode.swift in Sources */, D0EC6DCE1EB9F58900EBF1C3 /* HorizontalStickersChatContextPanelNode.swift in Sources */, D0EC6DCF1EB9F58900EBF1C3 /* HorizontalStickerGridItem.swift in Sources */, @@ -5249,6 +5531,7 @@ D0EC6DD71EB9F58900EBF1C3 /* VerticalListContextResultsChatInputPanelItem.swift in Sources */, D0EC6DD81EB9F58900EBF1C3 /* VerticalListContextResultsChatInputPanelButtonItem.swift in Sources */, D064EF871F69A06F00AC0398 /* MessageContentKind.swift in Sources */, + D020A9DA1FEAE675008C66F7 /* OverlayPlayerController.swift in Sources */, D0EC6DD91EB9F58900EBF1C3 /* HorizontalListContextResultsChatInputContextPanelNode.swift in Sources */, D0E9BA161F05574500F079A4 /* STPCardValidator.m in Sources */, D0EC6DDA1EB9F58900EBF1C3 /* HorizontalListContextResultsChatInputPanelItem.swift in Sources */, @@ -5265,6 +5548,7 @@ D0E9BAC91F05738600F079A4 /* STPAPIClient+ApplePay.m in Sources */, D0EC6DDF1EB9F58900EBF1C3 /* ChatTextInputAudioRecordingTimeNode.swift in Sources */, D0EC6DE01EB9F58900EBF1C3 /* ChatTextInputAudioRecordingCancelIndicator.swift in Sources */, + D09D88711F86D36700BEB4C9 /* CountryList.swift in Sources */, D0EC6FAE1EBA112600EBF1C3 /* delay_estimator.cc in Sources */, D0EC6DE11EB9F58900EBF1C3 /* ChatMessageSelectionInputPanelNode.swift in Sources */, D0EC6DE21EB9F58900EBF1C3 /* ChatChannelSubscriberInputPanelNode.swift in Sources */, @@ -5297,7 +5581,6 @@ D033C60B1F0D306E0044EABA /* TelegramVideoNode.swift in Sources */, D0EC6DF91EB9F58900EBF1C3 /* PeerMediaCollectionInterfaceStateButtons.swift in Sources */, D080B27F1F4C7C6000AA3847 /* InstantPageManagedMediaId.swift in Sources */, - D0EC6DFA1EB9F58900EBF1C3 /* PeerMediaCollectionModeSelectionNode.swift in Sources */, D087BFAD1F741B9D003FD209 /* ShareContentContainerNode.swift in Sources */, D0EC6DFB1EB9F58900EBF1C3 /* AvatarGalleryController.swift in Sources */, D0EC6DFC1EB9F58900EBF1C3 /* GalleryController.swift in Sources */, @@ -5313,17 +5596,17 @@ D0EC6E031EB9F58900EBF1C3 /* GalleryFooterContentNode.swift in Sources */, D0E9BA0A1F0457DD00F079A4 /* BotCheckoutWebInteractionController.swift in Sources */, D0EC6E041EB9F58900EBF1C3 /* SecretMediaPreviewController.swift in Sources */, + D0C26D571FDF2388004ABF18 /* OpenChatMessage.swift in Sources */, D0EC6E051EB9F58900EBF1C3 /* SecretMediaPreviewControllerNode.swift in Sources */, + D0208AD61FA33D14001F0D5F /* RaiseToListenActivator.m in Sources */, D0EC6E061EB9F58900EBF1C3 /* ChatDocumentGalleryItem.swift in Sources */, D0EC6E071EB9F58900EBF1C3 /* ChatHoleGalleryItem.swift in Sources */, D0EC6E081EB9F58900EBF1C3 /* ChatImageGalleryItem.swift in Sources */, D048EA891F4F297500188713 /* InstantPageSettingsFontFamilyItemNode.swift in Sources */, - D0EC6E091EB9F58900EBF1C3 /* ChatVideoGalleryItem.swift in Sources */, D0EC6E0A1EB9F58900EBF1C3 /* ChatVideoGalleryItemScrubberView.swift in Sources */, D0EC6FC31EBA135100EBF1C3 /* auto_correlation.c in Sources */, D0EC6E0B1EB9F58900EBF1C3 /* ZoomableContentGalleryItemNode.swift in Sources */, D0EC6E0C1EB9F58900EBF1C3 /* ChatItemGalleryFooterContentNode.swift in Sources */, - D0EC6E0D1EB9F58900EBF1C3 /* ChatItemGalleryItemNode.swift in Sources */, D0EC6FD01EBA135100EBF1C3 /* filter_ar_fast_q12.c in Sources */, D0E9BABD1F05735F00F079A4 /* STPPaymentConfiguration.m in Sources */, D0EC6FEA1EBA17C300EBF1C3 /* fft4g.c in Sources */, @@ -5335,7 +5618,11 @@ D0E9BA211F05577700F079A4 /* STPCardParams.m in Sources */, D0EC6E111EB9F58900EBF1C3 /* InstantPageNode.swift in Sources */, D0EC6E121EB9F58900EBF1C3 /* InstantPageLayout.swift in Sources */, + D0D4345C1F97CEAA00CC1806 /* ProxySettingsController.swift in Sources */, + D08BDF641FA37BEA009D08E1 /* ChatRecordingPreviewInputPanelNode.swift in Sources */, + D0943B071FDEC529001522CC /* InstantVideoRadialStatusNode.swift in Sources */, D0EC6E131EB9F58900EBF1C3 /* InstantPageItem.swift in Sources */, + D0AD02EA1FFFEBEF00C1DCFF /* ChatMessageLiveLocationTextNode.swift in Sources */, D0EC6E141EB9F58900EBF1C3 /* InstantPageMedia.swift in Sources */, D0EC6E151EB9F58900EBF1C3 /* InstantPageLinkSelectionView.swift in Sources */, D0FE4DE01F0ACA8300E8A0B3 /* InstantVideoNode.swift in Sources */, @@ -5343,6 +5630,8 @@ D0EC6E161EB9F58900EBF1C3 /* InstantPageLayoutSpacings.swift in Sources */, D0EC6E171EB9F58900EBF1C3 /* InstantPageTextStyleStack.swift in Sources */, D0EC6E181EB9F58900EBF1C3 /* InstantPageTextItem.swift in Sources */, + D01C06B51FBB7720001561AB /* ChatMediaInputSettingsItem.swift in Sources */, + D091C7A61F8ECEA300D7DE13 /* SettingsThemeWallpaperNode.swift in Sources */, D0EC6E191EB9F58900EBF1C3 /* InstantPageAnchorItem.swift in Sources */, D05677531F4CA0D0001B723E /* InstantPagePeerReferenceNode.swift in Sources */, D0EC6E1A1EB9F58900EBF1C3 /* InstantPageImageItem.swift in Sources */, @@ -5365,8 +5654,10 @@ D0EC6FBA1EBA114200EBF1C3 /* analog_agc.c in Sources */, D0EC6E281EB9F58900EBF1C3 /* ContactsController.swift in Sources */, D0EC6E291EB9F58900EBF1C3 /* ContactsControllerNode.swift in Sources */, + D0AF323A1FB1D8D60097362B /* ChatOverlayNavigationBar.swift in Sources */, D0EC6E2A1EB9F58900EBF1C3 /* ContactsSearchContainerNode.swift in Sources */, D099D74F1EEFEE6A00A3128C /* GameControllerNode.swift in Sources */, + D0943AF61FDAAE7E001522CC /* MultipleAvatarsNode.swift in Sources */, D0EC6FD21EBA135100EBF1C3 /* get_hanning_window.c in Sources */, D0EC6E2B1EB9F58900EBF1C3 /* ComposeController.swift in Sources */, D099D74D1EEFEE1500A3128C /* GameController.swift in Sources */, @@ -5391,7 +5682,6 @@ D0E9BA651F055B4500F079A4 /* BotCheckoutNativeCardEntryController.swift in Sources */, D0EC6E391EB9F58900EBF1C3 /* ItemListCheckboxItem.swift in Sources */, D0EC6E3A1EB9F58900EBF1C3 /* ItemListSwitchItem.swift in Sources */, - D09E63A41F0FAB91003444CD /* EmbedGalleryVideoItem.swift in Sources */, D0EC6E3B1EB9F58900EBF1C3 /* ItemListPeerItem.swift in Sources */, D0EC6E3C1EB9F58900EBF1C3 /* ItemListPeerActionItem.swift in Sources */, D0EC6E3D1EB9F58900EBF1C3 /* ItemListMultilineInputItem.swift in Sources */, @@ -5405,18 +5695,23 @@ D0EC6E431EB9F58900EBF1C3 /* ItemListEditableDeleteControlNode.swift in Sources */, D0EC6E441EB9F58900EBF1C3 /* ItemListSingleLineInputItem.swift in Sources */, D01776B31F1D69A80044446D /* RadialStatusNode.swift in Sources */, + D01C06BE1FBCAF06001561AB /* ChatMessageBubbleMosaicLayout.swift in Sources */, D0EC6E451EB9F58900EBF1C3 /* ItemListMultilineTextItem.swift in Sources */, + D02F4AE91FCF370B004DFBAE /* ChatMessageInteractiveMediaBadge.swift in Sources */, D0EC6E461EB9F58900EBF1C3 /* ItemListLoadingIndicatorEmptyStateItem.swift in Sources */, D01A21AF1F39EA2E00DDA104 /* InstantPageTheme.swift in Sources */, D0EC6E471EB9F58900EBF1C3 /* ItemListTextEmptyStateItem.swift in Sources */, D0EC6E481EB9F58900EBF1C3 /* ItemListController.swift in Sources */, D0EC6E491EB9F58900EBF1C3 /* ItemListControllerEmptyStateItem.swift in Sources */, D0EC6E4A1EB9F58900EBF1C3 /* ItemListControllerNode.swift in Sources */, + D0B37C5C1F8D22AE004252DF /* ThemeSettingsController.swift in Sources */, D0EC6E4B1EB9F58900EBF1C3 /* ItemListControllerSegmentedTitleView.swift in Sources */, D0EC6E4D1EB9F58900EBF1C3 /* PeerInfoController.swift in Sources */, D0EC6E4E1EB9F58900EBF1C3 /* GroupInfoController.swift in Sources */, D0E9BA331F05583A00F079A4 /* STPPostalCodeValidator.m in Sources */, D0EC6E4F1EB9F58900EBF1C3 /* ChannelVisibilityController.swift in Sources */, + D09250061FE5371D003F693F /* GlobalExperimentalSettings.swift in Sources */, + D0A24D281F92C27100584D24 /* DefaultDarkAccentPresentationTheme.swift in Sources */, D025A4231F79344500563950 /* FetchManager.swift in Sources */, D00BDA1F1EE5B69200C64C5E /* ChannelAdminController.swift in Sources */, D0EC6E501EB9F58900EBF1C3 /* ChannelAdminsController.swift in Sources */, @@ -5425,6 +5720,7 @@ D0EC6FA61EBA111500EBF1C3 /* channel_buffer.cc in Sources */, D0EC6E531EB9F58900EBF1C3 /* ChannelMembersController.swift in Sources */, D0EC6FD71EBA135100EBF1C3 /* min_max_operations.c in Sources */, + D01C06BA1FBBB076001561AB /* ItemListSelectableControlNode.swift in Sources */, D0EC6E541EB9F58900EBF1C3 /* ConvertToSupergroupController.swift in Sources */, D0EC6E551EB9F58900EBF1C3 /* GroupAdminsController.swift in Sources */, D0EC6FDA1EBA135100EBF1C3 /* real_fft.c in Sources */, @@ -5438,6 +5734,7 @@ D0EC6E5E1EB9F58900EBF1C3 /* ItemListRecentSessionItem.swift in Sources */, D0EC6FCE1EBA135100EBF1C3 /* energy.c in Sources */, D00ADFDD1EBB73C200873D2E /* OverlayMediaManager.swift in Sources */, + D056CD7C1FF3E92C00880D28 /* DirectionalPanGestureRecognizer.swift in Sources */, D0EC6E5F1EB9F58900EBF1C3 /* RecentSessionsController.swift in Sources */, D0EC6E601EB9F58900EBF1C3 /* BlockedPeersController.swift in Sources */, D06BEC8C1F65E30A0035A545 /* WebEmbedVideoContent.swift in Sources */, @@ -5445,6 +5742,7 @@ D0471B4B1EFD64AC0074D609 /* BotCheckoutHeaderItem.swift in Sources */, D0EC6E621EB9F58900EBF1C3 /* SelectivePrivacySettingsPeersController.swift in Sources */, D0EC6EA91EBA0FBB00EBF1C3 /* BufferOutputStream.cpp in Sources */, + D0DFD5E21FCE2BA50039B3B1 /* CalculatingCacheSizeItem.swift in Sources */, D0EC6E631EB9F58900EBF1C3 /* TwoStepVerificationUnlockController.swift in Sources */, D0EC6E641EB9F58900EBF1C3 /* TwoStepVerificationPasswordEntryController.swift in Sources */, D0EC6EAC1EBA0FBB00EBF1C3 /* EchoCanceller.cpp in Sources */, @@ -5459,11 +5757,10 @@ D0EC6E6B1EB9F58900EBF1C3 /* InstalledStickerPacksController.swift in Sources */, D0EC6EB31EBA0FC100EBF1C3 /* AudioInput.cpp in Sources */, D0EC6E6C1EB9F58900EBF1C3 /* FeaturedStickerPacksController.swift in Sources */, + D0B85C231FF70BF400E795B4 /* AuthorizationSequenceAwaitingAccountResetController.swift in Sources */, D0EC6E6D1EB9F58900EBF1C3 /* ItemListStickerPackItem.swift in Sources */, D0EC6E6E1EB9F58900EBF1C3 /* ArhivedStickerPacksController.swift in Sources */, D0EC6EA81EBA0FB300EBF1C3 /* BufferInputStream.cpp in Sources */, - D0EC6E6F1EB9F58900EBF1C3 /* SettingsThemesItem.swift in Sources */, - D0EC6E701EB9F58900EBF1C3 /* SettingsThemeWallpaperNode.swift in Sources */, D0EC6E711EB9F58900EBF1C3 /* ThemeGalleryController.swift in Sources */, D0C0B5B11EE1C421000F4D2C /* ChatDateSelectionSheet.swift in Sources */, D0EC6FD91EBA135100EBF1C3 /* randomization_functions.c in Sources */, @@ -5478,6 +5775,7 @@ D0EC6E761EB9F58900EBF1C3 /* SettingsController.swift in Sources */, D0EC6E771EB9F58900EBF1C3 /* NotificationsAndSounds.swift in Sources */, D0EC6E781EB9F58900EBF1C3 /* NotificationSoundSelection.swift in Sources */, + D056CD741FF2996B00880D28 /* ExternalMusicAlbumArtResources.swift in Sources */, D0EC6FDD1EBA135100EBF1C3 /* resample_48khz.c in Sources */, D0EC6FD51EBA135100EBF1C3 /* levinson_durbin.c in Sources */, D0EC6FC01EBA132B00EBF1C3 /* nsx_core_c.c in Sources */, @@ -5487,6 +5785,8 @@ D0EC6FE71EBA135100EBF1C3 /* vector_scaling_operations.c in Sources */, D0EC6E7C1EB9F58900EBF1C3 /* UsernameSetupController.swift in Sources */, D0471B621EFEB5B70074D609 /* BotPaymentSwitchItemNode.swift in Sources */, + D09250041FE5363D003F693F /* ExperimentalSettings.swift in Sources */, + D0C44B641FC64D0500227BE0 /* SwipeToDismissGestureRecognizer.swift in Sources */, D0EC6E7D1EB9F58900EBF1C3 /* ChangePhoneNumberIntroController.swift in Sources */, D0EC6FA31EBA10E400EBF1C3 /* checks.cc in Sources */, D0EC6E7E1EB9F58900EBF1C3 /* ChangePhoneNumberController.swift in Sources */, @@ -5776,7 +6076,6 @@ ); OTHER_CFLAGS = ( "-DTGVOIP_USE_CUSTOM_CRYPTO", - "-DTGVOIP_USE_AUDIO_SESSION", "-DWEBRTC_APM_DEBUG_DUMP=0", "-DWEBRTC_POSIX", ); @@ -5792,6 +6091,116 @@ }; name = "Debug AppStore"; }; + D0924FEE1FE52C29003F693F /* Release Hockeyapp Internal */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D0F69DB91D6B88190046BCD6 /* TelegramUI.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_SUSPICIOUS_MOVES = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + MTL_ENABLE_DEBUG_INFO = NO; + OTHER_SWIFT_FLAGS = ""; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 3.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = "Release Hockeyapp Internal"; + }; + D0924FEF1FE52C29003F693F /* Release Hockeyapp Internal */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D0F69DB91D6B88190046BCD6 /* TelegramUI.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + COPY_PHASE_STRIP = YES; + DEVELOPMENT_TEAM = X834Q8SBVP; + HEADERMAP_USES_VFS = YES; + INFOPLIST_FILE = TelegramUITests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + OTHER_SWIFT_FLAGS = "-Xfrontend -debug-time-function-bodies -driver-show-incremental"; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.Telegram.TelegramUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.0; + }; + name = "Release Hockeyapp Internal"; + }; + D0924FF01FE52C29003F693F /* Release Hockeyapp Internal */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + COPY_PHASE_STRIP = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = X834Q8SBVP; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + HEADERMAP_USES_VFS = YES; + HEADER_SEARCH_PATHS = "third-party/ogg"; + INFOPLIST_FILE = "$(SRCROOT)/TelegramUI/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/third-party/opus/lib", + "$(PROJECT_DIR)/third-party/libwebp/lib", + "$(PROJECT_DIR)/third-party/FFmpeg-iOS/lib", + ); + OTHER_CFLAGS = ( + "-DTGVOIP_USE_CUSTOM_CRYPTO", + "-DWEBRTC_APM_DEBUG_DUMP=0", + "-DWEBRTC_POSIX", + ); + OTHER_LDFLAGS = "-ObjC"; + OTHER_SWIFT_FLAGS = ""; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.TelegramUI; + PRODUCT_NAME = TelegramUI; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + SWIFT_INSTALL_OBJC_HEADER = YES; + SWIFT_VERSION = 4.0; + USER_HEADER_SEARCH_PATHS = submodules/libtgvoip/webrtc_dsp; + }; + name = "Release Hockeyapp Internal"; + }; D0EC6E9E1EB9F79800EBF1C3 /* Debug Hockeyapp */ = { isa = XCBuildConfiguration; buildSettings = { @@ -5815,7 +6224,6 @@ ); OTHER_CFLAGS = ( "-DTGVOIP_USE_CUSTOM_CRYPTO", - "-DTGVOIP_USE_AUDIO_SESSION", "-DWEBRTC_APM_DEBUG_DUMP=0", "-DWEBRTC_POSIX", ); @@ -5853,7 +6261,6 @@ ); OTHER_CFLAGS = ( "-DTGVOIP_USE_CUSTOM_CRYPTO", - "-DTGVOIP_USE_AUDIO_SESSION", "-DWEBRTC_APM_DEBUG_DUMP=0", "-DWEBRTC_POSIX", ); @@ -5889,7 +6296,6 @@ ); OTHER_CFLAGS = ( "-DTGVOIP_USE_CUSTOM_CRYPTO", - "-DTGVOIP_USE_AUDIO_SESSION", "-DWEBRTC_APM_DEBUG_DUMP=0", "-DWEBRTC_POSIX", ); @@ -6066,6 +6472,7 @@ D0EC6E9E1EB9F79800EBF1C3 /* Debug Hockeyapp */, D079FD281F06BEF70038FADE /* Debug AppStore */, D0EC6E9F1EB9F79800EBF1C3 /* Release Hockeyapp */, + D0924FF01FE52C29003F693F /* Release Hockeyapp Internal */, D0EC6EA01EB9F79800EBF1C3 /* Release AppStore */, ); defaultConfigurationIsVisible = 0; @@ -6077,6 +6484,7 @@ D0FC40911D5B8E7500261D9D /* Debug Hockeyapp */, D079FD261F06BEF70038FADE /* Debug AppStore */, D0FC40921D5B8E7500261D9D /* Release Hockeyapp */, + D0924FEE1FE52C29003F693F /* Release Hockeyapp Internal */, D0400EDB1D5B900A007931CE /* Release AppStore */, ); defaultConfigurationIsVisible = 0; @@ -6088,6 +6496,7 @@ D0FC40971D5B8E7500261D9D /* Debug Hockeyapp */, D079FD271F06BEF70038FADE /* Debug AppStore */, D0FC40981D5B8E7500261D9D /* Release Hockeyapp */, + D0924FEF1FE52C29003F693F /* Release Hockeyapp Internal */, D0400EDD1D5B900A007931CE /* Release AppStore */, ); defaultConfigurationIsVisible = 0; diff --git a/TelegramUI.xcodeproj/xcuserdata/peter.xcuserdatad/xcschemes/xcschememanagement.plist b/TelegramUI.xcodeproj/xcuserdata/peter.xcuserdatad/xcschemes/xcschememanagement.plist index 4c50bc14ee..4dab2d7cd2 100644 --- a/TelegramUI.xcodeproj/xcuserdata/peter.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/TelegramUI.xcodeproj/xcuserdata/peter.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,7 +7,7 @@ TelegramUI.xcscheme orderHint - 12 + 13 SuppressBuildableAutocreation diff --git a/TelegramUI/ActivityIndicator.swift b/TelegramUI/ActivityIndicator.swift index f78e27c74c..86245f47c5 100644 --- a/TelegramUI/ActivityIndicator.swift +++ b/TelegramUI/ActivityIndicator.swift @@ -3,7 +3,7 @@ import AsyncDisplayKit enum ActivityIndicatorType: Equatable { case navigationAccent(PresentationTheme) - case custom(UIColor) + case custom(UIColor, CGFloat) static func ==(lhs: ActivityIndicatorType, rhs: ActivityIndicatorType) -> Bool { switch lhs { @@ -13,8 +13,8 @@ enum ActivityIndicatorType: Equatable { } else { return false } - case let .custom(lhsColor): - if case let .custom(rhsColor) = rhs, lhsColor.isEqual(rhsColor) { + case let .custom(lhsColor, lhsDiameter): + if case let .custom(rhsColor, rhsDiameter) = rhs, lhsColor.isEqual(rhsColor), lhsDiameter == rhsDiameter { return true } else { return false @@ -34,8 +34,8 @@ final class ActivityIndicator: ASDisplayNode { switch type { case let .navigationAccent(theme): self.indicatorNode.image = PresentationResourcesRootController.navigationIndefiniteActivityImage(theme) - case let .custom(color): - self.indicatorNode.image = generateIndefiniteActivityIndicatorImage(color: color) + case let .custom(color, diameter): + self.indicatorNode.image = generateIndefiniteActivityIndicatorImage(color: color, diameter: diameter) } } } @@ -56,8 +56,8 @@ final class ActivityIndicator: ASDisplayNode { switch type { case let .navigationAccent(theme): self.indicatorNode.image = PresentationResourcesRootController.navigationIndefiniteActivityImage(theme) - case let .custom(color): - self.indicatorNode.image = generateIndefiniteActivityIndicatorImage(color: color, diameter: 22.0) + case let .custom(color, diameter): + self.indicatorNode.image = generateIndefiniteActivityIndicatorImage(color: color, diameter: diameter) } super.init() @@ -93,7 +93,12 @@ final class ActivityIndicator: ASDisplayNode { } override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { - return CGSize(width: 22.0, height: 22.0) + switch self.type { + case .navigationAccent: + return CGSize(width: 22.0, height: 22.0) + case let .custom(_, diameter): + return CGSize(width: diameter, height: diameter) + } } override func layout() { @@ -101,7 +106,13 @@ final class ActivityIndicator: ASDisplayNode { let size = self.bounds.size - let indicatorSize = CGSize(width: 22.0, height: 22.0) + let indicatorSize: CGSize + switch self.type { + case .navigationAccent: + indicatorSize = CGSize(width: 22.0, height: 22.0) + case let .custom(_, diameter): + indicatorSize = CGSize(width: diameter, height: diameter) + } self.indicatorNode.frame = CGRect(origin: CGPoint(x: floor((size.width - indicatorSize.width) / 2.0), y: floor((size.height - indicatorSize.height) / 2.0)), size: indicatorSize) } } diff --git a/TelegramUI/ArhivedStickerPacksController.swift b/TelegramUI/ArhivedStickerPacksController.swift index 286432f299..4f75dc5444 100644 --- a/TelegramUI/ArhivedStickerPacksController.swift +++ b/TelegramUI/ArhivedStickerPacksController.swift @@ -55,8 +55,8 @@ private enum ArchivedStickerPacksEntryId: Hashable { } private enum ArchivedStickerPacksEntry: ItemListNodeEntry { - case info(String) - case pack(Int32, PresentationTheme, StickerPackCollectionInfo, StickerPackItem?, String, Bool, ItemListStickerPackItemEditing) + case info(PresentationTheme, String) + case pack(Int32, PresentationTheme, PresentationStrings, StickerPackCollectionInfo, StickerPackItem?, String, Bool, ItemListStickerPackItemEditing) var section: ItemListSectionId { switch self { @@ -69,27 +69,30 @@ private enum ArchivedStickerPacksEntry: ItemListNodeEntry { switch self { case .info: return .index(0) - case let .pack(_, _, info, _, _, _, _): + case let .pack(_, _, _, info, _, _, _, _): return .pack(info.id) } } static func ==(lhs: ArchivedStickerPacksEntry, rhs: ArchivedStickerPacksEntry) -> Bool { switch lhs { - case let .info(text): - if case .info(text) = rhs { + case let .info(lhsTheme, lhsText): + if case let .info(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .pack(lhsIndex, lhsTheme, lhsInfo, lhsTopItem, lhsCount, lhsEnabled, lhsEditing): - if case let .pack(rhsIndex, rhsTheme, rhsInfo, rhsTopItem, rhsCount, rhsEnabled, rhsEditing) = rhs { + case let .pack(lhsIndex, lhsTheme, lhsStrings, lhsInfo, lhsTopItem, lhsCount, lhsEnabled, lhsEditing): + if case let .pack(rhsIndex, rhsTheme, rhsStrings, rhsInfo, rhsTopItem, rhsCount, rhsEnabled, rhsEditing) = rhs { if lhsIndex != rhsIndex { return false } if lhsTheme !== rhsTheme { return false } + if lhsStrings !== rhsStrings { + return false + } if lhsInfo != rhsInfo { return false } @@ -121,9 +124,9 @@ private enum ArchivedStickerPacksEntry: ItemListNodeEntry { default: return true } - case let .pack(lhsIndex, _, _, _, _, _, _): + case let .pack(lhsIndex, _, _, _, _, _, _, _): switch rhs { - case let .pack(rhsIndex, _, _, _, _, _, _): + case let .pack(rhsIndex, _, _, _, _, _, _, _): return lhsIndex < rhsIndex default: return false @@ -133,10 +136,10 @@ private enum ArchivedStickerPacksEntry: ItemListNodeEntry { func item(_ arguments: ArchivedStickerPacksControllerArguments) -> ListViewItem { switch self { - case let .info(text): - return ItemListTextItem(text: .plain(text), sectionId: self.section) - case let .pack(_, theme, info, topItem, count, enabled, editing): - return ItemListStickerPackItem(theme: theme, account: arguments.account, packInfo: info, itemCount: count, topItem: topItem, unread: false, control: .none, editing: editing, enabled: enabled, sectionId: self.section, action: { + case let .info(theme, text): + return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + case let .pack(_, theme, strings, info, topItem, count, enabled, editing): + return ItemListStickerPackItem(theme: theme, strings: strings, account: arguments.account, packInfo: info, itemCount: count, topItem: topItem, unread: false, control: .none, editing: editing, enabled: enabled, sectionId: self.section, action: { arguments.openStickerPack(info) }, setPackIdWithRevealedOptions: { current, previous in arguments.setPackIdWithRevealedOptions(current, previous) @@ -192,19 +195,15 @@ private struct ArchivedStickerPacksControllerState: Equatable { } } -private func stringForStickerCount(_ count: Int32) -> String { - if count == 1 { - return "1 sticker" - } else { - return "\(count) stickers" - } +private func stringForStickerCount(_ count: Int32, strings: PresentationStrings) -> String { + return strings.StickerPack_StickerCount(count) } private func archivedStickerPacksControllerEntries(presentationData: PresentationData, state: ArchivedStickerPacksControllerState, packs: [ArchivedStickerPackItem]?, installedView: CombinedView) -> [ArchivedStickerPacksEntry] { var entries: [ArchivedStickerPacksEntry] = [] if let packs = packs { - entries.append(.info(presentationData.strings.StickerPacksSettings_ArchivedPacks_Info + "\n\n")) + entries.append(.info(presentationData.theme, presentationData.strings.StickerPacksSettings_ArchivedPacks_Info + "\n\n")) var installedIds = Set() if let view = installedView.views[.itemCollectionIds(namespaces: [Namespaces.ItemCollection.CloudStickerPacks])] as? ItemCollectionIdsView, let ids = view.idsByNamespace[Namespaces.ItemCollection.CloudStickerPacks] { @@ -214,7 +213,7 @@ private func archivedStickerPacksControllerEntries(presentationData: Presentatio var index: Int32 = 0 for item in packs { if !installedIds.contains(item.info.id) { - entries.append(.pack(index, presentationData.theme, item.info, item.topItems.first, stringForStickerCount(item.info.count), !state.removingPackIds.contains(item.info.id), ItemListStickerPackItemEditing(editable: true, editing: state.editing, revealed: state.packIdWithRevealedOptions == item.info.id))) + entries.append(.pack(index, presentationData.theme, presentationData.strings, item.info, item.topItems.first, stringForStickerCount(item.info.count, strings: presentationData.strings), !state.removingPackIds.contains(item.info.id), ItemListStickerPackItemEditing(editable: true, editing: state.editing, revealed: state.packIdWithRevealedOptions == item.info.id))) index += 1 } } @@ -322,7 +321,7 @@ public func archivedStickerPacksController(account: Account) -> ViewController { var emptyStateItem: ItemListControllerEmptyStateItem? if packs == nil { - emptyStateItem = ItemListLoadingIndicatorEmptyStateItem() + emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme) } let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.StickerPacksSettings_ArchivedPacks), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) diff --git a/TelegramUI/AudioRecordningToneData.swift b/TelegramUI/AudioRecordningToneData.swift new file mode 100644 index 0000000000..7f1ae867a4 --- /dev/null +++ b/TelegramUI/AudioRecordningToneData.swift @@ -0,0 +1,67 @@ +import Foundation +import AVFoundation + +private func loadAudioRecordingToneData() -> Data? { + let outputSettings: [String: Any] = [ + AVFormatIDKey: kAudioFormatLinearPCM as NSNumber, + AVSampleRateKey: 44100.0 as NSNumber, + AVLinearPCMBitDepthKey: 16 as NSNumber, + AVLinearPCMIsNonInterleaved: false as NSNumber, + AVLinearPCMIsFloatKey: false as NSNumber, + AVLinearPCMIsBigEndianKey: false as NSNumber + ] + + guard let url = Bundle.main.url(forResource: "begin_record", withExtension: "caf") else { + return nil + } + + let asset = AVURLAsset(url: url) + + guard let assetReader = try? AVAssetReader(asset: asset) else { + return nil + } + + let readerOutput = AVAssetReaderAudioMixOutput(audioTracks: asset.tracks, audioSettings: outputSettings) + + if !assetReader.canAdd(readerOutput) { + return nil + } + + assetReader.add(readerOutput) + + if !assetReader.startReading() { + return nil + } + + var data = Data() + + while assetReader.status == .reading { + if let nextBuffer = readerOutput.copyNextSampleBuffer() { + var abl = AudioBufferList() + var blockBuffer: CMBlockBuffer? = nil + CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer(nextBuffer, nil, &abl, MemoryLayout.size, nil, nil, kCMSampleBufferFlag_AudioBufferList_Assure16ByteAlignment, &blockBuffer) + let size = Int(CMSampleBufferGetTotalSampleSize(nextBuffer)) + if size != 0, let mData = abl.mBuffers.mData { + data.append(Data(bytes: mData, count: size)) + } + /*AudioBufferList abl; + CMBlockBufferRef blockBuffer = NULL; + CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer(nextBuffer, NULL, &abl, sizeof(abl), NULL, NULL, kCMSampleBufferFlag_AudioBufferList_Assure16ByteAlignment, &blockBuffer); + UInt64 size = CMSampleBufferGetTotalSampleSize(nextBuffer); + if (size != 0) { + [data appendBytes:abl.mBuffers[0].mData length:size]; + } + + CFRelease(nextBuffer); + if (blockBuffer) { + CFRelease(blockBuffer); + }*/ + } else { + break + } + } + + return data +} + +let audioRecordingToneData: Data? = loadAudioRecordingToneData() diff --git a/TelegramUI/AudioWaveformNode.swift b/TelegramUI/AudioWaveformNode.swift index 0530ade789..e097b4fac0 100644 --- a/TelegramUI/AudioWaveformNode.swift +++ b/TelegramUI/AudioWaveformNode.swift @@ -112,13 +112,13 @@ final class AudioWaveformNode: ASDisplayNode { let adjustedSampleHeight = sampleHeight - sampleWidth if adjustedSampleHeight.isLessThanOrEqualTo(sampleWidth) { - context.fillEllipse(in: CGRect(x: offset, y: size.height - sampleWidth, width: sampleWidth, height: sampleHeight)) + context.fillEllipse(in: CGRect(x: offset, y: size.height - sampleWidth, width: sampleWidth, height: sampleWidth)) context.fill(CGRect(x: offset, y: size.height - halfSampleWidth, width: sampleWidth, height: halfSampleWidth)) } else { let adjustedRect = CGRect(x: offset, y: size.height - adjustedSampleHeight, width: sampleWidth, height: adjustedSampleHeight) context.fill(adjustedRect) context.fillEllipse(in: CGRect(x: adjustedRect.minX, y: adjustedRect.minY - halfSampleWidth, width: sampleWidth, height: sampleWidth)) - context.fillEllipse(in: CGRect(x: adjustedRect.minX, y: adjustedRect.maxY - halfSampleWidth, width: sampleWidth, height: sampleHeight)) + context.fillEllipse(in: CGRect(x: adjustedRect.minX, y: adjustedRect.maxY - halfSampleWidth, width: sampleWidth, height: sampleWidth)) } } } diff --git a/TelegramUI/AuthorizationLayout.swift b/TelegramUI/AuthorizationLayout.swift new file mode 100644 index 0000000000..e3166c4f42 --- /dev/null +++ b/TelegramUI/AuthorizationLayout.swift @@ -0,0 +1,143 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display + +struct AuthorizationLayoutItemSpacing { + let weight: CGFloat + let maxValue: CGFloat +} + +struct AuthorizationLayoutItem { + let node: ASDisplayNode + let size: CGSize + let spacingBefore: AuthorizationLayoutItemSpacing + let spacingAfter: AuthorizationLayoutItemSpacing +} + +final class SolvedAuthorizationLayoutItem { + let item: AuthorizationLayoutItem + var spacingBefore: CGFloat? + var spacingAfter: CGFloat? + + init(item: AuthorizationLayoutItem) { + self.item = item + } +} + +func layoutAuthorizationItems(bounds: CGRect, items: [AuthorizationLayoutItem], transition: ContainedViewLayoutTransition, failIfDoesNotFit: Bool) -> Bool { + var fixedHeight: CGFloat = 0.0 + var totalSpacerWeight: CGFloat = 0.0 + for item in items { + fixedHeight += item.size.height + totalSpacerWeight += item.spacingBefore.weight + totalSpacerWeight += item.spacingAfter.weight + } + + let solvedItems = items.map(SolvedAuthorizationLayoutItem.init) + + if failIfDoesNotFit && bounds.size.height - fixedHeight < 0.0 { + return false + } + + var remainingSpacersHeight = max(0.0, bounds.size.height - fixedHeight) + + for i in 0 ..< 3 { + if i == 0 || i == 2 { + while true { + var hasUnsolvedItems = false + + for item in solvedItems { + if item.spacingBefore == nil { + hasUnsolvedItems = true + if item.item.spacingBefore.maxValue.isZero { + item.spacingBefore = 0.0 + } else { + item.spacingBefore = floor(item.item.spacingBefore.weight * remainingSpacersHeight / totalSpacerWeight) + } + } + + if item.spacingAfter == nil { + hasUnsolvedItems = true + if item.item.spacingAfter.maxValue.isZero { + item.spacingAfter = 0.0 + } else { + item.spacingAfter = floor(item.item.spacingAfter.weight * remainingSpacersHeight / totalSpacerWeight) + } + } + } + + if !hasUnsolvedItems { + break + } + } + } else { + var updated = false + for item in solvedItems { + if !item.item.spacingBefore.maxValue.isZero { + if item.spacingBefore! > item.item.spacingBefore.maxValue { + updated = true + } + } + if !item.item.spacingAfter.maxValue.isZero { + if item.spacingAfter! > item.item.spacingAfter.maxValue { + updated = true + } + } + } + + if updated { + for item in solvedItems { + if !item.item.spacingBefore.maxValue.isZero { + if item.spacingBefore! > item.item.spacingBefore.maxValue { + item.spacingBefore = item.item.spacingBefore.maxValue + } else { + item.spacingBefore = nil + } + } + if !item.item.spacingAfter.maxValue.isZero { + if item.spacingAfter! > item.item.spacingAfter.maxValue { + item.spacingAfter = item.item.spacingAfter.maxValue + } else { + item.spacingAfter = nil + } + } + } + + fixedHeight = 0.0 + totalSpacerWeight = 0.0 + + for item in solvedItems { + fixedHeight += item.item.size.height + if let spacingBefore = item.spacingBefore { + fixedHeight += spacingBefore + } else if !item.item.spacingBefore.maxValue.isZero { + totalSpacerWeight += item.item.spacingBefore.weight + } + if let spacingAfter = item.spacingAfter { + fixedHeight += spacingAfter + } else if !item.item.spacingAfter.maxValue.isZero { + totalSpacerWeight += item.item.spacingAfter.weight + } + } + + remainingSpacersHeight = max(0.0, bounds.size.height - fixedHeight) + } + } + } + + var totalHeight: CGFloat = 0.0 + for item in solvedItems { + totalHeight += item.spacingBefore! + item.spacingAfter! + item.item.size.height + } + + var verticalOrigin: CGFloat = bounds.minY + floor((bounds.size.height - totalHeight) / 2.0) + for item in solvedItems { + verticalOrigin += item.spacingBefore! + transition.updateFrame(node: item.item.node, frame: CGRect(origin: CGPoint(x: floor((bounds.size.width - item.item.size.width) / 2.0), y: verticalOrigin), size: item.item.size)) + verticalOrigin += item.item.size.height + verticalOrigin += item.spacingAfter! + } + + return true +} diff --git a/TelegramUI/AuthorizationSequenceAwaitingAccountResetController.swift b/TelegramUI/AuthorizationSequenceAwaitingAccountResetController.swift new file mode 100644 index 0000000000..c5d8b651ca --- /dev/null +++ b/TelegramUI/AuthorizationSequenceAwaitingAccountResetController.swift @@ -0,0 +1,84 @@ +import Foundation +import Display +import AsyncDisplayKit + +final class AuthorizationSequenceAwaitingAccountResetController: ViewController { + private var controllerNode: AuthorizationSequenceAwaitingAccountResetControllerNode { + return self.displayNode as! AuthorizationSequenceAwaitingAccountResetControllerNode + } + + private let strings: PresentationStrings + private let theme: AuthorizationTheme + + var logout: (() -> Void)? + var reset: (() -> Void)? + + var protectedUntil: Int32? + var number: String? + + var inProgress: Bool = false { + didSet { + if self.inProgress { + let item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: self.theme.accentColor)) + self.navigationItem.rightBarButtonItem = item + } else { + self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.strings.Common_Next, style: .done, target: self, action: #selector(self.logoutPressed)) + } + } + } + + init(strings: PresentationStrings, theme: AuthorizationTheme) { + self.strings = strings + self.theme = theme + + super.init(navigationBarTheme: AuthorizationSequenceController.navigationBarTheme(theme)) + + self.statusBar.statusBarStyle = theme.statusBarStyle + + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: " ", style: .plain, target: self, action: nil) + self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.strings.Settings_Logout, style: .plain, target: self, action: #selector(self.logoutPressed)) + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func loadDisplayNode() { + self.displayNode = AuthorizationSequenceAwaitingAccountResetControllerNode(strings: self.strings, theme: self.theme) + self.displayNodeDidLoad() + + self.controllerNode.reset = { [weak self] in + self?.reset?() + } + + if let protectedUntil = self.protectedUntil, let number = self.number { + self.controllerNode.updateData(protectedUntil: protectedUntil, number: number) + } + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + } + + func updateData(protectedUntil: Int32, number: String) { + if self.protectedUntil != protectedUntil || self.number != number { + self.protectedUntil = protectedUntil + self.number = number + if self.isNodeLoaded { + self.controllerNode.updateData(protectedUntil: protectedUntil, number: number) + } + } + } + + override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) + } + + @objc func logoutPressed() { + self.logout?() + } +} + + diff --git a/TelegramUI/AuthorizationSequenceAwaitingAccountResetControllerNode.swift b/TelegramUI/AuthorizationSequenceAwaitingAccountResetControllerNode.swift new file mode 100644 index 0000000000..8cd6d51a66 --- /dev/null +++ b/TelegramUI/AuthorizationSequenceAwaitingAccountResetControllerNode.swift @@ -0,0 +1,153 @@ +import Foundation +import AsyncDisplayKit +import Display + +private func timerValueString(days: Int32, hours: Int32, minutes: Int32, color: UIColor, strings: PresentationStrings) -> NSAttributedString { + var string = NSMutableAttributedString() + + var daysString = "" + if days > 0 { + daysString = strings.MessageTimer_Days(days) + " " + } + + var hoursString = "" + if hours > 0 || days > 0 { + daysString = strings.MessageTimer_Hours(hours) + " " + } + + let minutesString = strings.MessageTimer_Minutes(minutes) + + return NSAttributedString(string: daysString + hoursString + minutesString, font: Font.regular(21.0), textColor: color) +} + +final class AuthorizationSequenceAwaitingAccountResetControllerNode: ASDisplayNode, UITextFieldDelegate { + private let strings: PresentationStrings + private let theme: AuthorizationTheme + + private let titleNode: ASTextNode + private let noticeNode: ASTextNode + + private let timerTitleNode: ASTextNode + private let timerValueNode: ASTextNode + private let resetNode: HighlightableButtonNode + + private var layoutArguments: (ContainerViewLayout, CGFloat)? + + var reset: (() -> Void)? + + private var protectedUntil: Int32 = 0 + + init(strings: PresentationStrings, theme: AuthorizationTheme) { + self.strings = strings + self.theme = theme + + self.titleNode = ASTextNode() + self.titleNode.isLayerBacked = true + self.titleNode.displaysAsynchronously = false + self.titleNode.attributedText = NSAttributedString(string: strings.Login_ResetAccountProtected_Title, font: Font.light(30.0), textColor: self.theme.primaryColor) + + self.noticeNode = ASTextNode() + self.noticeNode.isLayerBacked = true + self.noticeNode.displaysAsynchronously = false + + self.timerTitleNode = ASTextNode() + self.timerTitleNode.isLayerBacked = true + self.timerTitleNode.displaysAsynchronously = false + self.timerTitleNode.attributedText = NSAttributedString(string: strings.Login_ResetAccountProtected_TimerTitle, font: Font.regular(16.0), textColor: self.theme.primaryColor) + + self.timerValueNode = ASTextNode() + self.timerValueNode.isLayerBacked = true + self.timerValueNode.displaysAsynchronously = false + + self.resetNode = HighlightableButtonNode() + self.resetNode.setAttributedTitle(NSAttributedString(string: strings.Login_ResetAccountProtected_Reset, font: Font.regular(21.0), textColor: self.theme.textPlaceholderColor), for: []) + self.resetNode.displaysAsynchronously = false + self.resetNode.isEnabled = false + + super.init() + + self.setViewBlock({ + return UITracingLayerView() + }) + + self.backgroundColor = self.theme.backgroundColor + + self.addSubnode(self.titleNode) + self.addSubnode(self.noticeNode) + self.addSubnode(self.timerTitleNode) + self.addSubnode(self.timerValueNode) + self.addSubnode(self.resetNode) + + self.resetNode.addTarget(self, action: #selector(self.resetPressed), forControlEvents: .touchUpInside) + } + + func updateData(protectedUntil: Int32, number: String) { + self.protectedUntil = protectedUntil + self.updateTimerValue() + + self.noticeNode.attributedText = NSAttributedString(string: strings.Login_ResetAccountProtected_Text(number).0, font: Font.regular(16.0), textColor: self.theme.primaryColor, paragraphAlignment: .center) + + if let (layout, navigationHeight) = self.layoutArguments { + self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .immediate) + } + } + + private func updateTimerValue() { + let timerSeconds = max(0, Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) - self.protectedUntil) + + let secondsInAMinute: Int32 = 60 + let secondsInAnHour: Int32 = 60 * secondsInAMinute + let secondsInADay: Int32 = 24 * secondsInAnHour + + let days = timerSeconds / secondsInADay + + let hourSeconds = timerSeconds % secondsInADay + let hours = hourSeconds / secondsInAnHour + + let minuteSeconds = hourSeconds % secondsInAnHour + var minutes = minuteSeconds / secondsInAMinute + + if days == 0 && hours == 0 && minutes == 0 && timerSeconds > 0 { + minutes = 1 + } + + self.timerValueNode.attributedText = timerValueString(days: days, hours: hours, minutes: minutes, color: self.theme.primaryColor, strings: self.strings) + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + self.layoutArguments = (layout, navigationBarHeight) + + var insets = layout.insets(options: [.input]) + insets.top = navigationBarHeight + + if max(layout.size.width, layout.size.height) > 1023.0 { + self.titleNode.attributedText = NSAttributedString(string: self.strings.Login_ResetAccountProtected_Title, font: Font.light(40.0), textColor: self.theme.primaryColor) + } else { + self.titleNode.attributedText = NSAttributedString(string: self.strings.Login_ResetAccountProtected_Title, font: Font.light(30.0), textColor: self.theme.primaryColor) + } + + let titleSize = self.titleNode.measure(CGSize(width: layout.size.width, height: CGFloat.greatestFiniteMagnitude)) + + let noticeSize = self.noticeNode.measure(CGSize(width: layout.size.width - 28.0, height: CGFloat.greatestFiniteMagnitude)) + + let timerTitleSize = self.timerTitleNode.measure(CGSize(width: layout.size.width, height: CGFloat.greatestFiniteMagnitude)) + let timerValueSize = self.timerValueNode.measure(CGSize(width: layout.size.width, height: CGFloat.greatestFiniteMagnitude)) + let resetSize = self.resetNode.measure(CGSize(width: layout.size.width, height: CGFloat.greatestFiniteMagnitude)) + + var items: [AuthorizationLayoutItem] = [] + items.append(AuthorizationLayoutItem(node: self.titleNode, size: titleSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0))) + items.append(AuthorizationLayoutItem(node: self.noticeNode, size: noticeSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 20.0, maxValue: 10.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0))) + + items.append(AuthorizationLayoutItem(node: self.timerTitleNode, size: timerTitleSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 100.0, maxValue: 100.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0))) + items.append(AuthorizationLayoutItem(node: self.self.timerValueNode, size: timerValueSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 10.0, maxValue: 10.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0))) + items.append(AuthorizationLayoutItem(node: self.resetNode, size: resetSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 10.0, maxValue: 10.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0))) + + let _ = layoutAuthorizationItems(bounds: CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: layout.size.height - insets.top - insets.bottom - 20.0)), items: items, transition: transition, failIfDoesNotFit: false) + } + + @objc func resetPressed() { + self.reset?() + } +} + + diff --git a/TelegramUI/AuthorizationSequenceCodeEntryController.swift b/TelegramUI/AuthorizationSequenceCodeEntryController.swift index 041f80d31c..88bb98bb99 100644 --- a/TelegramUI/AuthorizationSequenceCodeEntryController.swift +++ b/TelegramUI/AuthorizationSequenceCodeEntryController.swift @@ -8,6 +8,9 @@ final class AuthorizationSequenceCodeEntryController: ViewController { return self.displayNode as! AuthorizationSequenceCodeEntryControllerNode } + private let strings: PresentationStrings + private let theme: AuthorizationTheme + var loginWithCode: ((String) -> Void)? var requestNextOption: (() -> Void)? @@ -18,19 +21,26 @@ final class AuthorizationSequenceCodeEntryController: ViewController { var inProgress: Bool = false { didSet { if self.inProgress { - let item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode()) + let item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: self.theme.accentColor)) self.navigationItem.rightBarButtonItem = item } else { - self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Next", style: .done, target: self, action: #selector(self.nextPressed)) + self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.strings.Common_Next, style: .done, target: self, action: #selector(self.nextPressed)) } self.controllerNode.inProgress = self.inProgress } } - init() { - super.init(navigationBarTheme: AuthorizationSequenceController.navigationBarTheme) + init(strings: PresentationStrings, theme: AuthorizationTheme) { + self.strings = strings + self.theme = theme - self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Next", style: .done, target: self, action: #selector(self.nextPressed)) + super.init(navigationBarTheme: AuthorizationSequenceController.navigationBarTheme(theme)) + + self.hasActiveInput = true + + self.statusBar.statusBarStyle = theme.statusBarStyle + + self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.strings.Common_Next, style: .done, target: self, action: #selector(self.nextPressed)) } required init(coder aDecoder: NSCoder) { @@ -38,7 +48,7 @@ final class AuthorizationSequenceCodeEntryController: ViewController { } override public func loadDisplayNode() { - self.displayNode = AuthorizationSequenceCodeEntryControllerNode() + self.displayNode = AuthorizationSequenceCodeEntryControllerNode(strings: self.strings, theme: self.theme) self.displayNodeDidLoad() self.controllerNode.loginWithCode = { [weak self] code in @@ -49,6 +59,10 @@ final class AuthorizationSequenceCodeEntryController: ViewController { self?.requestNextOption?() } + self.controllerNode.requestAnotherOption = { [weak self] in + self?.requestNextOption?() + } + if let (number, codeType, nextType, timeout) = self.data { self.controllerNode.updateData(number: number, codeType: codeType, nextType: nextType, timeout: timeout) } @@ -73,7 +87,7 @@ final class AuthorizationSequenceCodeEntryController: ViewController { override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: 0.0, transition: transition) + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) } @objc func nextPressed() { diff --git a/TelegramUI/AuthorizationSequenceCodeEntryControllerNode.swift b/TelegramUI/AuthorizationSequenceCodeEntryControllerNode.swift index 24052918c6..a8a2807618 100644 --- a/TelegramUI/AuthorizationSequenceCodeEntryControllerNode.swift +++ b/TelegramUI/AuthorizationSequenceCodeEntryControllerNode.swift @@ -4,53 +4,57 @@ import Display import TelegramCore import SwiftSignalKit -func authorizationCurrentOptionText(_ type: SentAuthorizationCodeType) -> NSAttributedString { +func authorizationCurrentOptionText(_ type: SentAuthorizationCodeType, strings: PresentationStrings, theme: AuthorizationTheme) -> NSAttributedString { switch type { case .sms: - return NSAttributedString(string: "We have sent you an SMS with a code to the number", font: Font.regular(16.0), textColor: UIColor.black, paragraphAlignment: .center) + return NSAttributedString(string: "We have sent you an SMS with a code to the number", font: Font.regular(16.0), textColor: theme.primaryColor, paragraphAlignment: .center) case .otherSession: let string = NSMutableAttributedString() - string.append(NSAttributedString(string: "We've sent the code to the ", font: Font.regular(16.0), textColor: UIColor.black)) - string.append(NSAttributedString(string: "Telegram", font: Font.medium(16.0), textColor: UIColor.black)) - string.append(NSAttributedString(string: " app on your other device.", font: Font.regular(16.0), textColor: UIColor.black)) + string.append(NSAttributedString(string: "We've sent the code to the ", font: Font.regular(16.0), textColor: theme.primaryColor)) + string.append(NSAttributedString(string: "Telegram", font: Font.medium(16.0), textColor: theme.primaryColor)) + string.append(NSAttributedString(string: " app on your other device.", font: Font.regular(16.0), textColor: theme.primaryColor)) let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.alignment = .center string.addAttribute(NSAttributedStringKey.paragraphStyle, value: paragraphStyle, range: NSMakeRange(0, string.length)) return string case .call, .flashCall: - return NSAttributedString(string: "Telegram dialed your number", font: Font.regular(16.0), textColor: UIColor.black, paragraphAlignment: .center) + return NSAttributedString(string: "Telegram dialed your number", font: Font.regular(16.0), textColor: theme.primaryColor, paragraphAlignment: .center) } } -func authorizationNextOptionText(_ type: AuthorizationCodeNextType?, timeout: Int32?) -> NSAttributedString { +func authorizationNextOptionText(_ type: AuthorizationCodeNextType?, timeout: Int32?, strings: PresentationStrings, theme: AuthorizationTheme) -> (NSAttributedString, Bool) { if let type = type, let timeout = timeout { let minutes = timeout / 60 let seconds = timeout % 60 switch type { case .sms: if timeout <= 0 { - return NSAttributedString(string: "Telegram sent you an SMS", font: Font.regular(16.0), textColor: UIColor.black, paragraphAlignment: .center) + return (NSAttributedString(string: strings.Login_CodeSentSms, font: Font.regular(16.0), textColor: theme.primaryColor, paragraphAlignment: .center), false) } else { - return NSAttributedString(string: String(format: "Telegram will send you an SMS in %d:%.2d", minutes, seconds), font: Font.regular(16.0), textColor: UIColor.black, paragraphAlignment: .center) + return (NSAttributedString(string: strings.Login_SmsRequestState1(Int(minutes), Int(seconds)).0, font: Font.regular(16.0), textColor: theme.primaryColor, paragraphAlignment: .center), false) } case .call, .flashCall: if timeout <= 0 { - return NSAttributedString(string: "Telegram dialed your number", font: Font.regular(16.0), textColor: UIColor.black, paragraphAlignment: .center) + return (NSAttributedString(string: strings.ChangePhoneNumberCode_Called, font: Font.regular(16.0), textColor: theme.primaryColor, paragraphAlignment: .center), false) } else { - return NSAttributedString(string: String(format: "Telegram will call you in %d:%.2d", minutes, seconds), font: Font.regular(16.0), textColor: UIColor.black, paragraphAlignment: .center) + return (NSAttributedString(string: String(format: strings.ChangePhoneNumberCode_CallTimer(String(format: "%d:%.2d", minutes, seconds)).0, minutes, seconds), font: Font.regular(16.0), textColor: theme.primaryColor, paragraphAlignment: .center), false) } } } else { - return NSAttributedString(string: "Haven't received the code?", font: Font.regular(16.0), textColor: UIColor(rgb: 0x007ee5), paragraphAlignment: .center) + return (NSAttributedString(string: strings.Login_HaveNotReceivedCodeInternal, font: Font.regular(16.0), textColor: theme.accentColor, paragraphAlignment: .center), true) } } + + final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextFieldDelegate { - private let navigationBackgroundNode: ASDisplayNode - private let stripeNode: ASDisplayNode + private let strings: PresentationStrings + private let theme: AuthorizationTheme + private let titleNode: ASTextNode + private let titleIconNode: ASImageNode private let currentOptionNode: ASTextNode - private let nextOptionNode: ASTextNode + private let nextOptionNode: HighlightableButtonNode private let codeField: TextFieldNode private let codeSeparatorNode: ASDisplayNode @@ -64,7 +68,7 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF var phoneNumber: String = "" { didSet { - self.titleNode.attributedText = NSAttributedString(string: self.phoneNumber, font: Font.light(30.0), textColor: UIColor.black) + self.titleNode.attributedText = NSAttributedString(string: self.phoneNumber, font: Font.light(30.0), textColor: self.theme.primaryColor) } } @@ -74,6 +78,7 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF var loginWithCode: ((String) -> Void)? var requestNextOption: (() -> Void)? + var requestAnotherOption: (() -> Void)? var inProgress: Bool = false { didSet { @@ -81,37 +86,66 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF } } - override init() { - self.navigationBackgroundNode = ASDisplayNode() - self.navigationBackgroundNode.isLayerBacked = true - self.navigationBackgroundNode.backgroundColor = UIColor(rgb: 0xefefef) - - self.stripeNode = ASDisplayNode() - self.stripeNode.isLayerBacked = true - self.stripeNode.backgroundColor = UIColor(rgb: 0xbcbbc1) + init(strings: PresentationStrings, theme: AuthorizationTheme) { + self.strings = strings + self.theme = theme self.titleNode = ASTextNode() self.titleNode.isLayerBacked = true self.titleNode.displaysAsynchronously = false + self.titleIconNode = ASImageNode() + self.titleIconNode.isLayerBacked = true + self.titleIconNode.displayWithoutProcessing = true + self.titleIconNode.displaysAsynchronously = false + self.titleIconNode.image = generateImage(CGSize(width: 81.0, height: 52.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.setFillColor(theme.primaryColor.cgColor) + context.setStrokeColor(theme.primaryColor.cgColor) + context.setLineWidth(2.97) + let _ = try? drawSvgPath(context, path: "M9.87179487,9.04664384 C9.05602951,9.04664384 8.39525641,9.70682916 8.39525641,10.5205479 L8.39525641,44.0547945 C8.39525641,44.8685133 9.05602951,45.5286986 9.87179487,45.5286986 L65.1538462,45.5286986 C65.9696115,45.5286986 66.6303846,44.8685133 66.6303846,44.0547945 L66.6303846,10.5205479 C66.6303846,9.70682916 65.9696115,9.04664384 65.1538462,9.04664384 L9.87179487,9.04664384 S ") + + let _ = try? drawSvgPath(context, path: "M0,44.0547945 L75.025641,44.0547945 C75.025641,45.2017789 74.2153348,46.1893143 73.0896228,46.4142565 L66.1123641,47.8084669 C65.4749109,47.9358442 64.8264231,48 64.1763458,48 L10.8492952,48 C10.1992179,48 9.55073017,47.9358442 8.91327694,47.8084669 L1.93601826,46.4142565 C0.810306176,46.1893143 0,45.2017789 0,44.0547945 Z ") + + let _ = try? drawSvgPath(context, path: "M2.96153846,16.4383562 L14.1495726,16.4383562 C15.7851852,16.4383562 17.1111111,17.7631027 17.1111111,19.3972603 L17.1111111,45.0410959 C17.1111111,46.6752535 15.7851852,48 14.1495726,48 L2.96153846,48 C1.32592593,48 0,46.6752535 0,45.0410959 L0,19.3972603 C0,17.7631027 1.32592593,16.4383562 2.96153846,16.4383562 Z ") + + context.setStrokeColor(theme.backgroundColor.cgColor) + context.setLineWidth(1.65) + let _ = try? drawSvgPath(context, path: "M2.96153846,15.6133562 L14.1495726,15.6133562 C16.2406558,15.6133562 17.9361111,17.3073033 17.9361111,19.3972603 L17.9361111,45.0410959 C17.9361111,47.1310529 16.2406558,48.825 14.1495726,48.825 L2.96153846,48.825 C0.870455286,48.825 -0.825,47.1310529 -0.825,45.0410959 L-0.825,19.3972603 C-0.825,17.3073033 0.870455286,15.6133562 2.96153846,15.6133562 S ") + + context.setFillColor(theme.backgroundColor.cgColor) + let _ = try? drawSvgPath(context, path: "M1.64529915,20.3835616 L15.465812,20.3835616 L15.465812,44.0547945 L1.64529915,44.0547945 Z ") + + context.setFillColor(theme.accentColor.cgColor) + let _ = try? drawSvgPath(context, path: "M66.4700855,0.0285884455 C60.7084674,0.0285884455 55.9687848,4.08259697 55.9687848,9.14830256 C55.9687848,12.0875991 57.5993165,14.6795278 60.0605723,16.3382966 C60.0568181,16.4358994 60.0611217,16.5884309 59.9318097,17.067302 C59.7721478,17.6586615 59.4575977,18.4958519 58.8015608,19.4258487 L58.3294314,20.083383 L59.1449275,20.0976772 C61.9723538,20.1099725 63.6110772,18.2528913 63.8662207,17.9535438 C64.7014993,18.1388449 65.5698144,18.2680167 66.4700855,18.2680167 C72.2312622,18.2680167 76.9713861,14.2140351 76.9713861,9.14830256 C76.9713861,4.08256999 72.2312622,0.0285884455 66.4700855,0.0285884455 Z ") + + let _ = try? drawSvgPath(context, path: "M64.1551769,18.856071 C63.8258967,19.1859287 63.4214479,19.5187 62.9094963,19.840779 C61.8188563,20.5269227 60.5584776,20.9288319 59.1304689,20.9225505 L56.7413094,20.8806727 L57.6592902,19.6022014 L58.127415,18.9502938 C58.6361919,18.2290526 58.9525079,17.5293964 59.1353377,16.8522267 C59.1487516,16.8025521 59.1603548,16.7584153 59.1703974,16.7187893 C56.653362,14.849536 55.1437848,12.1128655 55.1437848,9.14830256 C55.1437848,3.61947515 60.2526259,-0.796411554 66.4700855,-0.796411554 C72.6872626,-0.796411554 77.7963861,3.61958236 77.7963861,9.14830256 C77.7963861,14.6770228 72.6872626,19.0930167 66.4700855,19.0930167 C65.7185957,19.0930167 64.9627196,19.0118067 64.1551769,18.856071 S ") + }) + self.currentOptionNode = ASTextNode() self.currentOptionNode.isLayerBacked = true self.currentOptionNode.displaysAsynchronously = false - self.nextOptionNode = ASTextNode() - self.nextOptionNode.isLayerBacked = true + self.nextOptionNode = HighlightableButtonNode() self.nextOptionNode.displaysAsynchronously = false - self.nextOptionNode.attributedText = authorizationNextOptionText(AuthorizationCodeNextType.call, timeout: 60) + let (nextOptionText, nextOptionActive) = authorizationNextOptionText(AuthorizationCodeNextType.call, timeout: 60, strings: self.strings, theme: self.theme) + self.nextOptionNode.setAttributedTitle(nextOptionText, for: []) + self.nextOptionNode.isUserInteractionEnabled = nextOptionActive self.codeSeparatorNode = ASDisplayNode() self.codeSeparatorNode.isLayerBacked = true - self.codeSeparatorNode.backgroundColor = UIColor(rgb: 0xbcbbc1) + self.codeSeparatorNode.backgroundColor = self.theme.separatorColor self.codeField = TextFieldNode() self.codeField.textField.font = Font.regular(24.0) self.codeField.textField.textAlignment = .center self.codeField.textField.keyboardType = .numberPad self.codeField.textField.returnKeyType = .done + self.codeField.textField.textColor = self.theme.primaryColor + self.codeField.textField.keyboardAppearance = self.theme.keyboardAppearance + self.codeField.textField.disableAutomaticKeyboardHandling = [.forward, .backward] + self.codeField.textField.tintColor = self.theme.accentColor super.init() @@ -119,19 +153,20 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF return UITracingLayerView() }) - self.backgroundColor = UIColor.white + self.backgroundColor = self.theme.backgroundColor - self.addSubnode(self.navigationBackgroundNode) - self.addSubnode(self.stripeNode) self.addSubnode(self.codeSeparatorNode) self.addSubnode(self.codeField) self.addSubnode(self.titleNode) + self.addSubnode(self.titleIconNode) self.addSubnode(self.currentOptionNode) self.addSubnode(self.nextOptionNode) self.codeField.textField.addTarget(self, action: #selector(self.codeFieldTextChanged(_:)), for: .editingChanged) - self.codeField.textField.attributedPlaceholder = NSAttributedString(string: "Code", font: Font.regular(24.0), textColor: UIColor(rgb: 0xbcbcc3)) + self.codeField.textField.attributedPlaceholder = NSAttributedString(string: strings.Login_Code, font: Font.regular(24.0), textColor: self.theme.textPlaceholderColor) + + self.nextOptionNode.addTarget(self, action: #selector(self.nextOptionNodePressed), forControlEvents: .touchUpInside) } deinit { @@ -142,14 +177,17 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF self.codeType = codeType self.phoneNumber = number - self.currentOptionNode.attributedText = authorizationCurrentOptionText(codeType) + self.currentOptionNode.attributedText = authorizationCurrentOptionText(codeType, strings: self.strings, theme: self.theme) if let timeout = timeout { self.currentTimeoutTime = timeout let disposable = ((Signal.single(1) |> delay(1.0, queue: Queue.mainQueue())) |> restart).start(next: { [weak self] _ in if let strongSelf = self { if let currentTimeoutTime = strongSelf.currentTimeoutTime, currentTimeoutTime > 0 { strongSelf.currentTimeoutTime = currentTimeoutTime - 1 - strongSelf.nextOptionNode.attributedText = authorizationNextOptionText(nextType, timeout:strongSelf.currentTimeoutTime) + let (nextOptionText, nextOptionActive) = authorizationNextOptionText(nextType, timeout:strongSelf.currentTimeoutTime, strings: strongSelf.strings, theme: strongSelf.theme) + strongSelf.nextOptionNode.setAttributedTitle(nextOptionText, for: []) + strongSelf.nextOptionNode.isUserInteractionEnabled = nextOptionActive + if let layoutArguments = strongSelf.layoutArguments { strongSelf.containerLayoutUpdated(layoutArguments.0, navigationBarHeight: layoutArguments.1, transition: .immediate) } @@ -164,87 +202,49 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF self.currentTimeoutTime = nil self.countdownDisposable.set(nil) } - self.nextOptionNode.attributedText = authorizationNextOptionText(nextType, timeout: self.currentTimeoutTime) + let (nextOptionText, nextOptionActive) = authorizationNextOptionText(nextType, timeout: self.currentTimeoutTime, strings: self.strings, theme: self.theme) + self.nextOptionNode.setAttributedTitle(nextOptionText, for: []) + self.nextOptionNode.isUserInteractionEnabled = nextOptionActive } func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { self.layoutArguments = (layout, navigationBarHeight) - let insets = layout.insets(options: [.statusBar, .input]) - let availableHeight = max(1.0, layout.size.height - insets.top - insets.bottom) + var insets = layout.insets(options: [.input]) + insets.top = navigationBarHeight if max(layout.size.width, layout.size.height) > 1023.0 { - self.titleNode.attributedText = NSAttributedString(string: self.phoneNumber, font: Font.light(30.0), textColor: UIColor.black) + self.titleNode.attributedText = NSAttributedString(string: self.phoneNumber, font: Font.light(40.0), textColor: self.theme.primaryColor) } else { - self.titleNode.attributedText = NSAttributedString(string: self.phoneNumber, font: Font.regular(20.0), textColor: UIColor.black) + self.titleNode.attributedText = NSAttributedString(string: self.phoneNumber, font: Font.light(30.0), textColor: self.theme.primaryColor) } let titleSize = self.titleNode.measure(CGSize(width: layout.size.width, height: CGFloat.greatestFiniteMagnitude)) - let additionalTitleSpacing: CGFloat - if titleSize.width > layout.size.width - 160.0 { - additionalTitleSpacing = 44.0 + + let currentOptionSize = self.currentOptionNode.measure(CGSize(width: layout.size.width - 28.0, height: CGFloat.greatestFiniteMagnitude)) + let nextOptionSize = self.nextOptionNode.measure(CGSize(width: layout.size.width, height: CGFloat.greatestFiniteMagnitude)) + + var items: [AuthorizationLayoutItem] = [] + if let codeType = self.codeType, case .otherSession = codeType { + self.titleIconNode.isHidden = false + items.append(AuthorizationLayoutItem(node: self.titleIconNode, size: self.titleIconNode.image!.size, spacingBefore: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0))) + items.append(AuthorizationLayoutItem(node: self.titleNode, size: titleSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 10.0, maxValue: 10.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0))) + items.append(AuthorizationLayoutItem(node: self.currentOptionNode, size: currentOptionSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 10.0, maxValue: 10.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0))) + items.append(AuthorizationLayoutItem(node: self.codeField, size: CGSize(width: layout.size.width - 88.0, height: 44.0), spacingBefore: AuthorizationLayoutItemSpacing(weight: 40.0, maxValue: 100.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0))) + items.append(AuthorizationLayoutItem(node: self.codeSeparatorNode, size: CGSize(width: layout.size.width - 88.0, height: UIScreenPixel), spacingBefore: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0))) + + items.append(AuthorizationLayoutItem(node: self.nextOptionNode, size: nextOptionSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 50.0, maxValue: 120.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0))) } else { - additionalTitleSpacing = 0.0 + self.titleIconNode.isHidden = true + items.append(AuthorizationLayoutItem(node: self.titleNode, size: titleSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0))) + items.append(AuthorizationLayoutItem(node: self.currentOptionNode, size: currentOptionSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 10.0, maxValue: 10.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0))) + items.append(AuthorizationLayoutItem(node: self.codeField, size: CGSize(width: layout.size.width - 88.0, height: 44.0), spacingBefore: AuthorizationLayoutItemSpacing(weight: 40.0, maxValue: 100.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0))) + items.append(AuthorizationLayoutItem(node: self.codeSeparatorNode, size: CGSize(width: layout.size.width - 88.0, height: UIScreenPixel), spacingBefore: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0))) + + items.append(AuthorizationLayoutItem(node: self.nextOptionNode, size: nextOptionSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 50.0, maxValue: 120.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0))) } - let minimalTitleSpacing: CGFloat = 10.0 - let maxTitleSpacing: CGFloat = 22.0 - let inputFieldsHeight: CGFloat = 60.0 - - let minimalNoticeSpacing: CGFloat = 11.0 - let maxNoticeSpacing: CGFloat = 35.0 - let noticeSize = self.currentOptionNode.measure(CGSize(width: layout.size.width - 28.0, height: CGFloat.greatestFiniteMagnitude)) - let minimalTermsOfServiceSpacing: CGFloat = 6.0 - let maxTermsOfServiceSpacing: CGFloat = 20.0 - let termsOfServiceSize = self.nextOptionNode.measure(CGSize(width: layout.size.width, height: CGFloat.greatestFiniteMagnitude)) - let minTrailingSpacing: CGFloat = 10.0 - - let inputHeight = inputFieldsHeight - let essentialHeight = additionalTitleSpacing + titleSize.height + minimalTitleSpacing + inputHeight + minimalNoticeSpacing + noticeSize.height - let additionalHeight = minimalTermsOfServiceSpacing + termsOfServiceSize.height + minTrailingSpacing - - let navigationHeight: CGFloat - if essentialHeight + additionalHeight > availableHeight || availableHeight * 0.66 - inputHeight < additionalHeight { - navigationHeight = min(floor(availableHeight * 0.3), availableHeight - inputFieldsHeight) - } else { - navigationHeight = floor(availableHeight * 0.3) - } - - transition.updateFrame(node: self.navigationBackgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: navigationHeight))) - transition.updateFrame(node: self.stripeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationHeight), size: CGSize(width: layout.size.width, height: UIScreenPixel))) - - let titleOffset: CGFloat - if navigationHeight * 0.5 < titleSize.height + minimalTitleSpacing { - titleOffset = floor((navigationHeight - titleSize.height) / 2.0) - } else { - titleOffset = max(navigationHeight * 0.5, navigationHeight - maxTitleSpacing - titleSize.height) - } - transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - titleSize.width) / 2.0), y: titleOffset), size: titleSize)) - - let codeFieldFrame = CGRect(origin: CGPoint(x: 0.0, y: navigationHeight + 3.0), size: CGSize(width: layout.size.width, height: 60.0)) - transition.updateFrame(node: self.codeField, frame: codeFieldFrame) - transition.updateFrame(node: self.codeSeparatorNode, frame: CGRect(origin: CGPoint(x: 22.0, y: navigationHeight + 60.0), size: CGSize(width: layout.size.width - 44.0, height: UIScreenPixel))) - - let additionalAvailableHeight = max(1.0, availableHeight - codeFieldFrame.maxY) - let additionalAvailableSpacing = max(1.0, additionalAvailableHeight - noticeSize.height - termsOfServiceSize.height) - let noticeSpacingFactor = maxNoticeSpacing / (maxNoticeSpacing + maxTermsOfServiceSpacing + minTrailingSpacing) - let termsOfServiceSpacingFactor = maxTermsOfServiceSpacing / (maxNoticeSpacing + maxTermsOfServiceSpacing + minTrailingSpacing) - - let noticeSpacing: CGFloat - let termsOfServiceSpacing: CGFloat - if additionalAvailableHeight <= maxNoticeSpacing + noticeSize.height + maxTermsOfServiceSpacing + termsOfServiceSize.height + minTrailingSpacing { - termsOfServiceSpacing = min(floor(termsOfServiceSpacingFactor * additionalAvailableSpacing), maxTermsOfServiceSpacing) - noticeSpacing = floor((additionalAvailableHeight - termsOfServiceSpacing - noticeSize.height - termsOfServiceSize.height) / 2.0) - } else { - noticeSpacing = min(floor(noticeSpacingFactor * additionalAvailableSpacing), maxNoticeSpacing) - termsOfServiceSpacing = min(floor(termsOfServiceSpacingFactor * additionalAvailableSpacing), maxTermsOfServiceSpacing) - } - - let currentOptionFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - noticeSize.width) / 2.0), y: codeFieldFrame.maxY + noticeSpacing), size: noticeSize) - let nextOptionFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - termsOfServiceSize.width) / 2.0), y: currentOptionFrame.maxY + termsOfServiceSpacing), size: termsOfServiceSize) - - transition.updateFrame(node: self.currentOptionNode, frame: currentOptionFrame) - transition.updateFrame(node: self.nextOptionNode, frame: nextOptionFrame) + let _ = layoutAuthorizationItems(bounds: CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: layout.size.height - insets.top - insets.bottom - 20.0)), items: items, transition: transition, failIfDoesNotFit: false) } func activateInput() { @@ -277,4 +277,8 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { return !self.inProgress } + + @objc func nextOptionNodePressed() { + self.requestAnotherOption?() + } } diff --git a/TelegramUI/AuthorizationSequenceController.swift b/TelegramUI/AuthorizationSequenceController.swift index 1a90302cea..85e46fc920 100644 --- a/TelegramUI/AuthorizationSequenceController.swift +++ b/TelegramUI/AuthorizationSequenceController.swift @@ -5,17 +5,28 @@ import Postbox import TelegramCore import SwiftSignalKit import MtProtoKitDynamic +import MessageUI public final class AuthorizationSequenceController: NavigationController { - static let navigationBarTheme = NavigationBarTheme(buttonColor: UIColor(rgb: 0x007ee5), primaryTextColor: .black, backgroundColor: .clear, separatorColor: .clear) + static func navigationBarTheme(_ theme: AuthorizationTheme) -> NavigationBarTheme { + return NavigationBarTheme(buttonColor: theme.accentColor, primaryTextColor: .black, backgroundColor: .clear, separatorColor: .clear, badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear) + } private var account: UnauthorizedAccount + private let apiId: Int32 + private let apiHash: String + private let strings: PresentationStrings + private let theme: AuthorizationTheme private var stateDisposable: Disposable? private let actionDisposable = MetaDisposable() - public init(account: UnauthorizedAccount) { + public init(account: UnauthorizedAccount, strings: PresentationStrings, apiId: Int32, apiHash: String) { self.account = account + self.apiId = apiId + self.apiHash = apiHash + self.strings = strings + self.theme = defaultAuthorizationTheme super.init(nibName: nil, bundle: nil) @@ -45,7 +56,7 @@ public final class AuthorizationSequenceController: NavigationController { if let currentController = currentController { controller = currentController } else { - controller = AuthorizationSequenceSplashController() + controller = AuthorizationSequenceSplashController(theme: self.theme) controller.nextPressed = { [weak self] in if let strongSelf = self { let masterDatacenterId = strongSelf.account.masterDatacenterId @@ -70,30 +81,34 @@ public final class AuthorizationSequenceController: NavigationController { if let currentController = currentController { controller = currentController } else { - controller = AuthorizationSequencePhoneEntryController() + controller = AuthorizationSequencePhoneEntryController(strings: self.strings, theme: self.theme) controller.loginWithNumber = { [weak self, weak controller] number in if let strongSelf = self { controller?.inProgress = true - strongSelf.actionDisposable.set((sendAuthorizationCode(account: strongSelf.account, phoneNumber: number, apiId: 10840, apiHash: "33c45224029d59cb3ad0c16134215aeb") |> deliverOnMainQueue).start(next: { [weak self] account in + strongSelf.actionDisposable.set((sendAuthorizationCode(account: strongSelf.account, phoneNumber: number, apiId: strongSelf.apiId, apiHash: strongSelf.apiHash) |> deliverOnMainQueue).start(next: { [weak self] account in if let strongSelf = self { controller?.inProgress = false strongSelf.account = account } }, error: { error in - if let controller = controller { + if let strongSelf = self, let controller = controller { controller.inProgress = false let text: String switch error { case .limitExceeded: - text = "You have requested authorization code too many times. Please try again later." + text = strongSelf.strings.Login_CodeFloodError case .invalidPhoneNumber: - text = "The phone number you entered is not valid. Please enter the correct number along with your area code." + text = strongSelf.strings.Login_InvalidPhoneError + case .phoneLimitExceeded: + text = strongSelf.strings.Login_PhoneFloodError + case .phoneBanned: + text = strongSelf.strings.Login_PhoneBannedError case .generic: - text = "An error occurred. Please try again later." + text = strongSelf.strings.Login_UnknownError } - controller.present(standardTextAlertController(title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})]), in: .window(.root)) + controller.present(standardTextAlertController(title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), in: .window(.root)) } })) } @@ -115,33 +130,78 @@ public final class AuthorizationSequenceController: NavigationController { if let currentController = currentController { controller = currentController } else { - controller = AuthorizationSequenceCodeEntryController() + controller = AuthorizationSequenceCodeEntryController(strings: self.strings, theme: self.theme) controller.loginWithCode = { [weak self, weak controller] code in if let strongSelf = self { controller?.inProgress = true strongSelf.actionDisposable.set((authorizeWithCode(account: strongSelf.account, code: code) |> deliverOnMainQueue).start(error: { error in Queue.mainQueue().async { - if let controller = controller { + if let strongSelf = self, let controller = controller { controller.inProgress = false let text: String switch error { case .limitExceeded: - text = "You have entered invalid code too many times. Please try again later." + text = strongSelf.strings.Login_CodeFloodError case .invalidCode: - text = "Invalid code. Please try again." + text = strongSelf.strings.Login_InvalidCodeError case .generic: - text = "An error occured." + text = strongSelf.strings.Login_UnknownError } - controller.present(standardTextAlertController(title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})]), in: .window(.root)) + controller.present(standardTextAlertController(title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), in: .window(.root)) } } })) } } } + controller.requestNextOption = { [weak self, weak controller] in + if let strongSelf = self { + if nextType == nil { + if MFMailComposeViewController.canSendMail() { + let phoneFormatted = formatPhoneNumber(number) + + let composeController = MFMailComposeViewController() + //composeController.mailComposeDelegate = strongSelf + composeController.setToRecipients(["sms@stel.com"]) + composeController.setSubject(strongSelf.strings.Login_EmailCodeSubject(phoneFormatted).0) + composeController.setMessageBody(strongSelf.strings.Login_EmailCodeBody(phoneFormatted).0, isHTML: false) + + controller?.view.window?.rootViewController?.present(composeController, animated: true, completion: nil) + } else { + controller?.present(standardTextAlertController(title: nil, text: strongSelf.strings.Login_EmailNotConfiguredError, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), in: .window(.root)) + } + } else { + controller?.inProgress = true + strongSelf.actionDisposable.set((resendAuthorizationCode(account: strongSelf.account) + |> deliverOnMainQueue).start(next: { result in + controller?.inProgress = false + }, error: { error in + if let strongSelf = self, let controller = controller { + controller.inProgress = false + + let text: String + switch error { + case .limitExceeded: + text = strongSelf.strings.Login_CodeFloodError + case .invalidPhoneNumber: + text = strongSelf.strings.Login_InvalidPhoneError + case .phoneLimitExceeded: + text = strongSelf.strings.Login_PhoneFloodError + case .phoneBanned: + text = strongSelf.strings.Login_PhoneBannedError + case .generic: + text = strongSelf.strings.Login_UnknownError + } + + controller.present(standardTextAlertController(title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), in: .window(.root)) + } + })) + } + } + } controller.updateData(number: formatPhoneNumber(number), codeType: type, nextType: nextType, timeout: timeout) return controller } @@ -158,37 +218,197 @@ public final class AuthorizationSequenceController: NavigationController { if let currentController = currentController { controller = currentController } else { - controller = AuthorizationSequencePasswordEntryController() + controller = AuthorizationSequencePasswordEntryController(strings: self.strings, theme: self.theme) controller.loginWithPassword = { [weak self, weak controller] password in if let strongSelf = self { controller?.inProgress = true strongSelf.actionDisposable.set((authorizeWithPassword(account: strongSelf.account, password: password) |> deliverOnMainQueue).start(error: { error in Queue.mainQueue().async { - if let controller = controller { + if let strongSelf = self, let controller = controller { controller.inProgress = false let text: String switch error { case .limitExceeded: - text = "You have entered invalid password too many times. Please try again later." + text = strongSelf.strings.LoginPassword_FloodError case .invalidPassword: - text = "Invalid password. Please try again." + text = strongSelf.strings.LoginPassword_InvalidPasswordError case .generic: - text = "An error occured. Please try again later." + text = strongSelf.strings.Login_UnknownError } - controller.present(standardTextAlertController(title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})]), in: .window(.root)) + controller.present(standardTextAlertController(title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), in: .window(.root)) } } })) } } } + controller.forgot = { [weak self, weak controller] in + if let strongSelf = self, let strongController = controller { + strongController.inProgress = true + strongSelf.actionDisposable.set((requestPasswordRecovery(account: strongSelf.account) + |> deliverOnMainQueue).start(next: { option in + if let strongSelf = self, let strongController = controller { + strongController.inProgress = false + switch option { + case let .email(pattern): + let _ = (strongSelf.account.postbox.modify { modifier -> Void in + if let state = modifier.getState() as? UnauthorizedAccountState, case let .passwordEntry(hint, number, code) = state.contents { + modifier.setState(UnauthorizedAccountState(masterDatacenterId: strongSelf.account.masterDatacenterId, contents: .passwordRecovery(hint: hint, number: number, code: code, emailPattern: pattern))) + } + }).start() + case .none: + strongController.present(standardTextAlertController(title: nil, text: strongSelf.strings.TwoStepAuth_RecoveryUnavailable, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), in: .window(.root)) + strongController.didForgotWithNoRecovery = true + } + } + }, error: { error in + if let strongSelf = self, let strongController = controller { + strongController.inProgress = false + } + })) + } + } + controller.reset = { [weak self, weak controller] in + if let strongSelf = self, let strongController = controller { + strongController.present(standardTextAlertController(title: nil, text: strongSelf.strings.TwoStepAuth_RecoveryUnavailable, actions: [ + TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_Cancel, action: {}), + TextAlertAction(type: .destructiveAction, title: strongSelf.strings.Login_ResetAccountProtected_Reset, action: { + if let strongSelf = self, let strongController = controller { + strongController.inProgress = true + strongSelf.actionDisposable.set((performAccountReset(account: strongSelf.account) + |> deliverOnMainQueue).start(next: { + if let strongController = controller { + strongController.inProgress = false + } + }, error: { error in + if let strongSelf = self, let strongController = controller { + strongController.inProgress = false + let text: String + switch error { + case .generic: + text = strongSelf.strings.Login_UnknownError + } + strongController.present(standardTextAlertController(title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), in: .window(.root)) + } + })) + } + })]), in: .window(.root)) + } + } controller.updateData(hint: hint) return controller } + private func passwordRecoveryController(emailPattern: String) -> AuthorizationSequencePasswordRecoveryController { + var currentController: AuthorizationSequencePasswordRecoveryController? + for c in self.viewControllers { + if let c = c as? AuthorizationSequencePasswordRecoveryController { + currentController = c + break + } + } + let controller: AuthorizationSequencePasswordRecoveryController + if let currentController = currentController { + controller = currentController + } else { + controller = AuthorizationSequencePasswordRecoveryController(strings: self.strings, theme: self.theme) + controller.recoverWithCode = { [weak self, weak controller] code in + if let strongSelf = self { + controller?.inProgress = true + + strongSelf.actionDisposable.set((performPasswordRecovery(account: strongSelf.account, code: code) |> deliverOnMainQueue).start(error: { error in + Queue.mainQueue().async { + if let strongSelf = self, let controller = controller { + controller.inProgress = false + + let text: String + switch error { + case .limitExceeded: + text = strongSelf.strings.LoginPassword_FloodError + case .invalidCode: + text = strongSelf.strings.Login_InvalidCodeError + case .expired: + text = strongSelf.strings.Login_CodeExpiredError + } + + controller.present(standardTextAlertController(title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), in: .window(.root)) + } + } + })) + } + } + controller.noAccess = { [weak self, weak controller] in + if let strongSelf = self, let controller = controller { + controller.present(standardTextAlertController(title: nil, text: strongSelf.strings.TwoStepAuth_RecoveryFailed, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), in: .window(.root)) + let account = strongSelf.account + let _ = (strongSelf.account.postbox.modify { modifier -> Void in + if let state = modifier.getState() as? UnauthorizedAccountState, case let .passwordRecovery(hint, number, code, _) = state.contents { + modifier.setState(UnauthorizedAccountState(masterDatacenterId: account.masterDatacenterId, contents: .passwordEntry(hint: hint, number: number, code: code))) + } + }).start() + } + } + } + controller.updateData(emailPattern: emailPattern) + return controller + } + + private func awaitingAccountResetController(protectedUntil: Int32, number: String?) -> AuthorizationSequenceAwaitingAccountResetController { + var currentController: AuthorizationSequenceAwaitingAccountResetController? + for c in self.viewControllers { + if let c = c as? AuthorizationSequenceAwaitingAccountResetController { + currentController = c + break + } + } + let controller: AuthorizationSequenceAwaitingAccountResetController + if let currentController = currentController { + controller = currentController + } else { + controller = AuthorizationSequenceAwaitingAccountResetController(strings: self.strings, theme: self.theme) + controller.reset = { [weak self, weak controller] in + if let strongSelf = self, let strongController = controller { + strongController.present(standardTextAlertController(title: nil, text: strongSelf.strings.TwoStepAuth_ResetAccountConfirmation, actions: [ + TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_Cancel, action: {}), + TextAlertAction(type: .destructiveAction, title: strongSelf.strings.Login_ResetAccountProtected_Reset, action: { + if let strongSelf = self, let strongController = controller { + strongController.inProgress = true + strongSelf.actionDisposable.set((performAccountReset(account: strongSelf.account) + |> deliverOnMainQueue).start(next: { + if let strongController = controller { + strongController.inProgress = false + } + }, error: { error in + if let strongSelf = self, let strongController = controller { + strongController.inProgress = false + let text: String + switch error { + case .generic: + text = strongSelf.strings.Login_UnknownError + } + strongController.present(standardTextAlertController(title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), in: .window(.root)) + } + })) + } + })]), in: .window(.root)) + } + } + controller.logout = { [weak self] in + if let strongSelf = self { + let account = strongSelf.account + let _ = (strongSelf.account.postbox.modify { modifier -> Void in + modifier.setState(UnauthorizedAccountState(masterDatacenterId: account.masterDatacenterId, contents: .empty)) + }).start() + } + } + } + controller.updateData(protectedUntil: protectedUntil, number: number ?? "") + return controller + } + private func signUpController(firstName: String, lastName: String) -> AuthorizationSequenceSignUpController { var currentController: AuthorizationSequenceSignUpController? for c in self.viewControllers { @@ -201,31 +421,31 @@ public final class AuthorizationSequenceController: NavigationController { if let currentController = currentController { controller = currentController } else { - controller = AuthorizationSequenceSignUpController() + controller = AuthorizationSequenceSignUpController(strings: self.strings, theme: self.theme) controller.signUpWithName = { [weak self, weak controller] firstName, lastName in if let strongSelf = self { controller?.inProgress = true strongSelf.actionDisposable.set((signUpWithName(account: strongSelf.account, firstName: firstName, lastName: lastName) |> deliverOnMainQueue).start(error: { error in Queue.mainQueue().async { - if let controller = controller { + if let strongSelf = self, let controller = controller { controller.inProgress = false let text: String switch error { case .limitExceeded: - text = "You have entered invalid password too many times. Please try again later." + text = strongSelf.strings.Login_CodeFloodError case .codeExpired: - text = "Authorization code has expired. Please start again." + text = strongSelf.strings.Login_CodeExpiredError case .invalidFirstName: - text = "Please enter valid first name" + text = strongSelf.strings.Login_InvalidFirstNameError case .invalidLastName: - text = "Please enter valid last name" + text = strongSelf.strings.Login_InvalidLastNameError case .generic: - text = "An error occured. Please try again later." + text = strongSelf.strings.Login_UnknownError } - controller.present(standardTextAlertController(title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})]), in: .window(.root)) + controller.present(standardTextAlertController(title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), in: .window(.root)) } } })) @@ -250,6 +470,10 @@ public final class AuthorizationSequenceController: NavigationController { self.setViewControllers([self.splashController(), self.codeEntryController(number: number, type: type, nextType: nextType, timeout: timeout)], animated: !self.viewControllers.isEmpty) case let .passwordEntry(hint, _, _): self.setViewControllers([self.splashController(), self.passwordEntryController(hint: hint)], animated: !self.viewControllers.isEmpty) + case let .passwordRecovery(_, _, _, emailPattern): + self.setViewControllers([self.splashController(), self.passwordRecoveryController(emailPattern: emailPattern)], animated: !self.viewControllers.isEmpty) + case let .awaitingAccountReset(protectedUntil, number): + self.setViewControllers([self.splashController(), self.awaitingAccountResetController(protectedUntil: protectedUntil, number: number)], animated: !self.viewControllers.isEmpty) case let .signUp(_, _, _, firstName, lastName): self.setViewControllers([self.splashController(), self.signUpController(firstName: firstName, lastName: lastName)], animated: !self.viewControllers.isEmpty) } diff --git a/TelegramUI/AuthorizationSequenceCountrySelectionController.swift b/TelegramUI/AuthorizationSequenceCountrySelectionController.swift index 7192ca9b22..c4a8cbd9a7 100644 --- a/TelegramUI/AuthorizationSequenceCountrySelectionController.swift +++ b/TelegramUI/AuthorizationSequenceCountrySelectionController.swift @@ -59,6 +59,7 @@ private let countryNamesAndCodes: [(String, String, Int)] = loadCountryNamesAndC private final class InnerCoutrySearchResultsController: UIViewController, UITableViewDelegate, UITableViewDataSource { private let displayCodes: Bool + private let theme: AuthorizationTheme private let tableView: UITableView @@ -70,8 +71,9 @@ private final class InnerCoutrySearchResultsController: UIViewController, UITabl var itemSelected: (((String, String, Int)) -> Void)? - init(displayCodes: Bool) { + init(strings: PresentationStrings, theme: AuthorizationTheme, displayCodes: Bool) { self.displayCodes = displayCodes + self.theme = theme self.tableView = UITableView(frame: CGRect(), style: .plain) @@ -92,6 +94,10 @@ private final class InnerCoutrySearchResultsController: UIViewController, UITabl self.tableView.autoresizingMask = [.flexibleWidth, .flexibleHeight] self.tableView.dataSource = self self.tableView.delegate = self + + self.tableView.backgroundColor = self.theme.backgroundColor + self.tableView.separatorColor = self.theme.separatorColor + self.tableView.backgroundView = UIView() } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { @@ -110,9 +116,13 @@ private final class InnerCoutrySearchResultsController: UIViewController, UITabl } cell.textLabel?.text = self.searchResults[indexPath.row].0 if self.displayCodes, let label = cell.accessoryView as? UILabel { - label.text = "+\(self.searchResults[indexPath.row].1)" + label.text = "+\(self.searchResults[indexPath.row].2)" label.sizeToFit() + label.textColor = self.theme.primaryColor } + cell.textLabel?.textColor = self.theme.primaryColor + cell.backgroundColor = self.theme.backgroundColor + cell.selectedBackgroundView?.backgroundColor = self.theme.itemHighlightedBackgroundColor return cell } @@ -122,6 +132,8 @@ private final class InnerCoutrySearchResultsController: UIViewController, UITabl } private final class InnerCountrySelectionController: UIViewController, UITableViewDelegate, UITableViewDataSource, UISearchResultsUpdating, UISearchBarDelegate { + private let strings: PresentationStrings + private let theme: AuthorizationTheme private let displayCodes: Bool private let tableView: UITableView @@ -135,7 +147,9 @@ private final class InnerCountrySelectionController: UIViewController, UITableVi var dismiss: (() -> Void)? var itemSelected: (((String, String, Int)) -> Void)? - init(displayCodes: Bool) { + init(strings: PresentationStrings, theme: AuthorizationTheme, displayCodes: Bool) { + self.strings = strings + self.theme = theme self.displayCodes = displayCodes self.tableView = UITableView(frame: CGRect(), style: .plain) @@ -157,8 +171,8 @@ private final class InnerCountrySelectionController: UIViewController, UITableVi super.init(nibName: nil, bundle: nil) - self.title = "Select Country" - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Cancel", style: .plain, target: self, action: #selector(self.cancelPressed)) + self.title = strings.Login_SelectCountry_Title + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) self.definesPresentationContext = true } @@ -172,23 +186,63 @@ private final class InnerCountrySelectionController: UIViewController, UITableVi self.view.backgroundColor = .white - self.searchResultsController = InnerCoutrySearchResultsController(displayCodes: self.displayCodes) + self.searchResultsController = InnerCoutrySearchResultsController(strings: self.strings, theme: self.theme, displayCodes: self.displayCodes) self.searchResultsController.itemSelected = { [weak self] item in self?.itemSelected?(item) } self.searchController = UISearchController(searchResultsController: self.searchResultsController) self.searchController.searchResultsUpdater = self - self.searchController.dimsBackgroundDuringPresentation = true + self.searchController.dimsBackgroundDuringPresentation = false self.searchController.searchBar.delegate = self + self.searchController.searchBar.keyboardAppearance = self.theme.keyboardAppearance + self.searchController.hidesNavigationBarDuringPresentation = true self.view.addSubview(self.tableView) + self.tableView.autoresizingMask = [.flexibleWidth, .flexibleHeight] self.tableView.tableHeaderView = self.searchController.searchBar self.tableView.dataSource = self self.tableView.delegate = self + self.tableView.sectionIndexColor = self.theme.accentColor + + self.tableView.backgroundColor = self.theme.backgroundColor + self.tableView.separatorColor = self.theme.separatorColor + self.tableView.backgroundView = UIView() self.tableView.frame = self.view.bounds self.view.addSubview(self.tableView) + + self.searchController.searchBar.barTintColor = self.theme.searchBarBackgroundColor + self.searchController.searchBar.tintColor = self.theme.accentColor + self.searchController.searchBar.backgroundColor = self.theme.searchBarBackgroundColor + self.searchController.searchBar.setTextColor(theme.searchBarTextColor) + + + let searchImage = generateImage(CGSize(width: 8.0, height: 28.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(self.theme.searchBarFillColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.width))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: size.height - size.width), size: CGSize(width: size.width, height: size.width))) + context.fill(CGRect(origin: CGPoint(x: 0.0, y: size.width / 2.0), size: CGSize(width: size.width, height: size.height - size.width))) + }) + self.searchController.searchBar.setSearchFieldBackgroundImage(searchImage, for: []) + self.searchController.searchBar.backgroundImage = UIImage() + } + + override func viewWillLayoutSubviews() { + super.viewWillLayoutSubviews() + + if #available(iOSApplicationExtension 11.0, *) { + var frame = self.searchController.view.frame + frame.origin.y = 12.0 + self.searchController.view.frame = frame + } + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + } func numberOfSections(in tableView: UITableView) -> Int { @@ -203,6 +257,11 @@ private final class InnerCountrySelectionController: UIViewController, UITableVi return self.sections[section].0 } + func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) { + (view as? UITableViewHeaderFooterView)?.backgroundView?.backgroundColor = self.theme.backgroundColor + (view as? UITableViewHeaderFooterView)?.textLabel?.textColor = self.theme.primaryColor + } + func sectionIndexTitles(for tableView: UITableView) -> [String]? { return self.sectionTitles } @@ -227,9 +286,13 @@ private final class InnerCountrySelectionController: UIViewController, UITableVi } cell.textLabel?.text = self.sections[indexPath.section].1[indexPath.row].0 if self.displayCodes, let label = cell.accessoryView as? UILabel { - label.text = "+\(self.sections[indexPath.section].1[indexPath.row].1)" + label.text = "+\(self.sections[indexPath.section].1[indexPath.row].2)" label.sizeToFit() + label.textColor = self.theme.primaryColor } + cell.textLabel?.textColor = self.theme.primaryColor + cell.backgroundColor = self.theme.backgroundColor + cell.selectedBackgroundView?.backgroundColor = self.theme.itemHighlightedBackgroundColor return cell } @@ -269,6 +332,8 @@ final class AuthorizationSequenceCountrySelectionController: ViewController { return nil } + private let theme: AuthorizationTheme + private var controllerNode: AuthorizationSequenceCountrySelectionControllerNode { return self.displayNode as! AuthorizationSequenceCountrySelectionControllerNode } @@ -278,12 +343,25 @@ final class AuthorizationSequenceCountrySelectionController: ViewController { var completeWithCountryCode: ((Int, String) -> Void)? - init(displayCodes: Bool = true) { - self.innerController = InnerCountrySelectionController(displayCodes: displayCodes) + init(strings: PresentationStrings, theme: AuthorizationTheme, displayCodes: Bool = true) { + self.theme = theme + self.innerController = InnerCountrySelectionController(strings: strings, theme: theme, displayCodes: displayCodes) self.innerNavigationController = UINavigationController(rootViewController: self.innerController) + self.innerController.navigation_setNavigationController(self.innerNavigationController) + self.innerNavigationController.navigationBar.barTintColor = theme.navigationBarBackgroundColor + self.innerNavigationController.navigationBar.tintColor = theme.accentColor + self.innerNavigationController.navigationBar.shadowImage = generateImage(CGSize(width: 1.0, height: 1.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(theme.navigationBarSeparatorColor.cgColor) + context.fill(CGRect(origin: CGPoint(), size: CGSize(width: 1.0, height: UIScreenPixel))) + }) + self.innerNavigationController.navigationBar.isTranslucent = false + self.innerNavigationController.navigationBar.titleTextAttributes = [NSAttributedStringKey.font: Font.semibold(17.0), NSAttributedStringKey.foregroundColor: theme.navigationBarTextColor] super.init(navigationBarTheme: nil) + self.statusBar.statusBarStyle = theme.statusBarStyle + self.innerController.dismiss = { [weak self] in self?.cancelPressed() } @@ -325,9 +403,10 @@ final class AuthorizationSequenceCountrySelectionController: ViewController { override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - self.innerNavigationController.view.frame = CGRect(origin: CGPoint(), size: layout.size) - self.innerController.view.frame = CGRect(origin: CGPoint(), size: layout.size) - //self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationBar.frame.maxY, transition: transition) + transition.animateView { + self.innerNavigationController.view.frame = CGRect(origin: CGPoint(), size: layout.size) + //self.innerController.view.frame = CGRect(origin: CGPoint(), size: layout.size) + } } private func cancelPressed() { diff --git a/TelegramUI/AuthorizationSequencePasswordEntryController.swift b/TelegramUI/AuthorizationSequencePasswordEntryController.swift index 303ee407f5..0722e37335 100644 --- a/TelegramUI/AuthorizationSequencePasswordEntryController.swift +++ b/TelegramUI/AuthorizationSequencePasswordEntryController.swift @@ -7,27 +7,49 @@ final class AuthorizationSequencePasswordEntryController: ViewController { return self.displayNode as! AuthorizationSequencePasswordEntryControllerNode } + private let strings: PresentationStrings + private let theme: AuthorizationTheme + var loginWithPassword: ((String) -> Void)? + var forgot: (() -> Void)? + var reset: (() -> Void)? var hint: String? + var didForgotWithNoRecovery: Bool = false { + didSet { + if self.didForgotWithNoRecovery != oldValue { + if self.isNodeLoaded, let hint = self.hint { + self.controllerNode.updateData(hint: hint, didForgotWithNoRecovery: didForgotWithNoRecovery) + } + } + } + } + private let hapticFeedback = HapticFeedback() var inProgress: Bool = false { didSet { if self.inProgress { - let item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode()) + let item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: self.theme.accentColor)) self.navigationItem.rightBarButtonItem = item } else { - self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Next", style: .done, target: self, action: #selector(self.nextPressed)) + self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.strings.Common_Next, style: .done, target: self, action: #selector(self.nextPressed)) } self.controllerNode.inProgress = self.inProgress } } - init() { - super.init(navigationBarTheme: AuthorizationSequenceController.navigationBarTheme) + init(strings: PresentationStrings, theme: AuthorizationTheme) { + self.strings = strings + self.theme = theme - self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Next", style: .done, target: self, action: #selector(self.nextPressed)) + super.init(navigationBarTheme: AuthorizationSequenceController.navigationBarTheme(theme)) + + self.hasActiveInput = true + + self.statusBar.statusBarStyle = theme.statusBarStyle + + self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.strings.Common_Next, style: .done, target: self, action: #selector(self.nextPressed)) } required init(coder aDecoder: NSCoder) { @@ -35,15 +57,23 @@ final class AuthorizationSequencePasswordEntryController: ViewController { } override public func loadDisplayNode() { - self.displayNode = AuthorizationSequencePasswordEntryControllerNode() + self.displayNode = AuthorizationSequencePasswordEntryControllerNode(strings: self.strings, theme: self.theme) self.displayNodeDidLoad() self.controllerNode.loginWithCode = { [weak self] _ in self?.nextPressed() } + self.controllerNode.forgot = { [weak self] in + self?.forgotPressed() + } + + self.controllerNode.reset = { [weak self] in + self?.resetPressed() + } + if let hint = self.hint { - self.controllerNode.updateData(hint: hint) + self.controllerNode.updateData(hint: hint, didForgotWithNoRecovery: self.didForgotWithNoRecovery) } } @@ -57,7 +87,7 @@ final class AuthorizationSequencePasswordEntryController: ViewController { if self.hint != hint { self.hint = hint if self.isNodeLoaded { - self.controllerNode.updateData(hint: hint) + self.controllerNode.updateData(hint: hint, didForgotWithNoRecovery: self.didForgotWithNoRecovery) } } } @@ -65,7 +95,7 @@ final class AuthorizationSequencePasswordEntryController: ViewController { override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: 0.0, transition: transition) + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) } @objc func nextPressed() { @@ -76,4 +106,16 @@ final class AuthorizationSequencePasswordEntryController: ViewController { self.loginWithPassword?(self.controllerNode.currentPassword) } } + + func forgotPressed() { + if self.didForgotWithNoRecovery { + self.present(standardTextAlertController(title: nil, text: self.strings.TwoStepAuth_RecoveryUnavailable, actions: [TextAlertAction(type: .defaultAction, title: self.strings.Common_OK, action: {})]), in: .window(.root)) + } else { + self.forgot?() + } + } + + func resetPressed() { + self.reset?() + } } diff --git a/TelegramUI/AuthorizationSequencePasswordEntryControllerNode.swift b/TelegramUI/AuthorizationSequencePasswordEntryControllerNode.swift index f809898592..183bb4d883 100644 --- a/TelegramUI/AuthorizationSequencePasswordEntryControllerNode.swift +++ b/TelegramUI/AuthorizationSequencePasswordEntryControllerNode.swift @@ -3,11 +3,13 @@ import AsyncDisplayKit import Display final class AuthorizationSequencePasswordEntryControllerNode: ASDisplayNode, UITextFieldDelegate { - private let navigationBackgroundNode: ASDisplayNode - private let stripeNode: ASDisplayNode + private let strings: PresentationStrings + private let theme: AuthorizationTheme + private let titleNode: ASTextNode - private let currentOptionNode: ASTextNode - private let nextOptionNode: ASTextNode + private let noticeNode: ASTextNode + private let forgotNode: HighlightableButtonNode + private let resetNode: HighlightableButtonNode private let codeField: TextFieldNode private let codeSeparatorNode: ASDisplayNode @@ -19,7 +21,10 @@ final class AuthorizationSequencePasswordEntryControllerNode: ASDisplayNode, UIT } var loginWithCode: ((String) -> Void)? - var requestNextOption: (() -> Void)? + var forgot: (() -> Void)? + var reset: (() -> Void)? + + var didForgotWithNoRecovery = false var inProgress: Bool = false { didSet { @@ -27,39 +32,41 @@ final class AuthorizationSequencePasswordEntryControllerNode: ASDisplayNode, UIT } } - override init() { - self.navigationBackgroundNode = ASDisplayNode() - self.navigationBackgroundNode.isLayerBacked = true - self.navigationBackgroundNode.backgroundColor = UIColor(rgb: 0xefefef) - - self.stripeNode = ASDisplayNode() - self.stripeNode.isLayerBacked = true - self.stripeNode.backgroundColor = UIColor(rgb: 0xbcbbc1) + init(strings: PresentationStrings, theme: AuthorizationTheme) { + self.strings = strings + self.theme = theme self.titleNode = ASTextNode() self.titleNode.isLayerBacked = true self.titleNode.displaysAsynchronously = false - self.titleNode.attributedText = NSAttributedString(string: "Your Password", font: Font.light(30.0), textColor: UIColor.black) + self.titleNode.attributedText = NSAttributedString(string: strings.LoginPassword_Title, font: Font.light(30.0), textColor: self.theme.primaryColor) - self.currentOptionNode = ASTextNode() - self.currentOptionNode.isLayerBacked = true - self.currentOptionNode.displaysAsynchronously = false - self.currentOptionNode.attributedText = NSAttributedString(string: "Two-step verification enabled.\nYour account is protected with an\nadditional password.", font: Font.regular(16.0), textColor: UIColor.black, paragraphAlignment: .center) + self.noticeNode = ASTextNode() + self.noticeNode.isLayerBacked = true + self.noticeNode.displaysAsynchronously = false + self.noticeNode.attributedText = NSAttributedString(string: strings.TwoStepAuth_EnterPasswordHelp, font: Font.regular(16.0), textColor: self.theme.primaryColor, paragraphAlignment: .center) - self.nextOptionNode = ASTextNode() - self.nextOptionNode.isLayerBacked = true - self.nextOptionNode.displaysAsynchronously = false - self.nextOptionNode.attributedText = NSAttributedString(string: "Forgot password?", font: Font.regular(16.0), textColor: UIColor(rgb: 0x007ee5), paragraphAlignment: .center) + self.forgotNode = HighlightableButtonNode() + self.forgotNode.displaysAsynchronously = false + self.forgotNode.setAttributedTitle(NSAttributedString(string: self.strings.TwoStepAuth_EnterPasswordForgot, font: Font.regular(16.0), textColor: self.theme.accentColor, paragraphAlignment: .center), for: []) + + self.resetNode = HighlightableButtonNode() + self.resetNode.displaysAsynchronously = false + self.resetNode.setAttributedTitle(NSAttributedString(string: self.strings.LoginPassword_ResetAccount, font: Font.regular(16.0), textColor: self.theme.destructiveColor, paragraphAlignment: .center), for: []) self.codeSeparatorNode = ASDisplayNode() self.codeSeparatorNode.isLayerBacked = true - self.codeSeparatorNode.backgroundColor = UIColor(rgb: 0xbcbbc1) + self.codeSeparatorNode.backgroundColor = self.theme.separatorColor self.codeField = TextFieldNode() self.codeField.textField.font = Font.regular(20.0) + self.codeField.textField.textColor = self.theme.primaryColor self.codeField.textField.textAlignment = .natural self.codeField.textField.isSecureTextEntry = true self.codeField.textField.returnKeyType = .done + self.codeField.textField.keyboardAppearance = self.theme.keyboardAppearance + self.codeField.textField.disableAutomaticKeyboardHandling = [.forward, .backward] + self.codeField.textField.tintColor = self.theme.accentColor super.init() @@ -67,101 +74,64 @@ final class AuthorizationSequencePasswordEntryControllerNode: ASDisplayNode, UIT return UITracingLayerView() }) - self.backgroundColor = UIColor.white + self.backgroundColor = self.theme.backgroundColor self.codeField.textField.delegate = self - self.addSubnode(self.navigationBackgroundNode) - self.addSubnode(self.stripeNode) self.addSubnode(self.codeSeparatorNode) self.addSubnode(self.codeField) self.addSubnode(self.titleNode) - self.addSubnode(self.currentOptionNode) - self.addSubnode(self.nextOptionNode) + self.addSubnode(self.forgotNode) + self.addSubnode(self.resetNode) + self.addSubnode(self.noticeNode) + + self.forgotNode.addTarget(self, action: #selector(self.forgotPressed), forControlEvents: .touchUpInside) + self.resetNode.addTarget(self, action: #selector(self.resetPressed), forControlEvents: .touchUpInside) } - func updateData(hint: String) { - self.codeField.textField.attributedPlaceholder = NSAttributedString(string: hint, font: Font.regular(20.0), textColor: UIColor(rgb: 0xbcbcc3)) + func updateData(hint: String, didForgotWithNoRecovery: Bool) { + self.didForgotWithNoRecovery = didForgotWithNoRecovery + self.codeField.textField.attributedPlaceholder = NSAttributedString(string: hint, font: Font.regular(20.0), textColor: self.theme.textPlaceholderColor) + if let (layout, navigationHeight) = self.layoutArguments { + self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .immediate) + } } func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { self.layoutArguments = (layout, navigationBarHeight) - let insets = layout.insets(options: [.statusBar, .input]) - let availableHeight = max(1.0, layout.size.height - insets.top - insets.bottom) + var insets = layout.insets(options: [.input]) + insets.top = navigationBarHeight if max(layout.size.width, layout.size.height) > 1023.0 { - self.titleNode.attributedText = NSAttributedString(string: "Your Password", font: Font.light(30.0), textColor: UIColor.black) + self.titleNode.attributedText = NSAttributedString(string: self.strings.LoginPassword_Title, font: Font.light(40.0), textColor: self.theme.primaryColor) } else { - self.titleNode.attributedText = NSAttributedString(string: "Your Password", font: Font.regular(20.0), textColor: UIColor.black) + self.titleNode.attributedText = NSAttributedString(string: self.strings.LoginPassword_Title, font: Font.light(30.0), textColor: self.theme.primaryColor) } let titleSize = self.titleNode.measure(CGSize(width: layout.size.width, height: CGFloat.greatestFiniteMagnitude)) - let additionalTitleSpacing: CGFloat - if titleSize.width > layout.size.width - 160.0 { - additionalTitleSpacing = 44.0 + + let noticeSize = self.noticeNode.measure(CGSize(width: layout.size.width - 28.0, height: CGFloat.greatestFiniteMagnitude)) + let forgotSize = self.forgotNode.measure(CGSize(width: layout.size.width, height: CGFloat.greatestFiniteMagnitude)) + let resetSize = self.resetNode.measure(CGSize(width: layout.size.width, height: CGFloat.greatestFiniteMagnitude)) + + var items: [AuthorizationLayoutItem] = [] + items.append(AuthorizationLayoutItem(node: self.titleNode, size: titleSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0))) + items.append(AuthorizationLayoutItem(node: self.noticeNode, size: noticeSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 10.0, maxValue: 10.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0))) + + items.append(AuthorizationLayoutItem(node: self.codeField, size: CGSize(width: layout.size.width - 88.0, height: 44.0), spacingBefore: AuthorizationLayoutItemSpacing(weight: 32.0, maxValue: 100.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0))) + items.append(AuthorizationLayoutItem(node: self.codeSeparatorNode, size: CGSize(width: layout.size.width - 88.0, height: UIScreenPixel), spacingBefore: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0))) + + items.append(AuthorizationLayoutItem(node: self.forgotNode, size: forgotSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 48.0, maxValue: 100.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0))) + + if self.didForgotWithNoRecovery { + self.resetNode.isHidden = false + items.append(AuthorizationLayoutItem(node: self.resetNode, size: resetSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 10.0, maxValue: 10.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0))) } else { - additionalTitleSpacing = 0.0 + self.resetNode.isHidden = true } - let minimalTitleSpacing: CGFloat = 10.0 - let maxTitleSpacing: CGFloat = 22.0 - let inputFieldsHeight: CGFloat = 60.0 - - let minimalNoticeSpacing: CGFloat = 11.0 - let maxNoticeSpacing: CGFloat = 35.0 - let noticeSize = self.currentOptionNode.measure(CGSize(width: layout.size.width - 28.0, height: CGFloat.greatestFiniteMagnitude)) - let minimalTermsOfServiceSpacing: CGFloat = 6.0 - let maxTermsOfServiceSpacing: CGFloat = 20.0 - let termsOfServiceSize = self.nextOptionNode.measure(CGSize(width: layout.size.width, height: CGFloat.greatestFiniteMagnitude)) - let minTrailingSpacing: CGFloat = 10.0 - - let inputHeight = inputFieldsHeight - let essentialHeight = additionalTitleSpacing + titleSize.height + minimalTitleSpacing + inputHeight + minimalNoticeSpacing + noticeSize.height - let additionalHeight = minimalTermsOfServiceSpacing + termsOfServiceSize.height + minTrailingSpacing - - let navigationHeight: CGFloat - if essentialHeight + additionalHeight > availableHeight || availableHeight * 0.66 - inputHeight < additionalHeight { - navigationHeight = min(floor(availableHeight * 0.3), availableHeight - inputFieldsHeight) - } else { - navigationHeight = floor(availableHeight * 0.3) - } - - transition.updateFrame(node: self.navigationBackgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: navigationHeight))) - transition.updateFrame(node: self.stripeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationHeight), size: CGSize(width: layout.size.width, height: UIScreenPixel))) - - let titleOffset: CGFloat - if navigationHeight * 0.5 < titleSize.height + minimalTitleSpacing { - titleOffset = floor((navigationHeight - titleSize.height) / 2.0) - } else { - titleOffset = max(navigationHeight * 0.5, navigationHeight - maxTitleSpacing - titleSize.height) - } - transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - titleSize.width) / 2.0), y: titleOffset), size: titleSize)) - - let codeFieldFrame = CGRect(origin: CGPoint(x: 22.0, y: navigationHeight + 3.0), size: CGSize(width: layout.size.width - 44.0, height: 60.0)) - transition.updateFrame(node: self.codeField, frame: codeFieldFrame) - transition.updateFrame(node: self.codeSeparatorNode, frame: CGRect(origin: CGPoint(x: 22.0, y: navigationHeight + 60.0), size: CGSize(width: layout.size.width - 44.0, height: UIScreenPixel))) - - let additionalAvailableHeight = max(1.0, availableHeight - codeFieldFrame.maxY) - let additionalAvailableSpacing = max(1.0, additionalAvailableHeight - noticeSize.height - termsOfServiceSize.height) - let noticeSpacingFactor = maxNoticeSpacing / (maxNoticeSpacing + maxTermsOfServiceSpacing + minTrailingSpacing) - let termsOfServiceSpacingFactor = maxTermsOfServiceSpacing / (maxNoticeSpacing + maxTermsOfServiceSpacing + minTrailingSpacing) - - let noticeSpacing: CGFloat - let termsOfServiceSpacing: CGFloat - if additionalAvailableHeight <= maxNoticeSpacing + noticeSize.height + maxTermsOfServiceSpacing + termsOfServiceSize.height + minTrailingSpacing { - termsOfServiceSpacing = min(floor(termsOfServiceSpacingFactor * additionalAvailableSpacing), maxTermsOfServiceSpacing) - noticeSpacing = floor((additionalAvailableHeight - termsOfServiceSpacing - noticeSize.height - termsOfServiceSize.height) / 2.0) - } else { - noticeSpacing = min(floor(noticeSpacingFactor * additionalAvailableSpacing), maxNoticeSpacing) - termsOfServiceSpacing = min(floor(termsOfServiceSpacingFactor * additionalAvailableSpacing), maxTermsOfServiceSpacing) - } - - let currentOptionFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - noticeSize.width) / 2.0), y: codeFieldFrame.maxY + noticeSpacing), size: noticeSize) - let nextOptionFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - termsOfServiceSize.width) / 2.0), y: currentOptionFrame.maxY + termsOfServiceSpacing), size: termsOfServiceSize) - - transition.updateFrame(node: self.currentOptionNode, frame: currentOptionFrame) - transition.updateFrame(node: self.nextOptionNode, frame: nextOptionFrame) + let _ = layoutAuthorizationItems(bounds: CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: layout.size.height - insets.top - insets.bottom - 20.0)), items: items, transition: transition, failIfDoesNotFit: false) } func activateInput() { @@ -180,4 +150,12 @@ final class AuthorizationSequencePasswordEntryControllerNode: ASDisplayNode, UIT self.loginWithCode?(self.currentPassword) return false } + + @objc func forgotPressed() { + self.forgot?() + } + + @objc func resetPressed() { + self.reset?() + } } diff --git a/TelegramUI/AuthorizationSequencePasswordRecoveryController.swift b/TelegramUI/AuthorizationSequencePasswordRecoveryController.swift new file mode 100644 index 0000000000..ac1bac4c0a --- /dev/null +++ b/TelegramUI/AuthorizationSequencePasswordRecoveryController.swift @@ -0,0 +1,96 @@ +import Foundation +import Display +import AsyncDisplayKit + +final class AuthorizationSequencePasswordRecoveryController: ViewController { + private var controllerNode: AuthorizationSequencePasswordRecoveryControllerNode { + return self.displayNode as! AuthorizationSequencePasswordRecoveryControllerNode + } + + private let strings: PresentationStrings + private let theme: AuthorizationTheme + + var recoverWithCode: ((String) -> Void)? + var noAccess: (() -> Void)? + + var emailPattern: String? + + private let hapticFeedback = HapticFeedback() + + var inProgress: Bool = false { + didSet { + if self.inProgress { + let item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: self.theme.accentColor)) + self.navigationItem.rightBarButtonItem = item + } else { + self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.strings.Common_Next, style: .done, target: self, action: #selector(self.nextPressed)) + } + self.controllerNode.inProgress = self.inProgress + } + } + + init(strings: PresentationStrings, theme: AuthorizationTheme) { + self.strings = strings + self.theme = theme + + super.init(navigationBarTheme: AuthorizationSequenceController.navigationBarTheme(theme)) + + self.hasActiveInput = true + + self.statusBar.statusBarStyle = theme.statusBarStyle + + self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.strings.Common_Next, style: .done, target: self, action: #selector(self.nextPressed)) + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func loadDisplayNode() { + self.displayNode = AuthorizationSequencePasswordRecoveryControllerNode(strings: self.strings, theme: self.theme) + self.displayNodeDidLoad() + + self.controllerNode.recoverWithCode = { [weak self] _ in + self?.nextPressed() + } + + self.controllerNode.noAccess = { [weak self] in + self?.noAccess?() + } + + if let emailPattern = self.emailPattern { + self.controllerNode.updateData(emailPattern: emailPattern) + } + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + self.controllerNode.activateInput() + } + + func updateData(emailPattern: String) { + if self.emailPattern != emailPattern { + self.emailPattern = emailPattern + if self.isNodeLoaded { + self.controllerNode.updateData(emailPattern: emailPattern) + } + } + } + + override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) + } + + @objc func nextPressed() { + if self.controllerNode.currentCode.isEmpty { + hapticFeedback.error() + self.controllerNode.animateError() + } else { + self.recoverWithCode?(self.controllerNode.currentCode) + } + } +} + diff --git a/TelegramUI/AuthorizationSequencePasswordRecoveryControllerNode.swift b/TelegramUI/AuthorizationSequencePasswordRecoveryControllerNode.swift new file mode 100644 index 0000000000..a46bfd189e --- /dev/null +++ b/TelegramUI/AuthorizationSequencePasswordRecoveryControllerNode.swift @@ -0,0 +1,136 @@ +import Foundation +import AsyncDisplayKit +import Display + +final class AuthorizationSequencePasswordRecoveryControllerNode: ASDisplayNode, UITextFieldDelegate { + private let strings: PresentationStrings + private let theme: AuthorizationTheme + + private let titleNode: ASTextNode + private let noticeNode: ASTextNode + private let noAccessNode: HighlightableButtonNode + + private let codeField: TextFieldNode + private let codeSeparatorNode: ASDisplayNode + + private var layoutArguments: (ContainerViewLayout, CGFloat)? + + var currentCode: String { + return self.codeField.textField.text ?? "" + } + + var recoverWithCode: ((String) -> Void)? + var noAccess: (() -> Void)? + + var inProgress: Bool = false { + didSet { + self.codeField.alpha = self.inProgress ? 0.6 : 1.0 + } + } + + init(strings: PresentationStrings, theme: AuthorizationTheme) { + self.strings = strings + self.theme = theme + + self.titleNode = ASTextNode() + self.titleNode.isLayerBacked = true + self.titleNode.displaysAsynchronously = false + self.titleNode.attributedText = NSAttributedString(string: strings.TwoStepAuth_RecoveryTitle, font: Font.light(30.0), textColor: self.theme.primaryColor) + + self.noticeNode = ASTextNode() + self.noticeNode.isLayerBacked = true + self.noticeNode.displaysAsynchronously = false + self.noticeNode.attributedText = NSAttributedString(string: strings.TwoStepAuth_RecoveryCodeHelp, font: Font.regular(16.0), textColor: self.theme.primaryColor, paragraphAlignment: .center) + + self.noAccessNode = HighlightableButtonNode() + self.noAccessNode.displaysAsynchronously = false + + self.codeSeparatorNode = ASDisplayNode() + self.codeSeparatorNode.isLayerBacked = true + self.codeSeparatorNode.backgroundColor = self.theme.separatorColor + + self.codeField = TextFieldNode() + self.codeField.textField.font = Font.regular(20.0) + self.codeField.textField.textColor = self.theme.primaryColor + self.codeField.textField.textAlignment = .center + self.codeField.textField.attributedPlaceholder = NSAttributedString(string: self.strings.TwoStepAuth_RecoveryCode, font: Font.regular(20.0), textColor: self.theme.textPlaceholderColor) + self.codeField.textField.returnKeyType = .done + self.codeField.textField.keyboardAppearance = self.theme.keyboardAppearance + self.codeField.textField.disableAutomaticKeyboardHandling = [.forward, .backward] + self.codeField.textField.tintColor = self.theme.accentColor + + super.init() + + self.setViewBlock({ + return UITracingLayerView() + }) + + self.backgroundColor = self.theme.backgroundColor + + self.codeField.textField.delegate = self + + self.addSubnode(self.codeSeparatorNode) + self.addSubnode(self.codeField) + self.addSubnode(self.titleNode) + self.addSubnode(self.noAccessNode) + self.addSubnode(self.noticeNode) + + self.noAccessNode.addTarget(self, action: #selector(self.noAccessPressed), forControlEvents: .touchUpInside) + } + + func updateData(emailPattern: String) { + self.noAccessNode.setAttributedTitle(NSAttributedString(string: self.strings.TwoStepAuth_RecoveryEmailUnavailable(emailPattern).0, font: Font.regular(16.0), textColor: self.theme.accentColor, paragraphAlignment: .center), for: []) + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + self.layoutArguments = (layout, navigationBarHeight) + + var insets = layout.insets(options: [.input]) + insets.top = navigationBarHeight + + if max(layout.size.width, layout.size.height) > 1023.0 { + self.titleNode.attributedText = NSAttributedString(string: self.strings.TwoStepAuth_RecoveryTitle, font: Font.light(40.0), textColor: self.theme.primaryColor) + } else { + self.titleNode.attributedText = NSAttributedString(string: self.strings.TwoStepAuth_RecoveryTitle, font: Font.light(30.0), textColor: self.theme.primaryColor) + } + + let titleSize = self.titleNode.measure(CGSize(width: layout.size.width, height: CGFloat.greatestFiniteMagnitude)) + + let noticeSize = self.noticeNode.measure(CGSize(width: layout.size.width - 28.0, height: CGFloat.greatestFiniteMagnitude)) + let noAccessSize = self.noAccessNode.measure(CGSize(width: layout.size.width, height: CGFloat.greatestFiniteMagnitude)) + let resetSize = self.noAccessNode.measure(CGSize(width: layout.size.width, height: CGFloat.greatestFiniteMagnitude)) + + var items: [AuthorizationLayoutItem] = [] + items.append(AuthorizationLayoutItem(node: self.titleNode, size: titleSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0))) + items.append(AuthorizationLayoutItem(node: self.noticeNode, size: noticeSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 10.0, maxValue: 10.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0))) + + items.append(AuthorizationLayoutItem(node: self.codeField, size: CGSize(width: layout.size.width - 88.0, height: 44.0), spacingBefore: AuthorizationLayoutItemSpacing(weight: 32.0, maxValue: 100.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0))) + items.append(AuthorizationLayoutItem(node: self.codeSeparatorNode, size: CGSize(width: layout.size.width - 88.0, height: UIScreenPixel), spacingBefore: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0))) + + items.append(AuthorizationLayoutItem(node: self.noAccessNode, size: noAccessSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 48.0, maxValue: 100.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0))) + + let _ = layoutAuthorizationItems(bounds: CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: layout.size.height - insets.top - insets.bottom - 20.0)), items: items, transition: transition, failIfDoesNotFit: false) + } + + func activateInput() { + self.codeField.textField.becomeFirstResponder() + } + + func animateError() { + self.codeField.layer.addShakeAnimation() + } + + @objc func passwordFieldTextChanged(_ textField: UITextField) { + + } + + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + self.recoverWithCode?(self.currentCode) + return false + } + + @objc func noAccessPressed() { + self.noAccess?() + } +} + diff --git a/TelegramUI/AuthorizationSequencePhoneEntryController.swift b/TelegramUI/AuthorizationSequencePhoneEntryController.swift index d5572e600b..711c27c65a 100644 --- a/TelegramUI/AuthorizationSequencePhoneEntryController.swift +++ b/TelegramUI/AuthorizationSequencePhoneEntryController.swift @@ -7,15 +7,18 @@ final class AuthorizationSequencePhoneEntryController: ViewController { return self.displayNode as! AuthorizationSequencePhoneEntryControllerNode } + private let strings: PresentationStrings + private let theme: AuthorizationTheme + private var currentData: (Int32, String)? var inProgress: Bool = false { didSet { if self.inProgress { - let item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode()) + let item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: self.theme.accentColor)) self.navigationItem.rightBarButtonItem = item } else { - self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Next", style: .done, target: self, action: #selector(self.nextPressed)) + self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.strings.Common_Next, style: .done, target: self, action: #selector(self.nextPressed)) } self.controllerNode.inProgress = self.inProgress } @@ -24,10 +27,17 @@ final class AuthorizationSequencePhoneEntryController: ViewController { private let hapticFeedback = HapticFeedback() - init() { - super.init(navigationBarTheme: AuthorizationSequenceController.navigationBarTheme) + init(strings: PresentationStrings, theme: AuthorizationTheme) { + self.strings = strings + self.theme = theme - self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Next", style: .done, target: self, action: #selector(self.nextPressed)) + super.init(navigationBarTheme: AuthorizationSequenceController.navigationBarTheme(theme)) + + self.hasActiveInput = true + + self.statusBar.statusBarStyle = theme.statusBarStyle + + self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: strings.Common_Next, style: .done, target: self, action: #selector(self.nextPressed)) } required init(coder aDecoder: NSCoder) { @@ -44,11 +54,11 @@ final class AuthorizationSequencePhoneEntryController: ViewController { } override public func loadDisplayNode() { - self.displayNode = AuthorizationSequencePhoneEntryControllerNode() + self.displayNode = AuthorizationSequencePhoneEntryControllerNode(strings: self.strings, theme: self.theme) self.displayNodeDidLoad() self.controllerNode.selectCountryCode = { [weak self] in if let strongSelf = self { - let controller = AuthorizationSequenceCountrySelectionController() + let controller = AuthorizationSequenceCountrySelectionController(strings: strongSelf.strings, theme: strongSelf.theme) controller.completeWithCountryCode = { code, _ in if let strongSelf = self, let currentData = strongSelf.currentData { strongSelf.updateData(countryCode: Int32(code), number: currentData.1) @@ -76,7 +86,7 @@ final class AuthorizationSequencePhoneEntryController: ViewController { override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: 0.0, transition: transition) + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) } @objc func nextPressed() { diff --git a/TelegramUI/AuthorizationSequencePhoneEntryControllerNode.swift b/TelegramUI/AuthorizationSequencePhoneEntryControllerNode.swift index 5fe15aa93a..65515d4d8a 100644 --- a/TelegramUI/AuthorizationSequencePhoneEntryControllerNode.swift +++ b/TelegramUI/AuthorizationSequencePhoneEntryControllerNode.swift @@ -3,345 +3,78 @@ import AsyncDisplayKit import Display import TelegramCore -let countryCodeToName: [Int: String] = [ - 1876: "Jamaica", - 1869: "Saint Kitts & Nevis", - 1868: "Trinidad & Tobago", - 1784: "Saint Vincent & the Grenadines", - 1767: "Dominica", - 1758: "Saint Lucia", - 1721: "Sint Maarten", - 1684: "American Samoa", - 1671: "Guam", - 1670: "Northern Mariana Islands", - 1664: "Montserrat", - 1649: "Turks & Caicos Islands", - 1473: "Grenada", - 1441: "Bermuda", - 1345: "Cayman Islands", - 1340: "US Virgin Islands", - 1284: "British Virgin Islands", - 1268: "Antigua & Barbuda", - 1264: "Anguilla", - 1246: "Barbados", - 1242: "Bahamas", - 998: "Uzbekistan", - 996: "Kyrgyzstan", - 995: "Georgia", - 994: "Azerbaijan", - 993: "Turkmenistan", - 992: "Tajikistan", - 977: "Nepal", - 976: "Mongolia", - 975: "Bhutan", - 974: "Qatar", - 973: "Bahrain", - 972: "Israel", - 971: "United Arab Emirates", - 970: "Palestine", - 968: "Oman", - 967: "Yemen", - 966: "Saudi Arabia", - 965: "Kuwait", - 964: "Iraq", - 963: "Syrian Arab Republic", - 962: "Jordan", - 961: "Lebanon", - 960: "Maldives", - 886: "Taiwan", - 880: "Bangladesh", - 856: "Laos", - 855: "Cambodia", - 853: "Macau", - 852: "Hong Kong", - 850: "North Korea", - 692: "Marshall Islands", - 691: "Micronesia", - 690: "Tokelau", - 689: "French Polynesia", - 688: "Tuvalu", - 687: "New Caledonia", - 686: "Kiribati", - 685: "Samoa", - 683: "Niue", - 682: "Cook Islands", - 681: "Wallis & Futuna", - 680: "Palau", - 679: "Fiji", - 678: "Vanuatu", - 677: "Solomon Islands", - 676: "Tonga", - 675: "Papua New Guinea", - 674: "Nauru", - 673: "Brunei Darussalam", - 672: "Norfolk Island", - 670: "Timor-Leste", - 599: "Bonaire, Sint Eustatius & Saba", - //599: "Curaçao", - 598: "Uruguay", - 597: "Suriname", - 596: "Martinique", - 595: "Paraguay", - 594: "French Guiana", - 593: "Ecuador", - 592: "Guyana", - 591: "Bolivia", - 590: "Guadeloupe", - 509: "Haiti", - 508: "Saint Pierre & Miquelon", - 507: "Panama", - 506: "Costa Rica", - 505: "Nicaragua", - 504: "Honduras", - 503: "El Salvador", - 502: "Guatemala", - 501: "Belize", - 500: "Falkland Islands", - 423: "Liechtenstein", - 421: "Slovakia", - 420: "Czech Republic", - 389: "Macedonia", - 387: "Bosnia & Herzegovina", - 386: "Slovenia", - 385: "Croatia", - 382: "Montenegro", - 381: "Serbia", - 380: "Ukraine", - 378: "San Marino", - 377: "Monaco", - 376: "Andorra", - 375: "Belarus", - 374: "Armenia", - 373: "Moldova", - 372: "Estonia", - 371: "Latvia", - 370: "Lithuania", - 359: "Bulgaria", - 358: "Finland", - 357: "Cyprus", - 356: "Malta", - 355: "Albania", - 354: "Iceland", - 353: "Ireland", - 352: "Luxembourg", - 351: "Portugal", - 350: "Gibraltar", - 299: "Greenland", - 298: "Faroe Islands", - 297: "Aruba", - 291: "Eritrea", - 290: "Saint Helena", - 269: "Comoros", - 268: "Swaziland", - 267: "Botswana", - 266: "Lesotho", - 265: "Malawi", - 264: "Namibia", - 263: "Zimbabwe", - 262: "Réunion", - 261: "Madagascar", - 260: "Zambia", - 258: "Mozambique", - 257: "Burundi", - 256: "Uganda", - 255: "Tanzania", - 254: "Kenya", - 253: "Djibouti", - 252: "Somalia", - 251: "Ethiopia", - 250: "Rwanda", - 249: "Sudan", - 248: "Seychelles", - 247: "Saint Helena", - 246: "Diego Garcia", - 245: "Guinea-Bissau", - 244: "Angola", - 243: "Congo (Dem. Rep.)", - 242: "Congo (Rep.)", - 241: "Gabon", - 240: "Equatorial Guinea", - 239: "São Tomé & Príncipe", - 238: "Cape Verde", - 237: "Cameroon", - 236: "Central African Rep.", - 235: "Chad", - 234: "Nigeria", - 233: "Ghana", - 232: "Sierra Leone", - 231: "Liberia", - 230: "Mauritius", - 229: "Benin", - 228: "Togo", - 227: "Niger", - 226: "Burkina Faso", - 225: "Côte d`Ivoire", - 224: "Guinea", - 223: "Mali", - 222: "Mauritania", - 221: "Senegal", - 220: "Gambia", - 218: "Libya", - 216: "Tunisia", - 213: "Algeria", - 212: "Morocco", - 211: "South Sudan", - 98: "Iran", - 95: "Myanmar", - 94: "Sri Lanka", - 93: "Afghanistan", - 92: "Pakistan", - 91: "India", - 90: "Turkey", - 86: "China", - 84: "Vietnam", - 82: "South Korea", - 81: "Japan", - 66: "Thailand", - 65: "Singapore", - 64: "New Zealand", - 63: "Philippines", - 62: "Indonesia", - 61: "Australia", - 60: "Malaysia", - 58: "Venezuela", - 57: "Colombia", - 56: "Chile", - 55: "Brazil", - 54: "Argentina", - 53: "Cuba", - 52: "Mexico", - 51: "Peru", - 49: "Germany", - 48: "Poland", - 47: "Norway", - 46: "Sweden", - 45: "Denmark", - 44: "United Kingdom", - 43: "Austria", - 41: "Switzerland", - 40: "Romania", - 39: "Italy", - 36: "Hungary", - 34: "Spain", - 33: "France", - 32: "Belgium", - 31: "Netherlands", - 30: "Greece", - 27: "South Africa", - 20: "Egypt", - 7: "Russian Federation", -// 7: "Kazakhstan", - 1: "USA", -// 1: "Puerto Rico", -// 1: "Dominican Rep.", -// 1: "Canada" -] - -private let countryButtonBackground = generateImage(CGSize(width: 61.0, height: 67.0), rotatedContext: { size, context in - let arrowSize: CGFloat = 10.0 - let lineWidth = UIScreenPixel - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setStrokeColor(UIColor(rgb: 0xbcbbc1).cgColor) - context.setLineWidth(lineWidth) - context.move(to: CGPoint(x: size.width, y: size.height - arrowSize - lineWidth / 2.0)) - context.addLine(to: CGPoint(x: size.width - 1.0, y: size.height - arrowSize - lineWidth / 2.0)) - context.addLine(to: CGPoint(x: size.width - 1.0 - arrowSize, y: size.height - lineWidth / 2.0)) - context.addLine(to: CGPoint(x: size.width - 1.0 - arrowSize - arrowSize, y: size.height - arrowSize - lineWidth / 2.0)) - context.addLine(to: CGPoint(x: 15.0, y: size.height - arrowSize - lineWidth / 2.0)) - context.strokePath() -})?.stretchableImage(withLeftCapWidth: 61, topCapHeight: 1) - -private let countryButtonHighlightedBackground = generateImage(CGSize(width: 60.0, height: 67.0), rotatedContext: { size, context in - let arrowSize: CGFloat = 10.0 - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(UIColor(rgb: 0xbcbbc1).cgColor) - context.fill(CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height - arrowSize))) - context.move(to: CGPoint(x: size.width, y: size.height - arrowSize)) - context.addLine(to: CGPoint(x: size.width - 1.0, y: size.height - arrowSize)) - context.addLine(to: CGPoint(x: size.width - 1.0 - arrowSize, y: size.height)) - context.addLine(to: CGPoint(x: size.width - 1.0 - arrowSize - arrowSize, y: size.height - arrowSize)) - context.closePath() - context.fillPath() -})?.stretchableImage(withLeftCapWidth: 61, topCapHeight: 2) - -private let phoneInputBackground = generateImage(CGSize(width: 85.0, height: 57.0), rotatedContext: { size, context in - let arrowSize: CGFloat = 10.0 - let lineWidth = UIScreenPixel - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setStrokeColor(UIColor(rgb: 0xbcbbc1).cgColor) - context.setLineWidth(lineWidth) - context.move(to: CGPoint(x: 15.0, y: size.height - lineWidth / 2.0)) - context.addLine(to: CGPoint(x: size.width, y: size.height - lineWidth / 2.0)) - context.strokePath() - context.move(to: CGPoint(x: size.width - 2.0 + lineWidth / 2.0, y: size.height - lineWidth / 2.0)) - context.addLine(to: CGPoint(x: size.width - 2.0 + lineWidth / 2.0, y: 0.0)) - context.strokePath() -})?.stretchableImage(withLeftCapWidth: 84, topCapHeight: 2) - -final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode { - private let navigationBackgroundNode: ASDisplayNode - private let stripeNode: ASDisplayNode - private let titleNode: ASTextNode - private let noticeNode: ASTextNode - private let termsOfServiceNode: ASTextNode - private let countryButton: ASButtonNode - private let phoneBackground: ASImageNode - private let phoneInputNode: PhoneInputNode - - var currentNumber: String { - return self.phoneInputNode.number +private func emojiFlagForISOCountryCode(_ countryCode: NSString) -> String { + if countryCode.length != 2 { + return "" } - var codeAndNumber: (Int32?, String) { - get { - return self.phoneInputNode.codeAndNumber - } set(value) { - self.phoneInputNode.codeAndNumber = value - } + let base: UInt32 = 127462 - 65 + let first: UInt32 = base + UInt32(countryCode.character(at: 0)) + let second: UInt32 = base + UInt32(countryCode.character(at: 1)) + + var data = Data() + data.count = 8 + data.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer) -> Void in + bytes[0] = first + bytes[1] = second } + return String(data: data, encoding: String.Encoding.utf32LittleEndian) ?? "" +} + +private final class PhoneAndCountryNode: ASDisplayNode { + let countryButton: ASButtonNode + let phoneBackground: ASImageNode + let phoneInputNode: PhoneInputNode var selectCountryCode: (() -> Void)? - var inProgress: Bool = false { - didSet { - self.phoneInputNode.enableEditing = !self.inProgress - self.phoneInputNode.alpha = self.inProgress ? 0.6 : 1.0 - self.countryButton.isEnabled = !self.inProgress - } - } - - override init() { - self.navigationBackgroundNode = ASDisplayNode() - self.navigationBackgroundNode.isLayerBacked = true - self.navigationBackgroundNode.backgroundColor = UIColor(rgb: 0xefefef) + init(strings: PresentationStrings, theme: AuthorizationTheme) { + let countryButtonBackground = generateImage(CGSize(width: 61.0, height: 67.0), rotatedContext: { size, context in + let arrowSize: CGFloat = 10.0 + let lineWidth = UIScreenPixel + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setStrokeColor(theme.separatorColor.cgColor) + context.setLineWidth(lineWidth) + context.move(to: CGPoint(x: 15.0, y: lineWidth / 2.0)) + context.addLine(to: CGPoint(x: size.width, y: lineWidth / 2.0)) + context.strokePath() + + context.move(to: CGPoint(x: size.width, y: size.height - arrowSize - lineWidth / 2.0)) + context.addLine(to: CGPoint(x: size.width - 1.0, y: size.height - arrowSize - lineWidth / 2.0)) + context.addLine(to: CGPoint(x: size.width - 1.0 - arrowSize, y: size.height - lineWidth / 2.0)) + context.addLine(to: CGPoint(x: size.width - 1.0 - arrowSize - arrowSize, y: size.height - arrowSize - lineWidth / 2.0)) + context.addLine(to: CGPoint(x: 15.0, y: size.height - arrowSize - lineWidth / 2.0)) + context.strokePath() + })?.stretchableImage(withLeftCapWidth: 61, topCapHeight: 1) - self.stripeNode = ASDisplayNode() - self.stripeNode.isLayerBacked = true - self.stripeNode.backgroundColor = UIColor(rgb: 0xbcbbc1) + let countryButtonHighlightedBackground = generateImage(CGSize(width: 60.0, height: 67.0), rotatedContext: { size, context in + let arrowSize: CGFloat = 10.0 + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(theme.itemHighlightedBackgroundColor.cgColor) + context.fill(CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height - arrowSize))) + context.move(to: CGPoint(x: size.width, y: size.height - arrowSize)) + context.addLine(to: CGPoint(x: size.width - 1.0, y: size.height - arrowSize)) + context.addLine(to: CGPoint(x: size.width - 1.0 - arrowSize, y: size.height)) + context.addLine(to: CGPoint(x: size.width - 1.0 - arrowSize - arrowSize, y: size.height - arrowSize)) + context.closePath() + context.fillPath() + })?.stretchableImage(withLeftCapWidth: 61, topCapHeight: 2) - self.titleNode = ASTextNode() - self.titleNode.isLayerBacked = true - self.titleNode.displaysAsynchronously = false - self.titleNode.attributedText = NSAttributedString(string: "Your Phone", font: Font.light(30.0), textColor: UIColor.black) - - self.noticeNode = ASTextNode() - self.noticeNode.isLayerBacked = true - self.noticeNode.displaysAsynchronously = false - self.noticeNode.attributedText = NSAttributedString(string: "Please confirm your country code and enter your phone number.", font: Font.regular(16.0), textColor: UIColor(rgb: 0x878787), paragraphAlignment: .center) - - self.termsOfServiceNode = ASTextNode() - self.termsOfServiceNode.isLayerBacked = true - self.termsOfServiceNode.displaysAsynchronously = false - let termsString = NSMutableAttributedString() - termsString.append(NSAttributedString(string: "By signing up,\nyou agree to the ", font: Font.regular(16.0), textColor: UIColor.black)) - termsString.append(NSAttributedString(string: "Terms of Service", font: Font.regular(16.0), textColor: UIColor(rgb: 0x007ee5))) - termsString.append(NSAttributedString(string: ".", font: Font.regular(16.0), textColor: UIColor.black)) - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.alignment = .center - termsString.addAttribute(NSAttributedStringKey.paragraphStyle, value: paragraphStyle, range: NSMakeRange(0, termsString.length)) - self.termsOfServiceNode.attributedText = termsString + let phoneInputBackground = generateImage(CGSize(width: 85.0, height: 57.0), rotatedContext: { size, context in + let lineWidth = UIScreenPixel + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setStrokeColor(theme.separatorColor.cgColor) + context.setLineWidth(lineWidth) + context.move(to: CGPoint(x: 15.0, y: size.height - lineWidth / 2.0)) + context.addLine(to: CGPoint(x: size.width, y: size.height - lineWidth / 2.0)) + context.strokePath() + context.move(to: CGPoint(x: size.width - 2.0 + lineWidth / 2.0, y: size.height - lineWidth / 2.0)) + context.addLine(to: CGPoint(x: size.width - 2.0 + lineWidth / 2.0, y: 0.0)) + context.strokePath() + })?.stretchableImage(withLeftCapWidth: 84, topCapHeight: 2) self.countryButton = ASButtonNode() + self.countryButton.displaysAsynchronously = false self.countryButton.setBackgroundImage(countryButtonBackground, for: []) self.countryButton.setBackgroundImage(countryButtonHighlightedBackground, for: .highlighted) @@ -355,34 +88,36 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode { super.init() - self.setViewBlock({ - return UITracingLayerView() - }) - - self.backgroundColor = UIColor.white - - self.addSubnode(self.navigationBackgroundNode) - self.addSubnode(self.stripeNode) - self.addSubnode(self.titleNode) - self.addSubnode(self.termsOfServiceNode) - self.addSubnode(self.noticeNode) self.addSubnode(self.phoneBackground) self.addSubnode(self.countryButton) self.addSubnode(self.phoneInputNode) + self.phoneInputNode.countryCodeField.textField.keyboardAppearance = theme.keyboardAppearance + self.phoneInputNode.numberField.textField.keyboardAppearance = theme.keyboardAppearance + self.phoneInputNode.countryCodeField.textField.textColor = theme.primaryColor + self.phoneInputNode.numberField.textField.textColor = theme.primaryColor + + self.phoneInputNode.countryCodeField.textField.tintColor = theme.accentColor + self.phoneInputNode.numberField.textField.tintColor = theme.accentColor + + self.phoneInputNode.countryCodeField.textField.disableAutomaticKeyboardHandling = [.forward] + self.phoneInputNode.numberField.textField.disableAutomaticKeyboardHandling = [.forward] + + self.countryButton.contentEdgeInsets = UIEdgeInsets(top: 0.0, left: 15.0, bottom: 10.0, right: 0.0) self.countryButton.contentHorizontalAlignment = .left - self.phoneInputNode.numberField.textField.attributedPlaceholder = NSAttributedString(string: "Your phone number", font: Font.regular(20.0), textColor: UIColor(rgb: 0xbcbcc3)) + self.phoneInputNode.numberField.textField.attributedPlaceholder = NSAttributedString(string: strings.Login_PhonePlaceholder, font: Font.regular(20.0), textColor: theme.textPlaceholderColor) self.countryButton.addTarget(self, action: #selector(self.countryPressed), forControlEvents: .touchUpInside) self.phoneInputNode.countryCodeUpdated = { [weak self] code in if let strongSelf = self { - if let code = Int(code), let countryName = countryCodeToName[code] { - strongSelf.countryButton.setTitle(countryName, with: Font.regular(20.0), with: .black, for: []) + if let code = Int(code), let (countryId, countryName) = countryCodeToIdAndName[code] { + let flagString = emojiFlagForISOCountryCode(countryId as NSString) + strongSelf.countryButton.setTitle("\(flagString) \(countryName)", with: Font.regular(20.0), with: theme.primaryColor, for: []) } else { - strongSelf.countryButton.setTitle("Select Country", with: Font.regular(20.0), with: .black, for: []) + strongSelf.countryButton.setTitle(strings.Login_SelectCountry_Title, with: Font.regular(20.0), with: theme.textPlaceholderColor, for: []) } } } @@ -390,101 +125,142 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode { self.phoneInputNode.number = "+1" } - func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { - let insets = layout.insets(options: [.statusBar, .input]) - let availableHeight = max(1.0, layout.size.height - insets.top - insets.bottom) - - if max(layout.size.width, layout.size.height) > 1023.0 { - self.titleNode.attributedText = NSAttributedString(string: "Your Phone", font: Font.light(40.0), textColor: UIColor.black) - } else { - self.titleNode.attributedText = NSAttributedString(string: "Your Phone", font: Font.light(30.0), textColor: UIColor.black) - } - - let titleSize = self.titleNode.measure(CGSize(width: layout.size.width, height: CGFloat.greatestFiniteMagnitude)) - let minimalTitleSpacing: CGFloat = 10.0 - let maxTitleSpacing: CGFloat = 28.0 - let countryButtonHeight: CGFloat = 57.0 - let inputFieldsHeight: CGFloat = 57.0 - - let minimalNoticeSpacing: CGFloat = 11.0 - let maxNoticeSpacing: CGFloat = 35.0 - let noticeSize = self.noticeNode.measure(CGSize(width: layout.size.width - 28.0, height: CGFloat.greatestFiniteMagnitude)) - let minimalTermsOfServiceSpacing: CGFloat = 6.0 - let maxTermsOfServiceSpacing: CGFloat = 20.0 - let termsOfServiceSize = self.termsOfServiceNode.measure(CGSize(width: layout.size.width, height: CGFloat.greatestFiniteMagnitude)) - let minTrailingSpacing: CGFloat = 10.0 - - let inputHeight = countryButtonHeight + inputFieldsHeight - let essentialHeight = titleSize.height + minimalTitleSpacing + inputHeight - let additionalHeight = minimalNoticeSpacing + noticeSize.height + minimalTermsOfServiceSpacing + termsOfServiceSize.height + minTrailingSpacing - - let navigationHeight: CGFloat - if essentialHeight + additionalHeight > availableHeight || availableHeight * 0.66 - inputHeight < additionalHeight { - transition.updateAlpha(node: self.noticeNode, alpha: 0.0) - transition.updateAlpha(node: self.termsOfServiceNode, alpha: 0.0) - navigationHeight = min(floor(availableHeight * 0.3), availableHeight - countryButtonHeight - inputFieldsHeight) - } else { - transition.updateAlpha(node: self.noticeNode, alpha: 1.0) - transition.updateAlpha(node: self.termsOfServiceNode, alpha: 1.0) - navigationHeight = floor(availableHeight * 0.3) - } - - transition.updateFrame(node: self.navigationBackgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: navigationHeight))) - transition.updateFrame(node: self.stripeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationHeight), size: CGSize(width: layout.size.width, height: UIScreenPixel))) - - let titleOffset: CGFloat - if navigationHeight * 0.5 < titleSize.height + minimalTitleSpacing { - titleOffset = floor((navigationHeight - titleSize.height) / 2.0) - } else { - titleOffset = max(navigationHeight * 0.5, navigationHeight - maxTitleSpacing - titleSize.height) - } - transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - titleSize.width) / 2.0), y: titleOffset), size: titleSize)) - - transition.updateFrame(node: self.countryButton, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationHeight), size: CGSize(width: layout.size.width, height: 67.0))) - transition.updateFrame(node: self.phoneBackground, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationHeight + 57.0), size: CGSize(width: layout.size.width, height: 57.0))) - - let countryCodeFrame = CGRect(origin: CGPoint(x: 18.0, y: navigationHeight + 58.0), size: CGSize(width: 60.0, height: 57.0)) - let numberFrame = CGRect(origin: CGPoint(x: 96.0, y: navigationHeight + 58.0), size: CGSize(width: layout.size.width - 96.0 - 8.0, height: 57.0)) - - let phoneInputFrame = countryCodeFrame.union(numberFrame) - - transition.updateFrame(node: self.phoneInputNode, frame: phoneInputFrame) - transition.updateFrame(node: self.phoneInputNode.countryCodeField, frame: countryCodeFrame.offsetBy(dx: -phoneInputFrame.minX, dy: -phoneInputFrame.minY)) - transition.updateFrame(node: self.phoneInputNode.numberField, frame: numberFrame.offsetBy(dx: -phoneInputFrame.minX, dy: -phoneInputFrame.minY)) - - let additionalAvailableHeight = max(1.0, availableHeight - phoneInputFrame.maxY) - let additionalAvailableSpacing = max(1.0, additionalAvailableHeight - noticeSize.height - termsOfServiceSize.height) - let noticeSpacingFactor = maxNoticeSpacing / (maxNoticeSpacing + maxTermsOfServiceSpacing + minTrailingSpacing) - let termsOfServiceSpacingFactor = maxTermsOfServiceSpacing / (maxNoticeSpacing + maxTermsOfServiceSpacing + minTrailingSpacing) - - let noticeSpacing: CGFloat - let termsOfServiceSpacing: CGFloat - if additionalAvailableHeight <= maxNoticeSpacing + noticeSize.height + maxTermsOfServiceSpacing + termsOfServiceSize.height + minTrailingSpacing { - termsOfServiceSpacing = min(floor(termsOfServiceSpacingFactor * additionalAvailableSpacing), maxTermsOfServiceSpacing) - noticeSpacing = floor((additionalAvailableHeight - termsOfServiceSpacing - noticeSize.height - termsOfServiceSize.height) / 2.0) - } else { - noticeSpacing = min(floor(noticeSpacingFactor * additionalAvailableSpacing), maxNoticeSpacing) - termsOfServiceSpacing = min(floor(termsOfServiceSpacingFactor * additionalAvailableSpacing), maxTermsOfServiceSpacing) - } - - let noticeFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - noticeSize.width) / 2.0), y: phoneInputFrame.maxY + noticeSpacing), size: noticeSize) - let termsOfServiceFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - termsOfServiceSize.width) / 2.0), y: noticeFrame.maxY + termsOfServiceSpacing), size: termsOfServiceSize) - - transition.updateFrame(node: self.noticeNode, frame: noticeFrame) - transition.updateFrame(node: self.termsOfServiceNode, frame: termsOfServiceFrame) - } - - func activateInput() { - self.phoneInputNode.numberField.textField.becomeFirstResponder() - } - - func animateError() { - self.phoneInputNode.countryCodeField.layer.addShakeAnimation() - self.phoneInputNode.numberField.layer.addShakeAnimation() - } - @objc func countryPressed() { self.selectCountryCode?() } + override func layout() { + super.layout() + + let size = self.bounds.size + + self.countryButton.frame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: 67.0)) + self.phoneBackground.frame = CGRect(origin: CGPoint(x: 0.0, y: size.height - 57.0), size: CGSize(width: size.width, height: 57.0)) + + let countryCodeFrame = CGRect(origin: CGPoint(x: 18.0, y: size.height - 57.0), size: CGSize(width: 60.0, height: 57.0)) + let numberFrame = CGRect(origin: CGPoint(x: 96.0, y: size.height - 57.0), size: CGSize(width: size.width - 96.0 - 8.0, height: 57.0)) + + let phoneInputFrame = countryCodeFrame.union(numberFrame) + + self.phoneInputNode.frame = phoneInputFrame + self.phoneInputNode.countryCodeField.frame = countryCodeFrame.offsetBy(dx: -phoneInputFrame.minX, dy: -phoneInputFrame.minY) + self.phoneInputNode.numberField.frame = numberFrame.offsetBy(dx: -phoneInputFrame.minX, dy: -phoneInputFrame.minY) + } +} + +final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode { + private let strings: PresentationStrings + private let theme: AuthorizationTheme + + private let titleNode: ASTextNode + private let noticeNode: ASTextNode + private let phoneAndCountryNode: PhoneAndCountryNode + private let termsOfServiceNode: ASTextNode + + var currentNumber: String { + return self.phoneAndCountryNode.phoneInputNode.number + } + + var codeAndNumber: (Int32?, String) { + get { + return self.phoneAndCountryNode.phoneInputNode.codeAndNumber + } set(value) { + self.phoneAndCountryNode.phoneInputNode.codeAndNumber = value + } + } + + var selectCountryCode: (() -> Void)? + + var inProgress: Bool = false { + didSet { + self.phoneAndCountryNode.phoneInputNode.enableEditing = !self.inProgress + self.phoneAndCountryNode.phoneInputNode.alpha = self.inProgress ? 0.6 : 1.0 + self.phoneAndCountryNode.countryButton.isEnabled = !self.inProgress + } + } + + init(strings: PresentationStrings, theme: AuthorizationTheme) { + self.strings = strings + self.theme = theme + + self.titleNode = ASTextNode() + self.titleNode.isLayerBacked = true + self.titleNode.displaysAsynchronously = false + self.titleNode.attributedText = NSAttributedString(string: strings.Login_PhoneTitle, font: Font.light(30.0), textColor: theme.primaryColor) + + self.noticeNode = ASTextNode() + self.noticeNode.isLayerBacked = true + self.noticeNode.displaysAsynchronously = false + self.noticeNode.attributedText = NSAttributedString(string: strings.Login_PhoneAndCountryHelp, font: Font.regular(16.0), textColor: theme.primaryColor, paragraphAlignment: .center) + + self.termsOfServiceNode = ASTextNode() + self.termsOfServiceNode.isLayerBacked = true + self.termsOfServiceNode.displaysAsynchronously = false + + let termsOfServiceAttributes = MarkdownAttributeSet(font: Font.regular(16.0), textColor: self.theme.primaryColor) + let termsOfServiceLinkAttributes = MarkdownAttributeSet(font: Font.regular(16.0), textColor: self.theme.accentColor) + + let termsString = parseMarkdownIntoAttributedString(self.strings.Login_TermsOfServiceLabel.replacingOccurrences(of: "]", with: "]()"), attributes: MarkdownAttributes(body: termsOfServiceAttributes, bold: termsOfServiceAttributes, link: termsOfServiceLinkAttributes, linkAttribute: { _ in + return nil + }), textAlignment: .center) + self.termsOfServiceNode.attributedText = termsString + + self.phoneAndCountryNode = PhoneAndCountryNode(strings: strings, theme: theme) + + super.init() + + self.setViewBlock({ + return UITracingLayerView() + }) + + self.backgroundColor = theme.backgroundColor + + self.addSubnode(self.titleNode) + self.addSubnode(self.termsOfServiceNode) + self.addSubnode(self.noticeNode) + self.addSubnode(self.phoneAndCountryNode) + + self.phoneAndCountryNode.selectCountryCode = { [weak self] in + self?.selectCountryCode?() + } + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + var insets = layout.insets(options: [.input]) + insets.top = navigationBarHeight + + if max(layout.size.width, layout.size.height) > 1023.0 { + self.titleNode.attributedText = NSAttributedString(string: strings.Login_PhoneTitle, font: Font.light(40.0), textColor: self.theme.primaryColor) + } else { + self.titleNode.attributedText = NSAttributedString(string: strings.Login_PhoneTitle, font: Font.light(30.0), textColor: self.theme.primaryColor) + } + + let titleSize = self.titleNode.measure(CGSize(width: layout.size.width, height: CGFloat.greatestFiniteMagnitude)) + let noticeSize = self.noticeNode.measure(CGSize(width: min(274.0, layout.size.width - 28.0), height: CGFloat.greatestFiniteMagnitude)) + let termsOfServiceSize = self.termsOfServiceNode.measure(CGSize(width: layout.size.width, height: CGFloat.greatestFiniteMagnitude)) + + var items: [AuthorizationLayoutItem] = [ + AuthorizationLayoutItem(node: self.titleNode, size: titleSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)), + AuthorizationLayoutItem(node: self.noticeNode, size: noticeSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 18.0, maxValue: 18.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)), + AuthorizationLayoutItem(node: self.phoneAndCountryNode, size: CGSize(width: layout.size.width, height: 115.0), spacingBefore: AuthorizationLayoutItemSpacing(weight: 54.0, maxValue: 54.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)), + AuthorizationLayoutItem(node: self.termsOfServiceNode, size: termsOfServiceSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 90.0, maxValue: 90.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)), + ] + + if layoutAuthorizationItems(bounds: CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: layout.size.height - insets.top - insets.bottom - 10.0)), items: items, transition: transition, failIfDoesNotFit: true) { + self.termsOfServiceNode.isHidden = false + } else { + items.removeLast() + let _ = layoutAuthorizationItems(bounds: CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: layout.size.height - insets.top - insets.bottom - 10.0)), items: items, transition: transition, failIfDoesNotFit: false) + self.termsOfServiceNode.isHidden = true + } + } + + func activateInput() { + self.phoneAndCountryNode.phoneInputNode.numberField.textField.becomeFirstResponder() + } + + func animateError() { + self.phoneAndCountryNode.phoneInputNode.countryCodeField.layer.addShakeAnimation() + self.phoneAndCountryNode.phoneInputNode.numberField.layer.addShakeAnimation() + } } diff --git a/TelegramUI/AuthorizationSequenceSignUpController.swift b/TelegramUI/AuthorizationSequenceSignUpController.swift index 586094f5f2..25678bc8e5 100644 --- a/TelegramUI/AuthorizationSequenceSignUpController.swift +++ b/TelegramUI/AuthorizationSequenceSignUpController.swift @@ -7,6 +7,9 @@ final class AuthorizationSequenceSignUpController: ViewController { return self.displayNode as! AuthorizationSequenceSignUpControllerNode } + private let strings: PresentationStrings + private let theme: AuthorizationTheme + var initialName: (String, String) = ("", "") var signUpWithName: ((String, String) -> Void)? @@ -15,7 +18,7 @@ final class AuthorizationSequenceSignUpController: ViewController { var inProgress: Bool = false { didSet { if self.inProgress { - let item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode()) + let item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: self.theme.accentColor)) self.navigationItem.rightBarButtonItem = item } else { self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Next", style: .done, target: self, action: #selector(self.nextPressed)) @@ -24,8 +27,11 @@ final class AuthorizationSequenceSignUpController: ViewController { } } - init() { - super.init(navigationBarTheme: AuthorizationSequenceController.navigationBarTheme) + init(strings: PresentationStrings, theme: AuthorizationTheme) { + self.strings = strings + self.theme = theme + + super.init(navigationBarTheme: AuthorizationSequenceController.navigationBarTheme(theme)) self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Next", style: .done, target: self, action: #selector(self.nextPressed)) } diff --git a/TelegramUI/AuthorizationSequenceSplashController.swift b/TelegramUI/AuthorizationSequenceSplashController.swift index f1dd146875..e40806e07e 100644 --- a/TelegramUI/AuthorizationSequenceSplashController.swift +++ b/TelegramUI/AuthorizationSequenceSplashController.swift @@ -9,13 +9,20 @@ final class AuthorizationSequenceSplashController: ViewController { return self.displayNode as! AuthorizationSequenceSplashControllerNode } - private let controller = RMIntroViewController() + private let theme: AuthorizationTheme + + private let controller: RMIntroViewController var nextPressed: (() -> Void)? - init() { + init(theme: AuthorizationTheme) { + self.theme = theme + self.controller = RMIntroViewController(backroundColor: theme.backgroundColor, primaryColor: theme.primaryColor, accentColor: theme.accentColor, regularDotColor: theme.disclosureControlColor, highlightedDotColor: theme.accentColor) + super.init(navigationBarTheme: nil) + self.statusBar.statusBarStyle = theme.statusBarStyle + self.controller.startMessaging = { [weak self] in self?.nextPressed?() } @@ -26,7 +33,7 @@ final class AuthorizationSequenceSplashController: ViewController { } override public func loadDisplayNode() { - self.displayNode = AuthorizationSequenceSplashControllerNode() + self.displayNode = AuthorizationSequenceSplashControllerNode(theme: self.theme) self.displayNodeDidLoad() } @@ -39,9 +46,9 @@ final class AuthorizationSequenceSplashController: ViewController { } override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated); - controller.viewWillAppear(false) + super.viewWillAppear(animated) self.addControllerIfNeeded() + controller.viewWillAppear(false) } override func viewDidAppear(_ animated: Bool) { diff --git a/TelegramUI/AuthorizationSequenceSplashControllerNode.swift b/TelegramUI/AuthorizationSequenceSplashControllerNode.swift index c15c5ec1ff..b76812d43d 100644 --- a/TelegramUI/AuthorizationSequenceSplashControllerNode.swift +++ b/TelegramUI/AuthorizationSequenceSplashControllerNode.swift @@ -3,14 +3,14 @@ import AsyncDisplayKit import Display final class AuthorizationSequenceSplashControllerNode: ASDisplayNode { - override init() { + init(theme: AuthorizationTheme) { super.init() self.setViewBlock({ return UITracingLayerView() }) - self.backgroundColor = UIColor.white + self.backgroundColor = theme.backgroundColor self.view.disablesInteractiveTransitionGestureRecognizer = true } diff --git a/TelegramUI/AuthorizationTheme.swift b/TelegramUI/AuthorizationTheme.swift new file mode 100644 index 0000000000..2dcd50ccf4 --- /dev/null +++ b/TelegramUI/AuthorizationTheme.swift @@ -0,0 +1,84 @@ +import Foundation +import UIKit +import Display + +final class AuthorizationTheme { + let statusBarStyle: StatusBarStyle + let navigationBarBackgroundColor: UIColor + let navigationBarTextColor: UIColor + let navigationBarSeparatorColor: UIColor + let searchBarBackgroundColor: UIColor + let searchBarFillColor: UIColor + let searchBarPlaceholderColor: UIColor + let searchBarTextColor: UIColor + let keyboardAppearance: UIKeyboardAppearance + let backgroundColor: UIColor + let primaryColor: UIColor + let separatorColor: UIColor + let itemHighlightedBackgroundColor: UIColor + let accentColor: UIColor + let destructiveColor: UIColor + let disclosureControlColor: UIColor + let textPlaceholderColor: UIColor + + init(statusBarStyle: StatusBarStyle, navigationBarBackgroundColor: UIColor, navigationBarTextColor: UIColor, navigationBarSeparatorColor: UIColor, searchBarBackgroundColor: UIColor, searchBarFillColor: UIColor, searchBarPlaceholderColor: UIColor, searchBarTextColor: UIColor, keyboardAppearance: UIKeyboardAppearance, backgroundColor: UIColor, primaryColor: UIColor, separatorColor: UIColor, itemHighlightedBackgroundColor: UIColor, accentColor: UIColor, destructiveColor: UIColor, disclosureControlColor: UIColor, textPlaceholderColor: UIColor) { + self.statusBarStyle = statusBarStyle + self.navigationBarBackgroundColor = navigationBarBackgroundColor + self.navigationBarTextColor = navigationBarTextColor + self.navigationBarSeparatorColor = navigationBarSeparatorColor + self.searchBarBackgroundColor = searchBarBackgroundColor + self.searchBarFillColor = searchBarFillColor + self.searchBarPlaceholderColor = searchBarPlaceholderColor + self.searchBarTextColor = searchBarTextColor + self.keyboardAppearance = keyboardAppearance + self.backgroundColor = backgroundColor + self.primaryColor = primaryColor + self.separatorColor = separatorColor + self.itemHighlightedBackgroundColor = itemHighlightedBackgroundColor + self.accentColor = accentColor + self.destructiveColor = destructiveColor + self.disclosureControlColor = disclosureControlColor + self.textPlaceholderColor = textPlaceholderColor + } +} + +let defaultLightAuthorizationTheme = AuthorizationTheme( + statusBarStyle: .Black, + navigationBarBackgroundColor: UIColor(red: 0.968626451, green: 0.968626451, blue: 0.968626451, alpha: 1.0), + navigationBarTextColor: .black, + navigationBarSeparatorColor: UIColor(red: 0.6953125, green: 0.6953125, blue: 0.6953125, alpha: 1.0), + searchBarBackgroundColor: .white, + searchBarFillColor: UIColor(rgb: 0xe9e9e9), + searchBarPlaceholderColor: UIColor(rgb: 0x8e8e93), + searchBarTextColor: .black, + keyboardAppearance: .default, + backgroundColor: .white, + primaryColor: .black, + separatorColor: .lightGray, + itemHighlightedBackgroundColor: .gray, + accentColor: .blue, + destructiveColor: .red, + disclosureControlColor: .lightGray, + textPlaceholderColor: .lightGray +) + +let defaultAuthorizationTheme = AuthorizationTheme( + statusBarStyle: .White, + navigationBarBackgroundColor: .black, + navigationBarTextColor: .white, + navigationBarSeparatorColor: UIColor(rgb: 0x252525), + searchBarBackgroundColor: .black, + searchBarFillColor: UIColor(rgb: 0x272728), + searchBarPlaceholderColor: UIColor(rgb: 0x5e5e5e), + searchBarTextColor: .white, + keyboardAppearance: .dark, + backgroundColor: .black, + primaryColor: .white, + separatorColor: UIColor(rgb: 0x252525), + itemHighlightedBackgroundColor: UIColor(rgb: 0x1b1b1b), + accentColor: .white, + destructiveColor: UIColor(rgb: 0xFF736B), + disclosureControlColor: UIColor(rgb: 0x717171), + textPlaceholderColor: UIColor(rgb: 0x4d4d4d) +) + diff --git a/TelegramUI/AutomaticMediaDownloadSettings.swift b/TelegramUI/AutomaticMediaDownloadSettings.swift index 7417973018..fd07a37b95 100644 --- a/TelegramUI/AutomaticMediaDownloadSettings.swift +++ b/TelegramUI/AutomaticMediaDownloadSettings.swift @@ -203,7 +203,8 @@ func updateMediaDownloadSettingsInteractively(postbox: Postbox, _ f: @escaping ( } else { currentSettings = AutomaticMediaDownloadSettings.defaultSettings } - return f(currentSettings) + let updated = f(currentSettings) + return updated }) } } diff --git a/TelegramUI/AvatarGalleryController.swift b/TelegramUI/AvatarGalleryController.swift index 89d945488f..37b5e7f235 100644 --- a/TelegramUI/AvatarGalleryController.swift +++ b/TelegramUI/AvatarGalleryController.swift @@ -77,7 +77,7 @@ func fetchedAvatarGalleryEntries(account: Account, peer: Peer) -> Signal<[Avatar for photo in photos { let indexData = GalleryItemIndexData(position: index, totalCount: Int32(photos.count)) if result.isEmpty, let first = initialEntries.first { - let image = TelegramMediaImage(imageId: photo.image.imageId, representations: first.representations) + let image = TelegramMediaImage(imageId: photo.image.imageId, representations: first.representations, reference: photo.reference) result.append(.image(image, indexData)) } else { result.append(.image(photo.image, indexData)) @@ -129,7 +129,8 @@ class AvatarGalleryController: ViewController { super.init(navigationBarTheme: GalleryController.darkNavigationTheme) - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)) + let backItem = UIBarButtonItem(backButtonAppearanceWithTitle: self.presentationData.strings.Common_Back, target: self, action: #selector(self.donePressed)) + self.navigationItem.leftBarButtonItem = backItem self.statusBar.statusBarStyle = .White diff --git a/TelegramUI/AvatarNode.swift b/TelegramUI/AvatarNode.swift index a80fd3871c..d6fc453d48 100644 --- a/TelegramUI/AvatarNode.swift +++ b/TelegramUI/AvatarNode.swift @@ -6,17 +6,21 @@ import Display import TelegramCore import SwiftSignalKit +private let savedMessagesIcon = UIImage(bundleImageName: "Avatar/SavedMessagesIcon")?.precomposed() + private class AvatarNodeParameters: NSObject { let accountPeerId: PeerId? let peerId: PeerId? let letters: [String] let font: UIFont + let savedMessagesIcon: Bool - init(accountPeerId: PeerId?, peerId: PeerId?, letters: [String], font: UIFont) { + init(accountPeerId: PeerId?, peerId: PeerId?, letters: [String], font: UIFont, savedMessagesIcon: Bool) { self.accountPeerId = accountPeerId self.peerId = peerId self.letters = letters self.font = font + self.savedMessagesIcon = savedMessagesIcon super.init() } @@ -25,15 +29,20 @@ private class AvatarNodeParameters: NSObject { private let gradientColors: [NSArray] = [ [UIColor(rgb: 0xff516a).cgColor, UIColor(rgb: 0xff885e).cgColor], [UIColor(rgb: 0xffa85c).cgColor, UIColor(rgb: 0xffcd6a).cgColor], - [UIColor(rgb: 0x54cb68).cgColor, UIColor(rgb: 0xa0de7e).cgColor], - [UIColor(rgb: 0x2a9ef1).cgColor, UIColor(rgb: 0x72d5fd).cgColor], [UIColor(rgb: 0x665fff).cgColor, UIColor(rgb: 0x82b1ff).cgColor], + [UIColor(rgb: 0x54cb68).cgColor, UIColor(rgb: 0xa0de7e).cgColor], + [UIColor(rgb: 0x4acccd).cgColor, UIColor(rgb: 0x00fcfd).cgColor], + [UIColor(rgb: 0x2a9ef1).cgColor, UIColor(rgb: 0x72d5fd).cgColor], [UIColor(rgb: 0xd669ed).cgColor, UIColor(rgb: 0xe0a2f3).cgColor], ] private let grayscaleColors: NSArray = [ UIColor(rgb: 0xefefef).cgColor, UIColor(rgb: 0xeeeeee).cgColor ] + +private let savedMessagesColors: NSArray = [ + UIColor(rgb: 0x2a9ef1).cgColor, UIColor(rgb: 0x72d5fd).cgColor +] private enum AvatarNodeState: Equatable { case empty @@ -54,12 +63,18 @@ private func ==(lhs: AvatarNodeState, rhs: AvatarNodeState) -> Bool { } } +public enum AvatarNodeImageOverride { + case none + case image(TelegramMediaImageRepresentation) + case savedMessagesIcon +} + public final class AvatarNode: ASDisplayNode { var font: UIFont { didSet { if oldValue !== font { if let parameters = self.parameters { - self.parameters = AvatarNodeParameters(accountPeerId: parameters.accountPeerId, peerId: parameters.peerId, letters: parameters.letters, font: self.font) + self.parameters = AvatarNodeParameters(accountPeerId: parameters.accountPeerId, peerId: parameters.peerId, letters: parameters.letters, font: self.font, savedMessagesIcon: parameters.savedMessagesIcon) } if !self.displaySuspended { @@ -111,10 +126,19 @@ public final class AvatarNode: ASDisplayNode { } } - public func setPeer(account: Account, peer: Peer, temporaryRepresentation: TelegramMediaImageRepresentation? = nil) { + public func setPeer(account: Account, peer: Peer, overrideImage: AvatarNodeImageOverride? = nil) { var representation: TelegramMediaImageRepresentation? - if let temporaryRepresentation = temporaryRepresentation { - representation = temporaryRepresentation + var savedMessagesIcon = false + if let overrideImage = overrideImage { + switch overrideImage { + case .none: + representation = nil + case let .image(image): + representation = image + case .savedMessagesIcon: + representation = nil + savedMessagesIcon = true + } } else { representation = peer.smallProfileImage } @@ -122,17 +146,20 @@ public final class AvatarNode: ASDisplayNode { if updatedState != self.state { self.state = updatedState - let parameters = AvatarNodeParameters(accountPeerId: account.peerId, peerId: peer.id, letters: peer.displayLetters, font: self.font) + let parameters = AvatarNodeParameters(accountPeerId: account.peerId, peerId: peer.id, letters: peer.displayLetters, font: self.font, savedMessagesIcon: savedMessagesIcon) self.displaySuspended = true self.contents = nil - if let signal = peerAvatarImage(account: account, peer: peer, temporaryRepresentation: temporaryRepresentation) { + if let signal = peerAvatarImage(account: account, representation: representation) { self.imageReady.set(self.imageNode.ready) self.imageNode.setSignal(signal) } else { self.imageReady.set(.single(true)) self.displaySuspended = false + if self.isNodeLoaded { + self.imageNode.contents = nil + } } if self.parameters == nil || self.parameters != parameters { self.parameters = parameters @@ -146,7 +173,7 @@ public final class AvatarNode: ASDisplayNode { if updatedState != self.state { self.state = updatedState - let parameters = AvatarNodeParameters(accountPeerId: nil, peerId: nil, letters: letters, font: self.font) + let parameters = AvatarNodeParameters(accountPeerId: nil, peerId: nil, letters: letters, font: self.font, savedMessagesIcon: false) self.displaySuspended = true self.contents = nil @@ -193,7 +220,9 @@ public final class AvatarNode: ASDisplayNode { } let colorsArray: NSArray - if colorIndex == -1 { + if let parameters = parameters as? AvatarNodeParameters, parameters.savedMessagesIcon { + colorsArray = savedMessagesColors + } else if colorIndex == -1 { colorsArray = grayscaleColors } else { colorsArray = gradientColors[colorIndex % gradientColors.count] @@ -209,23 +238,34 @@ public final class AvatarNode: ASDisplayNode { context.setBlendMode(.normal) if let parameters = parameters as? AvatarNodeParameters { - let letters = parameters.letters - let string = letters.count == 0 ? "" : (letters[0] + (letters.count == 1 ? "" : letters[1])) - let attributedString = NSAttributedString(string: string, attributes: [NSAttributedStringKey.font: parameters.font, NSAttributedStringKey.foregroundColor: UIColor.white]) - - let line = CTLineCreateWithAttributedString(attributedString) - let lineBounds = CTLineGetBoundsWithOptions(line, .useGlyphPathBounds) - - let lineOffset = CGPoint(x: string == "B" ? 1.0 : 0.0, y: 0.0) - let lineOrigin = CGPoint(x: floorToScreenPixels(-lineBounds.origin.x + (bounds.size.width - lineBounds.size.width) / 2.0) + lineOffset.x, y: floorToScreenPixels(-lineBounds.origin.y + (bounds.size.height - lineBounds.size.height) / 2.0)) - - context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0) - context.scaleBy(x: 1.0, y: -1.0) - context.translateBy(x: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0) - - context.translateBy(x: lineOrigin.x, y: lineOrigin.y) - CTLineDraw(line, context) - context.translateBy(x: -lineOrigin.x, y: -lineOrigin.y) + if parameters.savedMessagesIcon { + let factor = bounds.size.width / 60.0 + context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0) + context.scaleBy(x: factor, y: -factor) + context.translateBy(x: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0) + + if let savedMessagesIcon = savedMessagesIcon { + context.draw(savedMessagesIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - savedMessagesIcon.size.width) / 2.0), y: floor((bounds.size.height - savedMessagesIcon.size.height) / 2.0)), size: savedMessagesIcon.size)) + } + } else { + let letters = parameters.letters + let string = letters.count == 0 ? "" : (letters[0] + (letters.count == 1 ? "" : letters[1])) + let attributedString = NSAttributedString(string: string, attributes: [NSAttributedStringKey.font: parameters.font, NSAttributedStringKey.foregroundColor: UIColor.white]) + + let line = CTLineCreateWithAttributedString(attributedString) + let lineBounds = CTLineGetBoundsWithOptions(line, .useGlyphPathBounds) + + let lineOffset = CGPoint(x: string == "B" ? 1.0 : 0.0, y: 0.0) + let lineOrigin = CGPoint(x: floorToScreenPixels(-lineBounds.origin.x + (bounds.size.width - lineBounds.size.width) / 2.0) + lineOffset.x, y: floorToScreenPixels(-lineBounds.origin.y + (bounds.size.height - lineBounds.size.height) / 2.0)) + + context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0) + + context.translateBy(x: lineOrigin.x, y: lineOrigin.y) + CTLineDraw(line, context) + context.translateBy(x: -lineOrigin.x, y: -lineOrigin.y) + } } } diff --git a/TelegramUI/BlockedPeersController.swift b/TelegramUI/BlockedPeersController.swift index 2c7da6102b..097f8c8dc7 100644 --- a/TelegramUI/BlockedPeersController.swift +++ b/TelegramUI/BlockedPeersController.swift @@ -9,11 +9,13 @@ private final class BlockedPeersControllerArguments { let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void let removePeer: (PeerId) -> Void + let openPeer: (Peer) -> Void - init(account: Account, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, removePeer: @escaping (PeerId) -> Void) { + init(account: Account, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, removePeer: @escaping (PeerId) -> Void, openPeer: @escaping (Peer) -> Void) { self.account = account self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions self.removePeer = removePeer + self.openPeer = openPeer } } @@ -102,7 +104,9 @@ private enum BlockedPeersEntry: ItemListNodeEntry { func item(_ arguments: BlockedPeersControllerArguments) -> ListViewItem { switch self { case let .peerItem(_, theme, strings, peer, editing, enabled): - return ItemListPeerItem(theme: theme, strings: strings, account: arguments.account, peer: peer, presence: nil, text: .none, label: .none, editing: editing, switchValue: nil, enabled: enabled, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { previousId, id in + return ItemListPeerItem(theme: theme, strings: strings, account: arguments.account, peer: peer, presence: nil, text: .none, label: .none, editing: editing, switchValue: nil, enabled: enabled, sectionId: self.section, action: { + arguments.openPeer(peer) + }, setPeerIdWithRevealedOptions: { previousId, id in arguments.setPeerIdWithRevealedOptions(previousId, id) }, removePeer: { peerId in arguments.removePeer(peerId) @@ -176,7 +180,7 @@ public func blockedPeersController(account: Account) -> ViewController { statePromise.set(stateValue.modify { f($0) }) } - var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)? + var pushControllerImpl: ((ViewController) -> Void)? let actionsDisposable = DisposableSet() @@ -227,6 +231,10 @@ public func blockedPeersController(account: Account) -> ViewController { } })) + }, openPeer: { peer in + if let controller = peerInfoController(account: account, peer: peer) { + pushControllerImpl?(controller) + } }) let peersSignal: Signal<[Peer]?, NoError> = .single(nil) |> then(requestBlockedPeers(account: account) |> map { Optional($0) }) @@ -241,13 +249,13 @@ public func blockedPeersController(account: Account) -> ViewController { var rightNavigationButton: ItemListNavigationButton? if let peers = peers, !peers.isEmpty { if state.editing { - rightNavigationButton = ItemListNavigationButton(title: "Done", style: .bold, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Done, style: .bold, enabled: true, action: { updateState { state in return state.withUpdatedEditing(false) } }) } else { - rightNavigationButton = ItemListNavigationButton(title: "Edit", style: .regular, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Edit, style: .regular, enabled: true, action: { updateState { state in return state.withUpdatedEditing(true) } @@ -258,16 +266,16 @@ public func blockedPeersController(account: Account) -> ViewController { var emptyStateItem: ItemListControllerEmptyStateItem? if let peers = peers { if peers.isEmpty { - emptyStateItem = ItemListTextEmptyStateItem(text: "Blocked users can't send you messages of add you to groups. They will not see your profile pictures, online and last seen status.") + emptyStateItem = ItemListTextEmptyStateItem(text: presentationData.strings.BlockedUsers_Info) } } else if peers == nil { - emptyStateItem = ItemListLoadingIndicatorEmptyStateItem() + emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme) } let previous = previousPeers previousPeers = peers - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text("Blocked Users"), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: "Back"), animateChanges: true) + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.BlockedUsers_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) let listState = ItemListNodeState(entries: blockedPeersControllerEntries(presentationData: presentationData, state: state, peers: peers), style: .blocks, emptyStateItem: emptyStateItem, animateChanges: previous != nil && peers != nil && previous!.count >= peers!.count) return (controllerState, (listState, arguments)) @@ -276,9 +284,9 @@ public func blockedPeersController(account: Account) -> ViewController { } let controller = ItemListController(account: account, state: signal) - presentControllerImpl = { [weak controller] c, p in + pushControllerImpl = { [weak controller] c in if let controller = controller { - controller.present(c, in: .window(.root), with: p) + (controller.navigationController as? NavigationController)?.pushViewController(c) } } return controller diff --git a/TelegramUI/BotCheckoutActionButton.swift b/TelegramUI/BotCheckoutActionButton.swift index 92e6f676b0..c431a0d73d 100644 --- a/TelegramUI/BotCheckoutActionButton.swift +++ b/TelegramUI/BotCheckoutActionButton.swift @@ -129,7 +129,7 @@ final class BotCheckoutActionButton: HighlightTrackingButtonNode { case let .active(title): if case .active = previousState { let makeLayout = TextNode.asyncLayout(self.labelNode) - let (labelLayout, labelApply) = makeLayout(NSAttributedString(string: title, font: titleFont, textColor: self.foregroundColor), nil, 1, .end, validLayout, .natural, nil, UIEdgeInsets()) + let (labelLayout, labelApply) = makeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: titleFont, textColor: self.foregroundColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: validLayout, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) self.labelNode.frame = CGRect(origin: CGPoint(x: floor((validLayout.width - labelLayout.size.width) / 2.0), y: floor((validLayout.height - labelLayout.size.height) / 2.0)), size: labelLayout.size) let _ = labelApply() } else { @@ -142,7 +142,7 @@ final class BotCheckoutActionButton: HighlightTrackingButtonNode { self.activeBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) let makeLayout = TextNode.asyncLayout(self.labelNode) - let (labelLayout, labelApply) = makeLayout(NSAttributedString(string: title, font: titleFont, textColor: self.foregroundColor), nil, 1, .end, validLayout, .natural, nil, UIEdgeInsets()) + let (labelLayout, labelApply) = makeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: titleFont, textColor: self.foregroundColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: validLayout, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) self.labelNode.frame = CGRect(origin: CGPoint(x: floor((validLayout.width - labelLayout.size.width) / 2.0), y: floor((validLayout.height - labelLayout.size.height) / 2.0)), size: labelLayout.size) let _ = labelApply() self.labelNode.alpha = 1.0 @@ -151,7 +151,7 @@ final class BotCheckoutActionButton: HighlightTrackingButtonNode { case let .inactive(title): if case .inactive = previousState { let makeLayout = TextNode.asyncLayout(self.labelNode) - let (labelLayout, labelApply) = makeLayout(NSAttributedString(string: title, font: titleFont, textColor: self.activeFillColor), nil, 1, .end, validLayout, .natural, nil, UIEdgeInsets()) + let (labelLayout, labelApply) = makeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: titleFont, textColor: self.activeFillColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: validLayout, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) self.labelNode.frame = CGRect(origin: CGPoint(x: floor((validLayout.width - labelLayout.size.width) / 2.0), y: floor((validLayout.height - labelLayout.size.height) / 2.0)), size: labelLayout.size) let _ = labelApply() } else { @@ -162,7 +162,7 @@ final class BotCheckoutActionButton: HighlightTrackingButtonNode { self.activeBackgroundNode.alpha = 0.0 let makeLayout = TextNode.asyncLayout(self.labelNode) - let (labelLayout, labelApply) = makeLayout(NSAttributedString(string: title, font: titleFont, textColor: self.foregroundColor), nil, 1, .end, validLayout, .natural, nil, UIEdgeInsets()) + let (labelLayout, labelApply) = makeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: titleFont, textColor: self.foregroundColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: validLayout, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) self.labelNode.frame = CGRect(origin: CGPoint(x: floor((validLayout.width - labelLayout.size.width) / 2.0), y: floor((validLayout.height - labelLayout.size.height) / 2.0)), size: labelLayout.size) let _ = labelApply() self.labelNode.alpha = 1.0 @@ -233,12 +233,12 @@ final class BotCheckoutActionButton: HighlightTrackingButtonNode { switch state { case let .active(title): let makeLayout = TextNode.asyncLayout(self.labelNode) - let (labelLayout, labelApply) = makeLayout(NSAttributedString(string: title, font: titleFont, textColor: self.foregroundColor), nil, 1, .end, size, .natural, nil, UIEdgeInsets()) + let (labelLayout, labelApply) = makeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: titleFont, textColor: self.foregroundColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: size, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let _ = labelApply() labelSize = labelLayout.size case let .inactive(title): let makeLayout = TextNode.asyncLayout(self.labelNode) - let (labelLayout, labelApply) = makeLayout(NSAttributedString(string: title, font: titleFont, textColor: self.activeFillColor), nil, 1, .end, size, .natural, nil, UIEdgeInsets()) + let (labelLayout, labelApply) = makeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: titleFont, textColor: self.activeFillColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: size, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let _ = labelApply() labelSize = labelLayout.size default: diff --git a/TelegramUI/BotCheckoutControllerNode.swift b/TelegramUI/BotCheckoutControllerNode.swift index 27a6443ef3..4fb4a51ab0 100644 --- a/TelegramUI/BotCheckoutControllerNode.swift +++ b/TelegramUI/BotCheckoutControllerNode.swift @@ -524,7 +524,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode, override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { var updatedInsets = layout.intrinsicInsets updatedInsets.bottom += BotCheckoutActionButton.diameter + 20.0 - super.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: updatedInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight), navigationBarHeight: navigationBarHeight, transition: transition) + super.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: updatedInsets, safeInsets: layout.safeInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging), navigationBarHeight: navigationBarHeight, transition: transition) let actionButtonFrame = CGRect(origin: CGPoint(x: 10.0, y: layout.size.height - 10.0 - BotCheckoutActionButton.diameter), size: CGSize(width: layout.size.width - 20.0, height: BotCheckoutActionButton.diameter)) transition.updateFrame(node: self.actionButton, frame: actionButtonFrame) @@ -743,7 +743,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode, private func requestPassword(cardTitle: String) { let period: Int32 let requiresBiometrics: Bool - if LocalAuth.isTouchIDAvailable { + if LocalAuth.biometricAuthentication != nil { period = 5 * 60 * 60 requiresBiometrics = true } else { diff --git a/TelegramUI/BotCheckoutHeaderItem.swift b/TelegramUI/BotCheckoutHeaderItem.swift index abff88a68a..47d00533b7 100644 --- a/TelegramUI/BotCheckoutHeaderItem.swift +++ b/TelegramUI/BotCheckoutHeaderItem.swift @@ -19,10 +19,10 @@ class BotCheckoutHeaderItem: ListViewItem, ItemListItem { self.sectionId = sectionId } - func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { async { let node = BotCheckoutHeaderItemNode() - let (layout, apply) = node.asyncLayout()(self, width, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) node.contentSize = layout.contentSize node.insets = layout.insets @@ -33,13 +33,13 @@ class BotCheckoutHeaderItem: ListViewItem, ItemListItem { } } - func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { if let node = node as? BotCheckoutHeaderItemNode { Queue.mainQueue().async { let makeLayout = node.asyncLayout() async { - let (layout, apply) = makeLayout(self, width, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) Queue.mainQueue().async { completion(layout, { apply() @@ -108,7 +108,7 @@ class BotCheckoutHeaderItemNode: ListViewItemNode { self.addSubnode(self.botNameNode) } - func asyncLayout() -> (_ item: BotCheckoutHeaderItem, _ width: CGFloat, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + func asyncLayout() -> (_ item: BotCheckoutHeaderItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeTextLayout = TextNode.asyncLayout(self.textNode) let makeBotNameLayout = TextNode.asyncLayout(self.botNameNode) @@ -116,7 +116,7 @@ class BotCheckoutHeaderItemNode: ListViewItemNode { let currentItem = self.item - return { item, width, neighbors in + return { item, params, neighbors in var updatedTheme: PresentationTheme? if currentItem?.theme !== item.theme { @@ -135,7 +135,7 @@ class BotCheckoutHeaderItemNode: ListViewItemNode { let textColor = item.theme.list.itemPrimaryTextColor - let contentInsets = UIEdgeInsets(top: 15.0, left: 15.0, bottom: 15.0, right: 15.0) + let contentInsets = UIEdgeInsets(top: 15.0, left: 15.0 + params.leftInset, bottom: 15.0, right: 15.0 + params.rightInset) let separatorHeight = UIScreenPixel let titleTextSpacing: CGFloat = 1.0 let textBotNameSpacing: CGFloat = 3.0 @@ -144,7 +144,7 @@ class BotCheckoutHeaderItemNode: ListViewItemNode { let imageSize = CGSize(width: 134.0, height: 134.0) let maxTextHeight = imageSize.height - var maxTextWidth = width - contentInsets.left - contentInsets.right + var maxTextWidth = params.width - contentInsets.left - contentInsets.right var imageApply: (() -> Void)? var updatedImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? @@ -157,11 +157,11 @@ class BotCheckoutHeaderItemNode: ListViewItemNode { } } - let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: item.invoice.title, font: titleFont, textColor: textColor), nil, 1, .end, CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.invoice.title, font: titleFont, textColor: textColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let (botNameLayout, botNameApply) = makeBotNameLayout(NSAttributedString(string: item.botName, font: textFont, textColor: item.theme.list.itemSecondaryTextColor), nil, 1, .end, CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let (botNameLayout, botNameApply) = makeBotNameLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.botName, font: textFont, textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let (textLayout, textApply) = makeTextLayout(NSAttributedString(string: item.invoice.description, font: textFont, textColor: textColor), nil, 0, .end, CGSize(width: maxTextWidth, height: maxTextHeight - titleLayout.size.height - titleTextSpacing - botNameLayout.size.height - textBotNameSpacing), .natural, nil, UIEdgeInsets()) + let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.invoice.description, font: textFont, textColor: textColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: maxTextHeight - titleLayout.size.height - titleTextSpacing - botNameLayout.size.height - textBotNameSpacing), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let contentHeight: CGFloat if let _ = imageApply { @@ -170,7 +170,7 @@ class BotCheckoutHeaderItemNode: ListViewItemNode { contentHeight = contentInsets.top + contentInsets.bottom + titleLayout.size.height + titleTextSpacing + textLayout.size.height + textBotNameSpacing + botNameLayout.size.height } - let contentSize = CGSize(width: width, height: contentHeight) + let contentSize = CGSize(width: params.width, height: contentHeight) let insets = itemListNeighborsPlainInsets(neighbors) let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) @@ -180,9 +180,9 @@ class BotCheckoutHeaderItemNode: ListViewItemNode { strongSelf.item = item if let _ = updatedTheme { - strongSelf.topStripeNode.backgroundColor = item.theme.list.itemSeparatorColor - strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemSeparatorColor - strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBackgroundColor + strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor } @@ -193,7 +193,7 @@ class BotCheckoutHeaderItemNode: ListViewItemNode { if let imageApply = imageApply { let _ = imageApply() if let updatedImageSignal = updatedImageSignal { - strongSelf.imageNode.setSignal(account: item.account, signal: updatedImageSignal) + strongSelf.imageNode.setSignal(updatedImageSignal) } strongSelf.imageNode.isHidden = false } else { @@ -211,7 +211,7 @@ class BotCheckoutHeaderItemNode: ListViewItemNode { strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0) } - strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: contentSize.height - separatorHeight), size: CGSize(width: width, height: separatorHeight)) + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: contentSize.height - separatorHeight), size: CGSize(width: params.width, height: separatorHeight)) var titleFrame = CGRect(origin: CGPoint(x: contentInsets.left, y: contentInsets.top), size: titleLayout.size) if let _ = imageApply { @@ -224,14 +224,14 @@ class BotCheckoutHeaderItemNode: ListViewItemNode { strongSelf.botNameNode.frame = CGRect(origin: CGPoint(x: textFrame.minX, y: textFrame.maxY + textBotNameSpacing), size: botNameLayout.size) - strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: width, height: 44.0 + UIScreenPixel + UIScreenPixel)) + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: 44.0 + UIScreenPixel + UIScreenPixel)) } }) } } - override func setHighlighted(_ highlighted: Bool, animated: Bool) { - super.setHighlighted(highlighted, animated: animated) + override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { + super.setHighlighted(highlighted, at: point, animated: animated) if highlighted { self.highlightedBackgroundNode.alpha = 1.0 diff --git a/TelegramUI/BotCheckoutInfoController.swift b/TelegramUI/BotCheckoutInfoController.swift index e10817dcd5..f1e9f8bf45 100644 --- a/TelegramUI/BotCheckoutInfoController.swift +++ b/TelegramUI/BotCheckoutInfoController.swift @@ -65,7 +65,7 @@ final class BotCheckoutInfoController: ViewController { self?.presentingViewController?.dismiss(animated: false, completion: nil) }, openCountrySelection: { [weak self] in if let strongSelf = self { - let controller = AuthorizationSequenceCountrySelectionController(displayCodes: false) + let controller = AuthorizationSequenceCountrySelectionController(strings: strongSelf.presentationData.strings, theme: defaultLightAuthorizationTheme, displayCodes: false) controller.completeWithCountryCode = { _, id in if let strongSelf = self { strongSelf.controllerNode.updateCountry(id) diff --git a/TelegramUI/BotCheckoutNativeCardEntryController.swift b/TelegramUI/BotCheckoutNativeCardEntryController.swift index 04a720c9b8..6603ebb698 100644 --- a/TelegramUI/BotCheckoutNativeCardEntryController.swift +++ b/TelegramUI/BotCheckoutNativeCardEntryController.swift @@ -70,7 +70,7 @@ final class BotCheckoutNativeCardEntryController: ViewController { self?.presentingViewController?.dismiss(animated: false, completion: nil) }, openCountrySelection: { [weak self] in if let strongSelf = self { - let controller = AuthorizationSequenceCountrySelectionController(displayCodes: false) + let controller = AuthorizationSequenceCountrySelectionController(strings: strongSelf.presentationData.strings, theme: defaultLightAuthorizationTheme, displayCodes: false) controller.completeWithCountryCode = { _, id in if let strongSelf = self { strongSelf.controllerNode.updateCountry(id) diff --git a/TelegramUI/BotCheckoutPriceItem.swift b/TelegramUI/BotCheckoutPriceItem.swift index 6ff432b948..e84220c052 100644 --- a/TelegramUI/BotCheckoutPriceItem.swift +++ b/TelegramUI/BotCheckoutPriceItem.swift @@ -20,10 +20,10 @@ class BotCheckoutPriceItem: ListViewItem, ItemListItem { self.sectionId = sectionId } - func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { async { let node = BotCheckoutPriceItemNode() - let (layout, apply) = node.asyncLayout()(self, width, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) node.contentSize = layout.contentSize node.insets = layout.insets @@ -34,13 +34,13 @@ class BotCheckoutPriceItem: ListViewItem, ItemListItem { } } - func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { if let node = node as? BotCheckoutPriceItemNode { Queue.mainQueue().async { let makeLayout = node.asyncLayout() async { - let (layout, apply) = makeLayout(self, width, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) Queue.mainQueue().async { completion(layout, { apply() @@ -93,14 +93,14 @@ class BotCheckoutPriceItemNode: ListViewItemNode { self.addSubnode(self.labelNode) } - func asyncLayout() -> (_ item: BotCheckoutPriceItem, _ width: CGFloat, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + func asyncLayout() -> (_ item: BotCheckoutPriceItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeLabelLayout = TextNode.asyncLayout(self.labelNode) - return { item, width, neighbors in - let rightInset: CGFloat = 16.0 + return { item, params, neighbors in + let rightInset: CGFloat = 16.0 + params.rightInset - let contentSize = CGSize(width: width, height: 34.0) + let contentSize = CGSize(width: params.width, height: 34.0) let insets = priceItemInsets(neighbors) let textFont: UIFont @@ -113,8 +113,8 @@ class BotCheckoutPriceItemNode: ListViewItemNode { textColor = item.theme.list.itemSecondaryTextColor } - let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: item.title, font: textFont, textColor: textColor), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) - let (labelLayout, labelApply) = makeLabelLayout(NSAttributedString(string: item.label, font: textFont, textColor: textColor), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: textFont, textColor: textColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.label, font: textFont, textColor: textColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in if let strongSelf = self { @@ -123,10 +123,10 @@ class BotCheckoutPriceItemNode: ListViewItemNode { let _ = titleApply() let _ = labelApply() - let leftInset: CGFloat = 16.0 + let leftInset: CGFloat = 16.0 + params.leftInset strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floor((contentSize.height - titleLayout.size.height) / 2.0)), size: titleLayout.size) - strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: width - rightInset - labelLayout.size.width, y: floor((contentSize.height - labelLayout.size.height) / 2.0)), size: labelLayout.size) + strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: params.width - rightInset - labelLayout.size.width, y: floor((contentSize.height - labelLayout.size.height) / 2.0)), size: labelLayout.size) } }) } diff --git a/TelegramUI/BotPaymentItemNode.swift b/TelegramUI/BotPaymentItemNode.swift index d313913173..050f2b81b2 100644 --- a/TelegramUI/BotPaymentItemNode.swift +++ b/TelegramUI/BotPaymentItemNode.swift @@ -34,9 +34,9 @@ class BotPaymentItemNode: ASDisplayNode { final func updateLayout(theme: PresentationTheme, width: CGFloat, measuredInset: CGFloat, previousItemNode: BotPaymentItemNode?, nextItemNode: BotPaymentItemNode?, transition: ContainedViewLayoutTransition) -> CGFloat { if self.theme !== theme { self.theme = theme - self.backgroundNode.backgroundColor = theme.list.itemBackgroundColor - self.topSeparatorNode.backgroundColor = theme.list.itemSeparatorColor - self.bottomSeparatorNode.backgroundColor = theme.list.itemSeparatorColor + self.backgroundNode.backgroundColor = theme.list.itemBlocksBackgroundColor + self.topSeparatorNode.backgroundColor = theme.list.itemBlocksSeparatorColor + self.bottomSeparatorNode.backgroundColor = theme.list.itemBlocksSeparatorColor } let height = self.layoutContents(theme: theme, width: width, measuredInset: measuredInset, transition: transition) diff --git a/TelegramUI/BotReceiptControllerNode.swift b/TelegramUI/BotReceiptControllerNode.swift index a203eaebba..f295fa95f5 100644 --- a/TelegramUI/BotReceiptControllerNode.swift +++ b/TelegramUI/BotReceiptControllerNode.swift @@ -296,7 +296,7 @@ final class BotReceiptControllerNode: ItemListControllerNode { override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { var updatedInsets = layout.intrinsicInsets updatedInsets.bottom += BotCheckoutActionButton.diameter + 20.0 - super.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: updatedInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight), navigationBarHeight: navigationBarHeight, transition: transition) + super.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: updatedInsets, safeInsets: layout.safeInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging), navigationBarHeight: navigationBarHeight, transition: transition) let actionButtonFrame = CGRect(origin: CGPoint(x: 10.0, y: layout.size.height - 10.0 - BotCheckoutActionButton.diameter), size: CGSize(width: layout.size.width - 20.0, height: BotCheckoutActionButton.diameter)) transition.updateFrame(node: self.actionButton, frame: actionButtonFrame) diff --git a/TelegramUI/CachedResourceRepresentations.swift b/TelegramUI/CachedResourceRepresentations.swift index 018078801b..54ae456c3c 100644 --- a/TelegramUI/CachedResourceRepresentations.swift +++ b/TelegramUI/CachedResourceRepresentations.swift @@ -26,20 +26,27 @@ final class CachedStickerAJpegRepresentation: CachedMediaResourceRepresentation } } +enum CachedScaledImageRepresentationMode: Int32 { + case fill = 0 + case aspectFit = 1 +} + final class CachedScaledImageRepresentation: CachedMediaResourceRepresentation { let size: CGSize + let mode: CachedScaledImageRepresentationMode var uniqueId: String { - return "scaled-image-\(Int(self.size.width))x\(Int(self.size.height))" + return "scaled-image-\(Int(self.size.width))x\(Int(self.size.height))-\(self.mode.rawValue)" } - init(size: CGSize) { + init(size: CGSize, mode: CachedScaledImageRepresentationMode) { self.size = size + self.mode = mode } func isEqual(to: CachedMediaResourceRepresentation) -> Bool { if let to = to as? CachedScaledImageRepresentation { - return self.size == to.size + return self.size == to.size && self.mode == to.mode } else { return false } diff --git a/TelegramUI/CalculatingCacheSizeItem.swift b/TelegramUI/CalculatingCacheSizeItem.swift new file mode 100644 index 0000000000..a8c11152fb --- /dev/null +++ b/TelegramUI/CalculatingCacheSizeItem.swift @@ -0,0 +1,209 @@ +import Foundation +import Display +import AsyncDisplayKit +import SwiftSignalKit + +class CalculatingCacheSizeItem: ListViewItem, ItemListItem { + let theme: PresentationTheme + let title: String + let sectionId: ItemListSectionId + let style: ItemListStyle + + init(theme: PresentationTheme, title: String, sectionId: ItemListSectionId, style: ItemListStyle) { + self.theme = theme + self.title = title + self.sectionId = sectionId + self.style = style + } + + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + async { + let node = CalculatingCacheSizeItemNode() + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + completion(node, { + return (nil, { apply() }) + }) + } + } + + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + if let node = node as? CalculatingCacheSizeItemNode { + Queue.mainQueue().async { + let makeLayout = node.asyncLayout() + + async { + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + Queue.mainQueue().async { + completion(layout, { + apply() + }) + } + } + } + } + } +} + +private let titleFont = Font.regular(14.0) + +class CalculatingCacheSizeItemNode: ListViewItemNode { + private let backgroundNode: ASDisplayNode + private let topStripeNode: ASDisplayNode + private let bottomStripeNode: ASDisplayNode + + private var activityIndicator: ActivityIndicator? + private let titleNode: TextNode + + private var item: CalculatingCacheSizeItem? + + var tag: Any? { + return self.item?.tag + } + + init() { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.backgroundColor = .white + + self.topStripeNode = ASDisplayNode() + self.topStripeNode.isLayerBacked = true + + self.bottomStripeNode = ASDisplayNode() + self.bottomStripeNode.isLayerBacked = true + + self.titleNode = TextNode() + self.titleNode.isLayerBacked = true + self.titleNode.contentMode = .left + self.titleNode.contentsScale = UIScreen.main.scale + + super.init(layerBacked: false, dynamicBounce: false) + + self.addSubnode(self.titleNode) + } + + func asyncLayout() -> (_ item: CalculatingCacheSizeItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + + let currentItem = self.item + + return { item, params, neighbors in + var updatedTheme: PresentationTheme? + + if currentItem?.theme !== item.theme { + updatedTheme = item.theme + } + + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) + + let contentSize: CGSize + let insets: UIEdgeInsets + let separatorHeight = UIScreenPixel + + let itemBackgroundColor: UIColor + let itemSeparatorColor: UIColor + switch item.style { + case .plain: + itemBackgroundColor = item.theme.list.plainBackgroundColor + itemSeparatorColor = item.theme.list.itemPlainSeparatorColor + contentSize = CGSize(width: params.width, height: titleLayout.size.height + 22.0 + 24.0) + insets = itemListNeighborsPlainInsets(neighbors) + case .blocks: + itemBackgroundColor = item.theme.list.itemBlocksBackgroundColor + itemSeparatorColor = item.theme.list.itemBlocksSeparatorColor + contentSize = CGSize(width: params.width, height: titleLayout.size.height + 22.0 + 24.0) + insets = itemListNeighborsGroupedInsets(neighbors) + } + + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + + return (layout, { [weak self] in + if let strongSelf = self { + strongSelf.item = item + + let activityIndicator: ActivityIndicator + if let current = strongSelf.activityIndicator { + activityIndicator = current + } else { + activityIndicator = ActivityIndicator(type: .custom(item.theme.list.itemAccentColor, 20.0), speed: ActivityIndicatorSpeed.slow) + strongSelf.addSubnode(activityIndicator) + } + + if let _ = updatedTheme { + strongSelf.topStripeNode.backgroundColor = itemSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor + strongSelf.backgroundNode.backgroundColor = itemBackgroundColor + activityIndicator.type = .custom(item.theme.list.itemAccentColor, 20.0) + } + + let _ = titleApply() + + let leftInset: CGFloat + + switch item.style { + case .plain: + leftInset = 35.0 + params.leftInset + + if strongSelf.backgroundNode.supernode != nil { + strongSelf.backgroundNode.removeFromSupernode() + } + if strongSelf.topStripeNode.supernode != nil { + strongSelf.topStripeNode.removeFromSupernode() + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0) + } + + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight)) + case .blocks: + leftInset = 16.0 + params.leftInset + + if strongSelf.backgroundNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) + } + if strongSelf.topStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) + } + switch neighbors.top { + case .sameSection(false): + strongSelf.topStripeNode.isHidden = true + default: + strongSelf.topStripeNode.isHidden = false + } + let bottomStripeInset: CGFloat + let bottomStripeOffset: CGFloat + switch neighbors.bottom { + case .sameSection(false): + bottomStripeInset = 16.0 + params.leftInset + bottomStripeOffset = -separatorHeight + default: + bottomStripeInset = 0.0 + bottomStripeOffset = 0.0 + } + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight)) + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight)) + } + + strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: params.leftInset + floor((params.width - params.leftInset - params.rightInset - titleLayout.size.width) / 2.0), y: 11.0 + 24.0), size: titleLayout.size) + + activityIndicator.frame = CGRect(origin: CGPoint(x: floor((params.width - 20.0) / 2.0), y: 8.0), size: CGSize(width: 20.0, height: 20.0)) + } + }) + } + } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) + } +} diff --git a/TelegramUI/CallControllerKeyPreviewNode.swift b/TelegramUI/CallControllerKeyPreviewNode.swift index 3b419d128a..bc06ecc444 100644 --- a/TelegramUI/CallControllerKeyPreviewNode.swift +++ b/TelegramUI/CallControllerKeyPreviewNode.swift @@ -43,7 +43,7 @@ final class CallControllerKeyPreviewNode: ASDisplayNode { override func didLoad() { super.didLoad() - self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapResture(_:)))) + self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) } func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { @@ -98,7 +98,7 @@ final class CallControllerKeyPreviewNode: ASDisplayNode { }) } - @objc func tapResture(_ recognizer: UITapGestureRecognizer) { + @objc func tapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { self.dismiss() } diff --git a/TelegramUI/CallControllerNode.swift b/TelegramUI/CallControllerNode.swift index a9e1dc6bfa..a95e5824c3 100644 --- a/TelegramUI/CallControllerNode.swift +++ b/TelegramUI/CallControllerNode.swift @@ -57,6 +57,7 @@ final class CallControllerNode: ASDisplayNode { self.containerNode = ASDisplayNode() self.imageNode = TransformImageNode() + self.imageNode.contentAnimations = [.subsequentUpdates] self.dimNode = ASDisplayNode() self.dimNode.isLayerBacked = true self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.4) @@ -131,14 +132,18 @@ final class CallControllerNode: ASDisplayNode { override func didLoad() { super.didLoad() - self.view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))) + let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))) + self.view.addGestureRecognizer(panRecognizer) + + let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))) + self.view.addGestureRecognizer(tapRecognizer) } func updatePeer(peer: Peer) { if !arePeersEqual(self.peer, peer) { self.peer = peer - self.imageNode.setSignal(account: self.account, signal: chatAvatarGalleryPhoto(account: self.account, representations: peer.profileImageRepresentations, autoFetchFullSize: true)) + self.imageNode.setSignal(chatAvatarGalleryPhoto(account: self.account, representations: peer.profileImageRepresentations, autoFetchFullSize: true)) self.statusNode.title = peer.displayTitle @@ -252,13 +257,15 @@ final class CallControllerNode: ASDisplayNode { let apply = self.imageNode.asyncLayout()(arguments) apply() + let navigationOffset: CGFloat = max(20.0, layout.safeInsets.top) + let backSize = self.backButtonNode.measure(CGSize(width: 320.0, height: 100.0)) if let image = self.backButtonArrowNode.image { - transition.updateFrame(node: self.backButtonArrowNode, frame: CGRect(origin: CGPoint(x: 10.0, y: 31.0), size: image.size)) + transition.updateFrame(node: self.backButtonArrowNode, frame: CGRect(origin: CGPoint(x: 10.0, y: navigationOffset + 11.0), size: image.size)) } - transition.updateFrame(node: self.backButtonNode, frame: CGRect(origin: CGPoint(x: 29.0, y: 31.0), size: backSize)) + transition.updateFrame(node: self.backButtonNode, frame: CGRect(origin: CGPoint(x: 29.0, y: navigationOffset + 11.0), size: backSize)) - let statusOffset: CGFloat + var statusOffset: CGFloat if layout.metrics.widthClass == .regular && layout.metrics.heightClass == .regular { if layout.size.height.isEqual(to: 1366.0) { statusOffset = 160.0 @@ -275,6 +282,8 @@ final class CallControllerNode: ASDisplayNode { } } + statusOffset += layout.safeInsets.top + let buttonsHeight: CGFloat = 75.0 let buttonsOffset: CGFloat if layout.size.width.isEqual(to: 320.0) { @@ -294,12 +303,12 @@ final class CallControllerNode: ASDisplayNode { transition.updateFrame(node: self.buttonsNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - (buttonsOffset - 40.0) - buttonsHeight), size: CGSize(width: layout.size.width, height: buttonsHeight))) let keyTextSize = self.keyButtonNode.frame.size - transition.updateFrame(node: self.keyButtonNode, frame: CGRect(origin: CGPoint(x: layout.size.width - keyTextSize.width - 8.0, y: 28.0), size: keyTextSize)) + transition.updateFrame(node: self.keyButtonNode, frame: CGRect(origin: CGPoint(x: layout.size.width - keyTextSize.width - 8.0, y: navigationOffset + 8.0), size: keyTextSize)) } @objc func keyPressed() { if self.keyPreviewNode == nil, let keyText = self.keyTextData?.1, let peer = self.peer { - let keyPreviewNode = CallControllerKeyPreviewNode(keyText: keyText, infoText: self.presentationData.strings.Call_EmojiDescription(peer.compactDisplayTitle).0, dismiss: { [weak self] in + let keyPreviewNode = CallControllerKeyPreviewNode(keyText: keyText, infoText: self.presentationData.strings.Call_EmojiDescription(peer.compactDisplayTitle).0.replacingOccurrences(of: "%%", with: "%"), dismiss: { [weak self] in if let _ = self?.keyPreviewNode { self?.backPressed() } @@ -328,6 +337,14 @@ final class CallControllerNode: ASDisplayNode { } } + @objc func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + if let _ = self.keyPreviewNode { + self.backPressed() + } + } + } + @objc func panGesture(_ recognizer: UIPanGestureRecognizer) { switch recognizer.state { case .changed: diff --git a/TelegramUI/CallControllerStatusNode.swift b/TelegramUI/CallControllerStatusNode.swift index 002d8afb64..fea7234fba 100644 --- a/TelegramUI/CallControllerStatusNode.swift +++ b/TelegramUI/CallControllerStatusNode.swift @@ -114,9 +114,9 @@ final class CallControllerStatusNode: ASDisplayNode { } let spacing: CGFloat = 4.0 - let (titleLayout, titleApply) = TextNode.asyncLayout(self.titleNode)(NSAttributedString(string: self.title, font: nameFont, textColor: .white), nil, 1, .end, CGSize(width: constrainedWidth, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets(top: 2.0, left: 2.0, bottom: 2.0, right: 2.0)) - let (statusMeasureLayout, statusMeasureApply) = TextNode.asyncLayout(self.statusMeasureNode)(NSAttributedString(string: statusMeasureText, font: statusFont, textColor: .white), nil, 1, .end, CGSize(width: constrainedWidth, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets(top: 2.0, left: 2.0, bottom: 2.0, right: 2.0)) - let (statusLayout, statusApply) = TextNode.asyncLayout(self.statusNode)(NSAttributedString(string: statusText, font: statusFont, textColor: .white), nil, 1, .end, CGSize(width: constrainedWidth, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets(top: 2.0, left: 2.0, bottom: 2.0, right: 2.0)) + let (titleLayout, titleApply) = TextNode.asyncLayout(self.titleNode)(TextNodeLayoutArguments(attributedString: NSAttributedString(string: self.title, font: nameFont, textColor: .white), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: constrainedWidth - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 2.0, bottom: 2.0, right: 2.0))) + let (statusMeasureLayout, statusMeasureApply) = TextNode.asyncLayout(self.statusMeasureNode)(TextNodeLayoutArguments(attributedString: NSAttributedString(string: statusMeasureText, font: statusFont, textColor: .white), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: constrainedWidth - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 2.0, bottom: 2.0, right: 2.0))) + let (statusLayout, statusApply) = TextNode.asyncLayout(self.statusNode)(TextNodeLayoutArguments(attributedString: NSAttributedString(string: statusText, font: statusFont, textColor: .white), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: constrainedWidth - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 2.0, bottom: 2.0, right: 2.0))) let _ = titleApply() let _ = statusApply() diff --git a/TelegramUI/CallListCallItem.swift b/TelegramUI/CallListCallItem.swift index 7164d0c326..b1486478fe 100644 --- a/TelegramUI/CallListCallItem.swift +++ b/TelegramUI/CallListCallItem.swift @@ -18,10 +18,53 @@ private func callDurationString(strings: PresentationStrings, duration: Int32) - } } +private func callListNeighbors(item: ListViewItem, topItem: ListViewItem?, bottomItem: ListViewItem?) -> ItemListNeighbors { + let topNeighbor: ItemListNeighbor + if let topItem = topItem { + if let item = item as? ItemListItem, let topItem = topItem as? ItemListItem { + if topItem.sectionId != item.sectionId { + topNeighbor = .otherSection(requestsNoInset: topItem.requestsNoInset) + } else { + topNeighbor = .sameSection(alwaysPlain: topItem.isAlwaysPlain) + } + } else { + if item is CallListCallItem && topItem is CallListCallItem { + topNeighbor = .sameSection(alwaysPlain: false) + } else { + topNeighbor = .otherSection(requestsNoInset: false) + } + } + } else { + topNeighbor = .none + } + + let bottomNeighbor: ItemListNeighbor + if let bottomItem = bottomItem { + if let item = item as? ItemListItem, let bottomItem = bottomItem as? ItemListItem { + if bottomItem.sectionId != item.sectionId { + bottomNeighbor = .otherSection(requestsNoInset: bottomItem.requestsNoInset) + } else { + bottomNeighbor = .sameSection(alwaysPlain: bottomItem.isAlwaysPlain) + } + } else { + if item is CallListCallItem && bottomItem is CallListCallItem { + bottomNeighbor = .sameSection(alwaysPlain: false) + } else { + bottomNeighbor = .otherSection(requestsNoInset: false) + } + } + } else { + bottomNeighbor = .none + } + + return ItemListNeighbors(top: topNeighbor, bottom: bottomNeighbor) +} + class CallListCallItem: ListViewItem { let theme: PresentationTheme let strings: PresentationStrings let account: Account + let style: ItemListStyle let topMessage: Message let messages: [Message] let editing: Bool @@ -32,10 +75,11 @@ class CallListCallItem: ListViewItem { let headerAccessoryItem: ListViewAccessoryItem? let header: ListViewItemHeader? - init(theme: PresentationTheme, strings: PresentationStrings, account: Account, topMessage: Message, messages: [Message], editing: Bool, revealed: Bool, interaction: CallListNodeInteraction) { + init(theme: PresentationTheme, strings: PresentationStrings, account: Account, style: ItemListStyle, topMessage: Message, messages: [Message], editing: Bool, revealed: Bool, interaction: CallListNodeInteraction) { self.theme = theme self.strings = strings self.account = account + self.style = style self.topMessage = topMessage self.messages = messages self.editing = editing @@ -46,12 +90,12 @@ class CallListCallItem: ListViewItem { self.header = nil } - func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { async { let node = CallListCallItemNode() let makeLayout = node.asyncLayout() let (first, last, firstWithHeader) = CallListCallItem.mergeType(item: self, previousItem: previousItem, nextItem: nextItem) - let (nodeLayout, nodeApply) = makeLayout(self, width, first, last, firstWithHeader) + let (nodeLayout, nodeApply) = makeLayout(self, params, first, last, firstWithHeader, callListNeighbors(item: self, topItem: previousItem, bottomItem: nextItem)) node.contentSize = nodeLayout.contentSize node.insets = nodeLayout.insets @@ -63,13 +107,13 @@ class CallListCallItem: ListViewItem { } } - func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { if let node = node as? CallListCallItemNode { Queue.mainQueue().async { let layout = node.asyncLayout() async { let (first, last, firstWithHeader) = CallListCallItem.mergeType(item: self, previousItem: previousItem, nextItem: nextItem) - let (nodeLayout, apply) = layout(self, width, first, last, firstWithHeader) + let (nodeLayout, apply) = layout(self, params, first, last, firstWithHeader, callListNeighbors(item: self, topItem: previousItem, bottomItem: nextItem)) var animated = true if case .None = animation { animated = false @@ -126,7 +170,8 @@ private let avatarFont: UIFont = UIFont(name: "ArialRoundedMTBold", size: 15.0)! class CallListCallItemNode: ItemListRevealOptionsItemNode { private let backgroundNode: ASDisplayNode - private let separatorNode: ASDisplayNode + private let topStripeNode: ASDisplayNode + private let bottomStripeNode: ASDisplayNode private let highlightedBackgroundNode: ASDisplayNode private let avatarNode: AvatarNode @@ -139,14 +184,17 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode { var editableControlNode: ItemListEditableControlNode? private var avatarState: (Account, Peer?)? - private var layoutParams: (CallListCallItem, CGFloat, Bool, Bool, Bool)? + private var layoutParams: (CallListCallItem, ListViewItemLayoutParams, Bool, Bool, Bool)? required init() { self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true - self.separatorNode = ASDisplayNode() - self.separatorNode.isLayerBacked = true + self.topStripeNode = ASDisplayNode() + self.topStripeNode.isLayerBacked = true + + self.bottomStripeNode = ASDisplayNode() + self.bottomStripeNode.isLayerBacked = true self.highlightedBackgroundNode = ASDisplayNode() self.highlightedBackgroundNode.isLayerBacked = true @@ -169,7 +217,6 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode { super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false) self.addSubnode(self.backgroundNode) - self.addSubnode(self.separatorNode) self.addSubnode(self.avatarNode) self.addSubnode(self.typeIconNode) self.addSubnode(self.titleNode) @@ -180,25 +227,29 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode { self.infoButtonNode.addTarget(self, action: #selector(self.infoPressed), forControlEvents: .touchUpInside) } - override func layoutForWidth(_ width: CGFloat, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { + override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { if let (item, _, _, _, _) = self.layoutParams { let (first, last, firstWithHeader) = CallListCallItem.mergeType(item: item, previousItem: previousItem, nextItem: nextItem) - self.layoutParams = (item, width, first, last, firstWithHeader) + self.layoutParams = (item, params, first, last, firstWithHeader) let makeLayout = self.asyncLayout() - let (nodeLayout, nodeApply) = makeLayout(item, width, first, last, firstWithHeader) + let (nodeLayout, nodeApply) = makeLayout(item, params, first, last, firstWithHeader, callListNeighbors(item: item, topItem: previousItem, bottomItem: nextItem)) self.contentSize = nodeLayout.contentSize self.insets = nodeLayout.insets let _ = nodeApply() } } - override func setHighlighted(_ highlighted: Bool, animated: Bool) { - super.setHighlighted(highlighted, animated: animated) + override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { + super.setHighlighted(highlighted, at: point, animated: animated) if highlighted { self.highlightedBackgroundNode.alpha = 1.0 if self.highlightedBackgroundNode.supernode == nil { - self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: self.separatorNode) + if self.backgroundNode.supernode != nil { + self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: self.backgroundNode) + } else { + self.insertSubnode(self.highlightedBackgroundNode, at: 0) + } } } else { if self.highlightedBackgroundNode.supernode != nil { @@ -218,14 +269,14 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode { } } - func asyncLayout() -> (_ item: CallListCallItem, _ width: CGFloat, _ first: Bool, _ last: Bool, _ firstWithHeader: Bool) -> (ListViewItemNodeLayout, () -> (Signal?, (Bool) -> Void)) { + func asyncLayout() -> (_ item: CallListCallItem, _ params: ListViewItemLayoutParams, _ first: Bool, _ last: Bool, _ firstWithHeader: Bool, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> (Signal?, (Bool) -> Void)) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeStatusLayout = TextNode.asyncLayout(self.statusNode) let makeDateLayout = TextNode.asyncLayout(self.dateNode) let editableControlLayout = ItemListEditableControlNode.asyncLayout(self.editableControlNode) let currentItem = self.layoutParams?.0 - return { [weak self] item, width, first, last, firstWithHeader in + return { [weak self] item, params, first, last, firstWithHeader, neighbors in var updatedTheme: PresentationTheme? var updatedInfoIcon = false @@ -238,18 +289,34 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode { let editingOffset: CGFloat var editableControlSizeAndApply: (CGSize, () -> ItemListEditableControlNode)? if item.editing { - let sizeAndApply = editableControlLayout(56.0) + let sizeAndApply = editableControlLayout(56.0, item.theme, false) editableControlSizeAndApply = sizeAndApply editingOffset = sizeAndApply.0.width } else { editingOffset = 0.0 } - var leftInset: CGFloat = 86.0 - let rightInset: CGFloat = 13.0 + var leftInset: CGFloat = 86.0 + params.leftInset + let rightInset: CGFloat = 13.0 + params.rightInset var infoIconRightInset: CGFloat = rightInset - var dateRightInset: CGFloat = 43.0 + let insets: UIEdgeInsets + let separatorHeight = UIScreenPixel + let itemBackgroundColor: UIColor + let itemSeparatorColor: UIColor + + switch item.style { + case .plain: + itemBackgroundColor = item.theme.list.plainBackgroundColor + itemSeparatorColor = item.theme.list.itemPlainSeparatorColor + insets = itemListNeighborsPlainInsets(neighbors) + case .blocks: + itemBackgroundColor = item.theme.list.itemBlocksBackgroundColor + itemSeparatorColor = item.theme.list.itemBlocksSeparatorColor + insets = itemListNeighborsGroupedInsets(neighbors) + } + + var dateRightInset: CGFloat = 43.0 + params.rightInset if item.editing { leftInset += editingOffset dateRightInset += 5.0 @@ -341,19 +408,21 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode { localtime_r(&t, &timeinfo) let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) - let dateText = stringForRelativeTimestamp(strings: item.strings, relativeTimestamp: item.topMessage.timestamp, relativeTo: timestamp) + let dateText = stringForRelativeTimestamp(strings: item.strings, relativeTimestamp: item.topMessage.timestamp, relativeTo: timestamp, timeFormat: .regular) - let (dateLayout, dateApply) = makeDateLayout(NSAttributedString(string: dateText, font: dateFont, textColor: item.theme.list.itemSecondaryTextColor), nil, 1, .end, CGSize(width: max(0.0, width - leftInset - rightInset), height: CGFloat.infinity), .natural, nil, UIEdgeInsets()) + let (dateLayout, dateApply) = makeDateLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: dateText, font: dateFont, textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(0.0, params.width - leftInset - rightInset), height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let (titleLayout, titleApply) = makeTitleLayout(titleAttributedString, nil, 1, .end, CGSize(width: max(0.0, width - leftInset - dateRightInset - dateLayout.size.width - 10.0), height: CGFloat.infinity), .natural, nil, UIEdgeInsets()) + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(0.0, params.width - leftInset - dateRightInset - dateLayout.size.width - 10.0), height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let (statusLayout, statusApply) = makeStatusLayout(statusAttributedString, nil, 1, .end, CGSize(width: max(0.0, width - leftInset - rightInset), height: CGFloat.infinity), .natural, nil, UIEdgeInsets()) + let (statusLayout, statusApply) = makeStatusLayout(TextNodeLayoutArguments(attributedString: statusAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(0.0, params.width - leftInset - rightInset), height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: width, height: 56.0), insets: UIEdgeInsets(top: firstWithHeader ? 29.0 : 0.0, left: 0.0, bottom: 0.0, right: 0.0)) + let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 56.0), insets: UIEdgeInsets(top: firstWithHeader ? 29.0 : 0.0, left: 0.0, bottom: 0.0, right: 0.0)) let outgoingIcon = PresentationResourcesCallList.outgoingIcon(item.theme) let infoIcon = PresentationResourcesCallList.infoButton(item.theme) + let contentSize = nodeLayout.contentSize + return (nodeLayout, { [weak self] in if let strongSelf = self { if let peer = item.topMessage.peers[item.topMessage.id.peerId] { @@ -362,7 +431,7 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode { return (strongSelf.avatarNode.ready, { [weak strongSelf] animated in if let strongSelf = strongSelf { - strongSelf.layoutParams = (item, width, first, last, firstWithHeader) + strongSelf.layoutParams = (item, params, first, last, firstWithHeader) let revealOffset = strongSelf.revealOffset @@ -374,11 +443,54 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode { } if let _ = updatedTheme { - strongSelf.separatorNode.backgroundColor = item.theme.list.itemSeparatorColor - strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBackgroundColor + strongSelf.topStripeNode.backgroundColor = itemSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor + strongSelf.backgroundNode.backgroundColor = itemBackgroundColor strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor } + switch item.style { + case .plain: + if strongSelf.backgroundNode.supernode != nil { + strongSelf.backgroundNode.removeFromSupernode() + } + if strongSelf.topStripeNode.supernode != nil { + strongSelf.topStripeNode.removeFromSupernode() + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0) + } + + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight)) + case .blocks: + if strongSelf.backgroundNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) + } + if strongSelf.topStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) + } + switch neighbors.top { + case .sameSection(false): + strongSelf.topStripeNode.isHidden = true + default: + strongSelf.topStripeNode.isHidden = false + } + let bottomStripeInset: CGFloat + switch neighbors.bottom { + case .sameSection(false): + bottomStripeInset = leftInset + default: + bottomStripeInset = 0.0 + } + + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: nodeLayout.size.width, height: separatorHeight)) + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: nodeLayout.size.width - bottomStripeInset, height: separatorHeight)) + } + if let editableControlSizeAndApply = editableControlSizeAndApply { if strongSelf.editableControlNode == nil { let editableControlNode = editableControlSizeAndApply.1() @@ -390,9 +502,9 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode { } strongSelf.editableControlNode = editableControlNode strongSelf.addSubnode(editableControlNode) - let editableControlFrame = CGRect(origin: CGPoint(x: revealOffset, y: 0.0), size: editableControlSizeAndApply.0) + let editableControlFrame = CGRect(origin: CGPoint(x: params.leftInset + revealOffset, y: 0.0), size: editableControlSizeAndApply.0) editableControlNode.frame = editableControlFrame - transition.animatePosition(node: editableControlNode, from: CGPoint(x: editableControlFrame.midX - editableControlFrame.size.width, y: editableControlFrame.midY)) + transition.animatePosition(node: editableControlNode, from: CGPoint(x: -editableControlFrame.size.width / 2.0, y: editableControlFrame.midY)) editableControlNode.alpha = 0.0 transition.updateAlpha(node: editableControlNode, alpha: 1.0) } @@ -415,7 +527,7 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode { transition.updateFrame(node: strongSelf.statusNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset, y: 30.0), size: statusLayout.size)) let _ = dateApply() - transition.updateFrame(node: strongSelf.dateNode, frame: CGRect(origin: CGPoint(x: editingOffset + revealOffset + width - dateRightInset - dateLayout.size.width, y: floor((nodeLayout.contentSize.height - dateLayout.size.height) / 2.0) + 2.0), size: dateLayout.size)) + transition.updateFrame(node: strongSelf.dateNode, frame: CGRect(origin: CGPoint(x: editingOffset + revealOffset + params.width - dateRightInset - dateLayout.size.width, y: floor((nodeLayout.contentSize.height - dateLayout.size.height) / 2.0) + 2.0), size: dateLayout.size)) if let outgoingIcon = outgoingIcon { if strongSelf.typeIconNode.image !== outgoingIcon { @@ -429,17 +541,17 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode { if updatedInfoIcon { strongSelf.infoButtonNode.setImage(infoIcon, for: []) } - transition.updateFrame(node: strongSelf.infoButtonNode, frame: CGRect(origin: CGPoint(x: revealOffset + width - infoIconRightInset - infoIcon.size.width, y: floor((nodeLayout.contentSize.height - infoIcon.size.height) / 2.0)), size: infoIcon.size)) + transition.updateFrame(node: strongSelf.infoButtonNode, frame: CGRect(origin: CGPoint(x: revealOffset + params.width - infoIconRightInset - infoIcon.size.width, y: floor((nodeLayout.contentSize.height - infoIcon.size.height) / 2.0)), size: infoIcon.size)) } transition.updateAlpha(node: strongSelf.infoButtonNode, alpha: item.editing ? 0.0 : 1.0) let topHighlightInset: CGFloat = (first || !nodeLayout.insets.top.isZero) ? 0.0 : separatorHeight strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: nodeLayout.contentSize.width, height: nodeLayout.contentSize.height)) strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -nodeLayout.insets.top - topHighlightInset), size: CGSize(width: nodeLayout.size.width, height: nodeLayout.size.height + topHighlightInset)) - transition.updateFrame(node: strongSelf.separatorNode, frame: CGRect(origin: CGPoint(x: leftInset, y: nodeLayout.contentSize.height - separatorHeight), size: CGSize(width: max(0.0, nodeLayout.size.width - 65.0), height: separatorHeight))) - strongSelf.separatorNode.isHidden = last - strongSelf.setRevealOptions([ItemListRevealOption(key: 0, title: item.strings.Common_Delete, icon: nil, color: UIColor(rgb: 0xff3824))]) + strongSelf.updateLayout(size: nodeLayout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) + + strongSelf.setRevealOptions([ItemListRevealOption(key: 0, title: item.strings.Common_Delete, icon: nil, color: item.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.theme.list.itemDisclosureActions.destructive.foregroundColor)]) } }) } else { @@ -491,24 +603,24 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode { override func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { super.updateRevealOffset(offset: offset, transition: transition) - if let item = self.layoutParams?.0 { + if let (item, params, _, _, _) = self.layoutParams { let revealOffset = offset let editingOffset: CGFloat if let editableControlNode = self.editableControlNode { editingOffset = editableControlNode.bounds.size.width var editableControlFrame = editableControlNode.frame - editableControlFrame.origin.x = offset + editableControlFrame.origin.x = params.leftInset + offset transition.updateFrame(node: editableControlNode, frame: editableControlFrame) } else { editingOffset = 0.0 } - let leftInset: CGFloat = 86.0 + editingOffset - let rightInset: CGFloat = 13.0 + let leftInset: CGFloat = 86.0 + params.leftInset + editingOffset + let rightInset: CGFloat = 13.0 + params.rightInset var infoIconRightInset: CGFloat = rightInset - var dateRightInset: CGFloat = 43.0 + var dateRightInset: CGFloat = 43.0 + params.rightInset if item.editing { dateRightInset += 5.0 infoIconRightInset -= 36.0 diff --git a/TelegramUI/CallListController.swift b/TelegramUI/CallListController.swift index 0e67df6f30..86fa293a6c 100644 --- a/TelegramUI/CallListController.swift +++ b/TelegramUI/CallListController.swift @@ -5,6 +5,11 @@ import Postbox import TelegramCore import SwiftSignalKit +public enum CallListControllerMode { + case tab + case navigation +} + public final class CallListController: ViewController { private var controllerNode: CallListControllerNode { return self.displayNode as! CallListControllerNode @@ -16,6 +21,7 @@ public final class CallListController: ViewController { } private let account: Account + private let mode: CallListControllerMode private var presentationData: PresentationData private var presentationDataDisposable: Disposable? @@ -27,8 +33,9 @@ public final class CallListController: ViewController { private let createActionDisposable = MetaDisposable() - public init(account: Account) { + public init(account: Account, mode: CallListControllerMode) { self.account = account + self.mode = mode self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } self.segmentedTitleView = ItemListControllerSegmentedTitleView(segments: [self.presentationData.strings.Calls_All, self.presentationData.strings.Calls_Missed], index: 0, color: self.presentationData.theme.rootController.navigationBar.accentTextColor) @@ -37,11 +44,13 @@ public final class CallListController: ViewController { self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style - self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationCallIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.callPressed)) + if case .tab = self.mode { + self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationCallIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.callPressed)) - self.tabBarItem.title = self.presentationData.strings.Calls_TabTitle - self.tabBarItem.image = UIImage(bundleImageName: "Chat List/Tabs/IconCalls") - self.tabBarItem.selectedImage = UIImage(bundleImageName: "Chat List/Tabs/IconCallsSelected") + self.tabBarItem.title = self.presentationData.strings.Calls_TabTitle + self.tabBarItem.image = UIImage(bundleImageName: "Chat List/Tabs/IconCalls") + self.tabBarItem.selectedImage = UIImage(bundleImageName: "Chat List/Tabs/IconCalls") + } self.segmentedTitleView.indexUpdated = { [weak self] index in if let strongSelf = self { @@ -66,6 +75,8 @@ public final class CallListController: ViewController { self.scrollToTop = { [weak self] in self?.controllerNode.scrollToLatest() } + + self.navigationItem.titleView = self.segmentedTitleView } required public init(coder aDecoder: NSCoder) { @@ -81,19 +92,27 @@ public final class CallListController: ViewController { self.segmentedTitleView.segments = [self.presentationData.strings.Calls_All, self.presentationData.strings.Calls_Missed] self.segmentedTitleView.color = self.presentationData.theme.rootController.navigationBar.accentTextColor - if let isEmpty = self.isEmpty, isEmpty { - self.navigationItem.title = self.presentationData.strings.Calls_TabTitle - } else { - if self.editingMode { - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)) - } else { - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed)) - } - } - self.tabBarItem.title = self.presentationData.strings.Calls_TabTitle self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) - self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationCallIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.callPressed)) + switch self.mode { + case .tab: + if let isEmpty = self.isEmpty, isEmpty { + } else { + if self.editingMode { + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)) + } else { + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed)) + } + } + + self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationCallIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.callPressed)) + case .navigation: + if self.editingMode { + self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)) + } else { + self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed)) + } + } self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style self.navigationBar?.updateTheme(NavigationBarTheme(rootControllerTheme: self.presentationData.theme)) @@ -105,7 +124,7 @@ public final class CallListController: ViewController { } override public func loadDisplayNode() { - self.displayNode = CallListControllerNode(account: self.account, presentationData: self.presentationData, call: { [weak self] peerId in + self.displayNode = CallListControllerNode(account: self.account, mode: self.mode, presentationData: self.presentationData, call: { [weak self] peerId in if let strongSelf = self { strongSelf.call(peerId) } @@ -127,15 +146,26 @@ public final class CallListController: ViewController { strongSelf.isEmpty = empty if empty { - strongSelf.navigationItem.setLeftBarButton(nil, animated: true) - strongSelf.navigationItem.title = strongSelf.presentationData.strings.Calls_TabTitle + switch strongSelf.mode { + case .tab: + strongSelf.navigationItem.setLeftBarButton(nil, animated: true) + case .navigation: + strongSelf.navigationItem.setRightBarButton(nil, animated: true) + } } else { - strongSelf.navigationItem.title = "" - strongSelf.navigationItem.titleView = strongSelf.segmentedTitleView - if strongSelf.editingMode { - strongSelf.navigationItem.leftBarButtonItem = UIBarButtonItem(title: strongSelf.presentationData.strings.Common_Done, style: .done, target: strongSelf, action: #selector(strongSelf.donePressed)) - } else { - strongSelf.navigationItem.leftBarButtonItem = UIBarButtonItem(title: strongSelf.presentationData.strings.Common_Edit, style: .plain, target: strongSelf, action: #selector(strongSelf.editPressed)) + switch strongSelf.mode { + case .tab: + if strongSelf.editingMode { + strongSelf.navigationItem.leftBarButtonItem = UIBarButtonItem(title: strongSelf.presentationData.strings.Common_Done, style: .done, target: strongSelf, action: #selector(strongSelf.donePressed)) + } else { + strongSelf.navigationItem.leftBarButtonItem = UIBarButtonItem(title: strongSelf.presentationData.strings.Common_Edit, style: .plain, target: strongSelf, action: #selector(strongSelf.editPressed)) + } + case .navigation: + if strongSelf.editingMode { + strongSelf.navigationItem.rightBarButtonItem = UIBarButtonItem(title: strongSelf.presentationData.strings.Common_Done, style: .done, target: strongSelf, action: #selector(strongSelf.donePressed)) + } else { + strongSelf.navigationItem.rightBarButtonItem = UIBarButtonItem(title: strongSelf.presentationData.strings.Common_Edit, style: .plain, target: strongSelf, action: #selector(strongSelf.editPressed)) + } } } } @@ -182,7 +212,12 @@ public final class CallListController: ViewController { @objc func editPressed() { self.editingMode = true - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)) + switch self.mode { + case .tab: + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)) + case .navigation: + self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)) + } self.controllerNode.updateState { state in return state.withUpdatedEditing(true) @@ -191,7 +226,12 @@ public final class CallListController: ViewController { @objc func donePressed() { self.editingMode = false - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed)) + switch self.mode { + case .tab: + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed)) + case .navigation: + self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed)) + } self.controllerNode.updateState { state in return state.withUpdatedEditing(false) diff --git a/TelegramUI/CallListControllerNode.swift b/TelegramUI/CallListControllerNode.swift index e042abc707..637c2c3835 100644 --- a/TelegramUI/CallListControllerNode.swift +++ b/TelegramUI/CallListControllerNode.swift @@ -54,12 +54,14 @@ final class CallListNodeInteraction { let call: (PeerId) -> Void let openInfo: (PeerId) -> Void let delete: ([MessageId]) -> Void + let updateShowCallsTab: (Bool) -> Void - init(setMessageIdWithRevealedOptions: @escaping (MessageId?, MessageId?) -> Void, call: @escaping (PeerId) -> Void, openInfo: @escaping (PeerId) -> Void, delete: @escaping ([MessageId]) -> Void) { + init(setMessageIdWithRevealedOptions: @escaping (MessageId?, MessageId?) -> Void, call: @escaping (PeerId) -> Void, openInfo: @escaping (PeerId) -> Void, delete: @escaping ([MessageId]) -> Void, updateShowCallsTab: @escaping (Bool) -> Void) { self.setMessageIdWithRevealedOptions = setMessageIdWithRevealedOptions self.call = call self.openInfo = openInfo self.delete = delete + self.updateShowCallsTab = updateShowCallsTab } } @@ -98,30 +100,42 @@ struct CallListNodeState: Equatable { } } -private func mappedInsertEntries(account: Account, nodeInteraction: CallListNodeInteraction, entries: [CallListNodeViewTransitionInsertEntry]) -> [ListViewInsertItem] { +private func mappedInsertEntries(account: Account, showSettings: Bool, nodeInteraction: CallListNodeInteraction, entries: [CallListNodeViewTransitionInsertEntry]) -> [ListViewInsertItem] { return entries.map { entry -> ListViewInsertItem in switch entry.entry { + case let .displayTab(theme, text, value): + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ItemListSwitchItem(theme: theme, title: text, value: value, enabled: true, sectionId: 0, style: .blocks, updated: { value in + nodeInteraction.updateShowCallsTab(value) + }), directionHint: entry.directionHint) + case let .displayTabInfo(theme, text): + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ItemListTextItem(theme: theme, text: .plain(text), sectionId: 0), directionHint: entry.directionHint) case let .messageEntry(topMessage, messages, theme, strings, editing, hasActiveRevealControls): - return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: CallListCallItem(theme: theme, strings: strings, account: account, topMessage: topMessage, messages: messages, editing: editing, revealed: hasActiveRevealControls, interaction: nodeInteraction), directionHint: entry.directionHint) - case let .holeEntry(theme): - return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListHoleItem(), directionHint: entry.directionHint) + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: CallListCallItem(theme: theme, strings: strings, account: account, style: showSettings ? .blocks : .plain, topMessage: topMessage, messages: messages, editing: editing, revealed: hasActiveRevealControls, interaction: nodeInteraction), directionHint: entry.directionHint) + case let .holeEntry(_, theme): + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListHoleItem(theme: theme), directionHint: entry.directionHint) } } } -private func mappedUpdateEntries(account: Account, nodeInteraction: CallListNodeInteraction, entries: [CallListNodeViewTransitionUpdateEntry]) -> [ListViewUpdateItem] { +private func mappedUpdateEntries(account: Account, showSettings: Bool, nodeInteraction: CallListNodeInteraction, entries: [CallListNodeViewTransitionUpdateEntry]) -> [ListViewUpdateItem] { return entries.map { entry -> ListViewUpdateItem in switch entry.entry { + case let .displayTab(theme, text, value): + return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ItemListSwitchItem(theme: theme, title: text, value: value, enabled: true, sectionId: 0, style: .blocks, updated: { value in + nodeInteraction.updateShowCallsTab(value) + }), directionHint: entry.directionHint) + case let .displayTabInfo(theme, text): + return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ItemListTextItem(theme: theme, text: .plain(text), sectionId: 0), directionHint: entry.directionHint) case let .messageEntry(topMessage, messages, theme, strings, editing, hasActiveRevealControls): - return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: CallListCallItem(theme: theme, strings: strings, account: account, topMessage: topMessage, messages: messages, editing: editing, revealed: hasActiveRevealControls, interaction: nodeInteraction), directionHint: entry.directionHint) - case let .holeEntry(theme): - return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListHoleItem(), directionHint: entry.directionHint) + return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: CallListCallItem(theme: theme, strings: strings, account: account, style: showSettings ? .blocks : .plain, topMessage: topMessage, messages: messages, editing: editing, revealed: hasActiveRevealControls, interaction: nodeInteraction), directionHint: entry.directionHint) + case let .holeEntry(_, theme): + return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListHoleItem(theme: theme), directionHint: entry.directionHint) } } } -private func mappedCallListNodeViewListTransition(account: Account, nodeInteraction: CallListNodeInteraction, transition: CallListNodeViewTransition) -> CallListNodeListViewTransition { - return CallListNodeListViewTransition(callListView: transition.callListView, deleteItems: transition.deleteItems, insertItems: mappedInsertEntries(account: account, nodeInteraction: nodeInteraction, entries: transition.insertEntries), updateItems: mappedUpdateEntries(account: account, nodeInteraction: nodeInteraction, entries: transition.updateEntries), options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange) +private func mappedCallListNodeViewListTransition(account: Account, showSettings: Bool, nodeInteraction: CallListNodeInteraction, transition: CallListNodeViewTransition) -> CallListNodeListViewTransition { + return CallListNodeListViewTransition(callListView: transition.callListView, deleteItems: transition.deleteItems, insertItems: mappedInsertEntries(account: account, showSettings: showSettings, nodeInteraction: nodeInteraction, entries: transition.insertEntries), updateItems: mappedUpdateEntries(account: account, showSettings: showSettings, nodeInteraction: nodeInteraction, entries: transition.updateEntries), options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange) } private final class CallListOpaqueTransactionState { @@ -134,6 +148,7 @@ private final class CallListOpaqueTransactionState { final class CallListControllerNode: ASDisplayNode { private let account: Account + private let mode: CallListControllerMode private var presentationData: PresentationData private var containerLayout: (ContainerViewLayout, CGFloat)? @@ -167,8 +182,9 @@ final class CallListControllerNode: ASDisplayNode { private let openInfo: (PeerId) -> Void private let emptyStateUpdated: (Bool) -> Void - init(account: Account, presentationData: PresentationData, call: @escaping (PeerId) -> Void, openInfo: @escaping (PeerId) -> Void, emptyStateUpdated: @escaping (Bool) -> Void) { + init(account: Account, mode: CallListControllerMode, presentationData: PresentationData, call: @escaping (PeerId) -> Void, openInfo: @escaping (PeerId) -> Void, emptyStateUpdated: @escaping (Bool) -> Void) { self.account = account + self.mode = mode self.presentationData = presentationData self.call = call self.openInfo = openInfo @@ -187,7 +203,14 @@ final class CallListControllerNode: ASDisplayNode { self.addSubnode(self.listNode) - self.backgroundColor = presentationData.theme.chatList.backgroundColor + switch self.mode { + case .tab: + self.backgroundColor = presentationData.theme.chatList.backgroundColor + self.listNode.backgroundColor = presentationData.theme.chatList.backgroundColor + case .navigation: + self.backgroundColor = presentationData.theme.list.blocksBackgroundColor + self.listNode.backgroundColor = presentationData.theme.list.blocksBackgroundColor + } let nodeInteraction = CallListNodeInteraction(setMessageIdWithRevealedOptions: { [weak self] messageId, fromMessageId in if let strongSelf = self { @@ -207,6 +230,12 @@ final class CallListControllerNode: ASDisplayNode { if let strongSelf = self { let _ = deleteMessagesInteractively(postbox: strongSelf.account.postbox, messageIds: messageIds, type: .forLocalPeer).start() } + }, updateShowCallsTab: { [weak self] value in + if let strongSelf = self { + let _ = updateCallListSettingsInteractively(postbox: strongSelf.account.postbox, { + $0.withUpdatedShowTab(value) + }).start() + } }) let viewProcessingQueue = self.viewProcessingQueue @@ -219,8 +248,25 @@ final class CallListControllerNode: ASDisplayNode { let previousView = Atomic(value: nil) - let callListNodeViewTransition = combineLatest(callListViewUpdate, self.statePromise.get()) |> mapToQueue { (update, state) -> Signal in - let processedView = CallListNodeView(originalView: update.view, filteredEntries: callListNodeEntriesForView(update.view, state: state)) + let showSettings: Bool + switch mode { + case .tab: + showSettings = false + case .navigation: + showSettings = true + } + + let showCallsTab = account.postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.callListSettings]) + |> map { view -> Bool in + var value = true + if let settings = view.values[ApplicationSpecificPreferencesKeys.callListSettings] as? CallListSettings { + value = settings.showTab + } + return value + } + + let callListNodeViewTransition = combineLatest(callListViewUpdate, self.statePromise.get(), showCallsTab) |> mapToQueue { (update, state, showCallsTab) -> Signal in + let processedView = CallListNodeView(originalView: update.view, filteredEntries: callListNodeEntriesForView(update.view, state: state, showSettings: showSettings, showCallsTab: showCallsTab)) let previous = previousView.swap(processedView) let reason: CallListNodeViewTransitionReason @@ -243,21 +289,27 @@ final class CallListControllerNode: ASDisplayNode { prepareOnMainQueue = true } } else { - switch update.type { - case .InitialUnread: - reason = .initial - prepareOnMainQueue = true - case .Generic: - reason = .interactiveChanges - case .UpdateVisible: - reason = .reload - case .FillHole: - reason = .reload + if previous?.originalView === update.view { + reason = .interactiveChanges + } else { + switch update.type { + case .Initial: + reason = .initial + prepareOnMainQueue = true + case .Generic: + reason = .interactiveChanges + case .UpdateVisible: + reason = .reload + case .Reload: + reason = .reload + case .ReloadAnimated: + reason = .reloadAnimated + } } } return preparedCallListNodeViewTransition(from: previous, to: processedView, reason: reason, account: account, scrollPosition: update.scrollPosition) - |> map({ mappedCallListNodeViewListTransition(account: account, nodeInteraction: nodeInteraction, transition: $0) }) + |> map({ mappedCallListNodeViewListTransition(account: account, showSettings: showSettings, nodeInteraction: nodeInteraction, transition: $0) }) |> runOn(prepareOnMainQueue ? Queue.mainQueue() : viewProcessingQueue) } @@ -295,7 +347,14 @@ final class CallListControllerNode: ASDisplayNode { func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { if theme !== self.currentState.theme || strings !== self.currentState.strings { - self.backgroundColor = theme.chatList.backgroundColor + switch self.mode { + case .tab: + self.backgroundColor = theme.chatList.backgroundColor + self.listNode.backgroundColor = theme.chatList.backgroundColor + case .navigation: + self.backgroundColor = theme.list.blocksBackgroundColor + self.listNode.backgroundColor = theme.list.blocksBackgroundColor + } self.updateState { return $0.withUpdatedPresentationData(theme: theme, strings: strings) @@ -313,8 +372,14 @@ final class CallListControllerNode: ASDisplayNode { func updateType(_ type: CallListViewType) { if type != self.currentLocationAndType.type { - if let view = self.callListView?.originalView, !view.entries.isEmpty { - self.currentLocationAndType = CallListNodeLocationAndType(location: .changeType(index: view.entries[view.entries.count - 1].highestIndex), type: type) + if let view = self.callListView?.originalView { + var index: MessageIndex + if !view.entries.isEmpty { + index = view.entries[view.entries.count - 1].highestIndex + } else { + index = MessageIndex.absoluteUpperBound() + } + self.currentLocationAndType = CallListNodeLocationAndType(location: .changeType(index: index), type: type) self.callListLocationAndType.set(self.currentLocationAndType) } } @@ -385,6 +450,8 @@ final class CallListControllerNode: ASDisplayNode { var insets = layout.insets(options: [.input]) insets.top += max(navigationBarHeight, layout.insets(options: [.statusBar]).top) + insets.left += layout.safeInsets.left + insets.right += layout.safeInsets.right self.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) self.listNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0) diff --git a/TelegramUI/CallListNodeEntries.swift b/TelegramUI/CallListNodeEntries.swift index 6b0738882c..149e26f058 100644 --- a/TelegramUI/CallListNodeEntries.swift +++ b/TelegramUI/CallListNodeEntries.swift @@ -3,11 +3,14 @@ import Postbox import TelegramCore enum CallListNodeEntryId: Hashable { + case setting(Int32) case hole(MessageIndex) case message(MessageIndex) var hashValue: Int { switch self { + case let .setting(value): + return value.hashValue case let .hole(index): return index.hashValue case let .message(index): @@ -15,12 +18,14 @@ enum CallListNodeEntryId: Hashable { } } - static func <(lhs: CallListNodeEntryId, rhs: CallListNodeEntryId) -> Bool { - return lhs.hashValue < rhs.hashValue - } - static func ==(lhs: CallListNodeEntryId, rhs: CallListNodeEntryId) -> Bool { switch lhs { + case let .setting(value): + if case .setting(value) = rhs { + return true + } else { + return false + } case let .hole(index): if case .hole(index) = rhs { return true @@ -48,11 +53,17 @@ private func areMessagesEqual(_ lhsMessage: Message, _ rhsMessage: Message) -> B } enum CallListNodeEntry: Comparable, Identifiable { + case displayTab(PresentationTheme, String, Bool) + case displayTabInfo(PresentationTheme, String) case messageEntry(topMessage: Message, messages: [Message], theme: PresentationTheme, strings: PresentationStrings, editing: Bool, hasActiveRevealControls: Bool) case holeEntry(index: MessageIndex, theme: PresentationTheme) var index: MessageIndex { switch self { + case .displayTab: + return MessageIndex.absoluteUpperBound() + case .displayTabInfo: + return MessageIndex.absoluteUpperBound().predecessor() case let .messageEntry(message, _, _, _, _, _): return MessageIndex(message) case let .holeEntry(index, _): @@ -62,6 +73,10 @@ enum CallListNodeEntry: Comparable, Identifiable { var stableId: CallListNodeEntryId { switch self { + case .displayTab: + return .setting(0) + case .displayTabInfo: + return .setting(1) case let .messageEntry(message, _, _, _, _, _): return .message(MessageIndex(message)) case let .holeEntry(index, _): @@ -70,11 +85,53 @@ enum CallListNodeEntry: Comparable, Identifiable { } static func <(lhs: CallListNodeEntry, rhs: CallListNodeEntry) -> Bool { - return lhs.index < rhs.index + switch lhs { + case .displayTab: + return false + case .displayTabInfo: + switch rhs { + case .displayTab: + return true + default: + return false + } + case let .holeEntry(lhsIndex, _): + switch rhs { + case let .holeEntry(rhsIndex, _): + return lhsIndex < rhsIndex + case let .messageEntry(topMessage, _, _, _, _, _): + return lhsIndex < MessageIndex(topMessage) + default: + return true + } + case let .messageEntry(lhsTopMessage, _, _, _, _, _): + let lhsIndex = MessageIndex(lhsTopMessage) + switch rhs { + case let .holeEntry(rhsIndex, _): + return lhsIndex < rhsIndex + case let .messageEntry(topMessage, _, _, _, _, _): + return lhsIndex < MessageIndex(topMessage) + default: + return true + } + + } } static func ==(lhs: CallListNodeEntry, rhs: CallListNodeEntry) -> Bool { switch lhs { + case let .displayTab(lhsTheme, lhsText, lhsValue): + if case let .displayTab(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + case let .displayTabInfo(lhsTheme, lhsText): + if case let .displayTabInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } case let .messageEntry(lhsMessage, lhsMessages, lhsTheme, lhsStrings, lhsEditing, lhsHasRevealControls): if case let .messageEntry(rhsMessage, rhsMessages, rhsTheme, rhsStrings, rhsEditing, rhsHasRevealControls) = rhs { if lhsTheme !== rhsTheme { @@ -114,7 +171,7 @@ enum CallListNodeEntry: Comparable, Identifiable { } } -func callListNodeEntriesForView(_ view: CallListView, state: CallListNodeState) -> [CallListNodeEntry] { +func callListNodeEntriesForView(_ view: CallListView, state: CallListNodeState, showSettings: Bool, showCallsTab: Bool) -> [CallListNodeEntry] { var result: [CallListNodeEntry] = [] for entry in view.entries { switch entry { @@ -124,5 +181,9 @@ func callListNodeEntriesForView(_ view: CallListView, state: CallListNodeState) result.append(.holeEntry(index: index, theme: state.theme)) } } + if showSettings { + result.append(.displayTabInfo(state.theme, state.strings.Calls_CallTabDescription)) + result.append(.displayTab(state.theme, state.strings.Calls_CallTabTitle, showCallsTab)) + } return result } diff --git a/TelegramUI/CallListNodeLocation.swift b/TelegramUI/CallListNodeLocation.swift index 0001c6261e..636f9583e7 100644 --- a/TelegramUI/CallListNodeLocation.swift +++ b/TelegramUI/CallListNodeLocation.swift @@ -34,9 +34,17 @@ struct CallListNodeLocationAndType: Equatable { } } +enum CallListNodeViewUpdateType { + case Initial + case Generic + case Reload + case ReloadAnimated + case UpdateVisible +} + struct CallListNodeViewUpdate { let view: CallListView - let type: ViewUpdateType + let type: CallListNodeViewUpdateType let scrollPosition: CallListNodeViewScrollPosition? } @@ -48,14 +56,12 @@ func callListViewForLocationAndType(locationAndType: CallListNodeLocationAndType } case let .changeType(index): return account.viewTracker.callListView(type: locationAndType.type, index: index, count: 120) |> map { view -> CallListNodeViewUpdate in - let genericType: ViewUpdateType - genericType = .Generic - return CallListNodeViewUpdate(view: view, type: genericType, scrollPosition: nil) + return CallListNodeViewUpdate(view: view, type: .ReloadAnimated, scrollPosition: nil) } case let .navigation(index): var first = true return account.viewTracker.callListView(type: locationAndType.type, index: index, count: 120) |> map { view -> CallListNodeViewUpdate in - let genericType: ViewUpdateType + let genericType: CallListNodeViewUpdateType if first { first = false genericType = .UpdateVisible @@ -69,7 +75,7 @@ func callListViewForLocationAndType(locationAndType: CallListNodeLocationAndType let callScrollPosition: CallListNodeViewScrollPosition = .index(index: index, position: scrollPosition, directionHint: directionHint, animated: animated) var first = true return account.viewTracker.callListView(type: locationAndType.type, index: index, count: 120) |> map { view -> CallListNodeViewUpdate in - let genericType: ViewUpdateType + let genericType: CallListNodeViewUpdateType let scrollPosition: CallListNodeViewScrollPosition? = first ? callScrollPosition : nil if first { first = false diff --git a/TelegramUI/CallListSettings.swift b/TelegramUI/CallListSettings.swift new file mode 100644 index 0000000000..e59bba2368 --- /dev/null +++ b/TelegramUI/CallListSettings.swift @@ -0,0 +1,53 @@ +import Foundation +import Postbox +import SwiftSignalKit + +public struct CallListSettings: PreferencesEntry, Equatable { + public let showTab: Bool + + public static var defaultSettings: CallListSettings { + return CallListSettings(showTab: true) + } + + public init(showTab: Bool) { + self.showTab = showTab + } + + public init(decoder: PostboxDecoder) { + self.showTab = decoder.decodeInt32ForKey("showTab", orElse: 0) != 0 + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeInt32(self.showTab ? 1 : 0, forKey: "showTab") + } + + public func isEqual(to: PreferencesEntry) -> Bool { + if let to = to as? CallListSettings { + return self == to + } else { + return false + } + } + + public static func ==(lhs: CallListSettings, rhs: CallListSettings) -> Bool { + return lhs.showTab == rhs.showTab + } + + func withUpdatedShowTab(_ showTab: Bool) -> CallListSettings { + return CallListSettings(showTab: showTab) + } +} + +func updateCallListSettingsInteractively(postbox: Postbox, _ f: @escaping (CallListSettings) -> CallListSettings) -> Signal { + return postbox.modify { modifier -> Void in + modifier.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.callListSettings, { entry in + let currentSettings: CallListSettings + if let entry = entry as? CallListSettings { + currentSettings = entry + } else { + currentSettings = CallListSettings.defaultSettings + } + return f(currentSettings) + }) + } +} diff --git a/TelegramUI/CallListViewTransition.swift b/TelegramUI/CallListViewTransition.swift index 15fe618ef8..15d4532ac5 100644 --- a/TelegramUI/CallListViewTransition.swift +++ b/TelegramUI/CallListViewTransition.swift @@ -14,6 +14,7 @@ enum CallListNodeViewTransitionReason { case interactiveChanges case holeChanges(filledHoleDirections: [MessageIndex: HoleFillDirection], removeHoleDirections: [MessageIndex: HoleFillDirection]) case reload + case reloadAnimated } struct CallListNodeViewTransitionInsertEntry { @@ -68,45 +69,49 @@ func preparedCallListNodeViewTransition(from fromView: CallListNodeView?, to toV var scrollToItem: ListViewScrollToItem? switch reason { - case .initial: - let _ = options.insert(.LowLatency) - let _ = options.insert(.Synchronous) - case .interactiveChanges: - let _ = options.insert(.AnimateAlpha) - let _ = options.insert(.AnimateInsertion) - - for (index, _, _) in indicesAndItems.sorted(by: { $0.0 > $1.0 }) { - let adjustedIndex = updatedCount - 1 - index - if adjustedIndex == maxAnimatedInsertionIndex + 1 { - maxAnimatedInsertionIndex += 1 - } - } - case .reload: - break - case let .holeChanges(filledHoleDirections, removeHoleDirections): - if let (_, removeDirection) = removeHoleDirections.first { - switch removeDirection { - case .LowerToUpper: - var holeIndex: MessageIndex? - for (index, _) in filledHoleDirections { - if holeIndex == nil || index < holeIndex! { - holeIndex = index - } + case .initial: + let _ = options.insert(.LowLatency) + let _ = options.insert(.Synchronous) + case .interactiveChanges: + let _ = options.insert(.AnimateAlpha) + let _ = options.insert(.AnimateInsertion) + + for (index, _, _) in indicesAndItems.sorted(by: { $0.0 > $1.0 }) { + let adjustedIndex = updatedCount - 1 - index + if adjustedIndex == maxAnimatedInsertionIndex + 1 { + maxAnimatedInsertionIndex += 1 } - - if let holeIndex = holeIndex { - for i in 0 ..< toView.filteredEntries.count { - if toView.filteredEntries[i].index >= holeIndex { - let index = toView.filteredEntries.count - 1 - (i - 1) - stationaryItemRange = (index, Int.max) - break + } + case .reload: + break + case .reloadAnimated: + let _ = options.insert(.LowLatency) + let _ = options.insert(.Synchronous) + let _ = options.insert(.AnimateCrossfade) + case let .holeChanges(filledHoleDirections, removeHoleDirections): + if let (_, removeDirection) = removeHoleDirections.first { + switch removeDirection { + case .LowerToUpper: + var holeIndex: MessageIndex? + for (index, _) in filledHoleDirections { + if holeIndex == nil || index < holeIndex! { + holeIndex = index } } - } - case .UpperToLower: - break - case .AroundIndex: - break + + if let holeIndex = holeIndex { + for i in 0 ..< toView.filteredEntries.count { + if toView.filteredEntries[i].index >= holeIndex { + let index = toView.filteredEntries.count - 1 - (i - 1) + stationaryItemRange = (index, Int.max) + break + } + } + } + case .UpperToLower: + break + case .AroundId, .AroundIndex: + break } } } diff --git a/TelegramUI/ChangePhoneNumberCodeController.swift b/TelegramUI/ChangePhoneNumberCodeController.swift index 4850a20106..d9deb0deb2 100644 --- a/TelegramUI/ChangePhoneNumberCodeController.swift +++ b/TelegramUI/ChangePhoneNumberCodeController.swift @@ -126,13 +126,13 @@ private struct ChangePhoneNumberCodeControllerState: Equatable { } } -private func changePhoneNumberCodeControllerEntries(presentationData: PresentationData, state: ChangePhoneNumberCodeControllerState, codeData: ChangeAccountPhoneNumberData, timeout: Int32?) -> [ChangePhoneNumberCodeEntry] { +private func changePhoneNumberCodeControllerEntries(presentationData: PresentationData, state: ChangePhoneNumberCodeControllerState, codeData: ChangeAccountPhoneNumberData, timeout: Int32?, strings: PresentationStrings, theme: AuthorizationTheme) -> [ChangePhoneNumberCodeEntry] { var entries: [ChangePhoneNumberCodeEntry] = [] entries.append(.codeEntry(presentationData.theme, state.codeText)) - var text = authorizationCurrentOptionText(codeData.type).string + var text = authorizationCurrentOptionText(codeData.type, strings: presentationData.strings, theme: defaultLightAuthorizationTheme).string if let nextType = codeData.nextType { - text += "\n\n" + authorizationNextOptionText(nextType, timeout: timeout).string + text += "\n\n" + authorizationNextOptionText(nextType, timeout: timeout, strings: presentationData.strings, theme: defaultAuthorizationTheme).0.string } entries.append(.codeInfo(presentationData.theme, text)) @@ -277,7 +277,7 @@ func changePhoneNumberCodeController(account: Account, phoneNumber: String, code } let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(formatPhoneNumber(phoneNumber)), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) - let listState = ItemListNodeState(entries: changePhoneNumberCodeControllerEntries(presentationData: presentationData, state: state, codeData: data, timeout: timeout), style: .blocks, focusItemTag: ChangePhoneNumberCodeTag.input, emptyStateItem: nil, animateChanges: false) + let listState = ItemListNodeState(entries: changePhoneNumberCodeControllerEntries(presentationData: presentationData, state: state, codeData: data, timeout: timeout, strings: presentationData.strings, theme: defaultLightAuthorizationTheme), style: .blocks, focusItemTag: ChangePhoneNumberCodeTag.input, emptyStateItem: nil, animateChanges: false) return (controllerState, (listState, arguments)) } |> afterDisposed { diff --git a/TelegramUI/ChangePhoneNumberController.swift b/TelegramUI/ChangePhoneNumberController.swift index 55514996c0..a9a11dd03e 100644 --- a/TelegramUI/ChangePhoneNumberController.swift +++ b/TelegramUI/ChangePhoneNumberController.swift @@ -17,10 +17,10 @@ final class ChangePhoneNumberController: ViewController { var inProgress: Bool = false { didSet { if self.inProgress { - let item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode()) + let item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: self.presentationData.theme.rootController.navigationBar.accentTextColor)) self.navigationItem.rightBarButtonItem = item } else { - self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Next", style: .done, target: self, action: #selector(self.nextPressed)) + self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Next, style: .done, target: self, action: #selector(self.nextPressed)) } self.controllerNode.inProgress = self.inProgress } @@ -33,7 +33,6 @@ final class ChangePhoneNumberController: ViewController { init(account: Account) { self.account = account - self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } super.init(navigationBarTheme: NavigationBarTheme(rootControllerTheme: self.presentationData.theme)) @@ -67,7 +66,7 @@ final class ChangePhoneNumberController: ViewController { self.displayNodeDidLoad() self.controllerNode.selectCountryCode = { [weak self] in if let strongSelf = self { - let controller = AuthorizationSequenceCountrySelectionController() + let controller = AuthorizationSequenceCountrySelectionController(strings: strongSelf.presentationData.strings, theme: defaultLightAuthorizationTheme) controller.completeWithCountryCode = { code, _ in if let strongSelf = self { strongSelf.updateData(countryCode: Int32(code), number: strongSelf.controllerNode.codeAndNumber.1) diff --git a/TelegramUI/ChangePhoneNumberControllerNode.swift b/TelegramUI/ChangePhoneNumberControllerNode.swift index d9644d98a2..2bb333b66b 100644 --- a/TelegramUI/ChangePhoneNumberControllerNode.swift +++ b/TelegramUI/ChangePhoneNumberControllerNode.swift @@ -139,7 +139,7 @@ final class ChangePhoneNumberControllerNode: ASDisplayNode { self.phoneInputNode.countryCodeUpdated = { [weak self] code in if let strongSelf = self { - if let code = Int(code), let countryName = countryCodeToName[code] { + if let code = Int(code), let (coutnryId, countryName) = countryCodeToIdAndName[code] { strongSelf.countryButton.setTitle(countryName, with: Font.regular(17.0), with: .black, for: []) } else { strongSelf.countryButton.setTitle(strongSelf.presentationData.strings.Login_CountryCode, with: Font.regular(17.0), with: .black, for: []) diff --git a/TelegramUI/ChannelAdminController.swift b/TelegramUI/ChannelAdminController.swift index 0eba7d5f96..084c78be24 100644 --- a/TelegramUI/ChannelAdminController.swift +++ b/TelegramUI/ChannelAdminController.swift @@ -172,7 +172,7 @@ private enum ChannelAdminEntry: ItemListNodeEntry { func item(_ arguments: ChannelAdminControllerArguments) -> ListViewItem { switch self { case let .info(theme, strings, peer, presence): - return ItemListAvatarAndNameInfoItem(account: arguments.account, theme: theme, strings: strings, peer: peer, presence: presence, cachedData: nil, state: ItemListAvatarAndNameInfoItemState(), sectionId: self.section, style: .blocks(withTopInset: true), editingNameUpdated: { _ in + return ItemListAvatarAndNameInfoItem(account: arguments.account, theme: theme, strings: strings, mode: .generic, peer: peer, presence: presence, cachedData: nil, state: ItemListAvatarAndNameInfoItemState(), sectionId: self.section, style: .blocks(withTopInset: true), editingNameUpdated: { _ in }, avatarTapped: { }) case let .rightItem(theme, _, text, right, flags, value, enabled): @@ -229,7 +229,7 @@ private func stringForRight(strings: PresentationStrings, right: TelegramChannel } else if right.contains(.canInviteUsers) { return strings.Channel_EditAdmin_PermissionInviteUsers } else if right.contains(.canChangeInviteLink) { - return strings.Channel_EditAdmin_PermissionChangeInviteLink + return "" } else if right.contains(.canPinMessages) { return strings.Channel_EditAdmin_PermissionPinMessages } else if right.contains(.canAddAdmins) { diff --git a/TelegramUI/ChannelAdminsController.swift b/TelegramUI/ChannelAdminsController.swift index f008daeb06..50c1890310 100644 --- a/TelegramUI/ChannelAdminsController.swift +++ b/TelegramUI/ChannelAdminsController.swift @@ -221,7 +221,8 @@ private enum ChannelAdminsEntry: ItemListNodeEntry { baseFlags = .broadcastSpecific } let flags = adminInfo.rights.flags.intersection(baseFlags) - peerText = strings.Channel_Management_LabelRights(Int32(flags.count)) + peerText = "" + //peerText = strings.Channel_Management_LabelRights(Int32(flags.count)) } else { peerText = "" } @@ -229,13 +230,13 @@ private enum ChannelAdminsEntry: ItemListNodeEntry { arguments.openAdmin(participant.participant) } } - return ItemListPeerItem(theme: theme, account: arguments.account, peer: participant.peer, presence: nil, text: .text(peerText), label: .none, editing: editing, switchValue: nil, enabled: enabled, sectionId: self.section, action: action, setPeerIdWithRevealedOptions: { previousId, id in + return ItemListPeerItem(theme: theme, strings: strings, account: arguments.account, peer: participant.peer, presence: nil, text: .text(peerText), label: .none, editing: editing, switchValue: nil, enabled: enabled, sectionId: self.section, action: action, setPeerIdWithRevealedOptions: { previousId, id in arguments.setPeerIdWithRevealedOptions(previousId, id) }, removePeer: { peerId in arguments.removeAdmin(peerId) }) case let .addAdmin(theme, text, editing): - return ItemListPeerActionItem(theme: theme, icon: PresentationResourcesItemList.plusIconImage(theme), title: text, sectionId: self.section, editing: editing, action: { + return ItemListPeerActionItem(theme: theme, icon: PresentationResourcesItemList.addPersonIcon(theme), title: text, sectionId: self.section, editing: editing, action: { arguments.addAdmin() }) case let .adminsInfo(theme, text): @@ -347,10 +348,10 @@ private func channelAdminsControllerEntries(presentationData: PresentationData, switch selectedType { case .everyoneCanAddMembers: selectedTypeValue = presentationData.strings.ChannelMembers_WhoCanAddMembers_AllMembers - infoText = presentationData.strings.ChannelMembers_AllMembersMayInviteOnHelp + infoText = presentationData.strings.ChannelMembers_WhoCanAddMembersAllHelp case .adminsCanAddMembers: selectedTypeValue = presentationData.strings.ChannelMembers_WhoCanAddMembers_Admins - infoText = presentationData.strings.ChannelMembers_AllMembersMayInviteOffHelp + infoText = presentationData.strings.ChannelMembers_WhoCanAddMembersAdminsHelp } entries.append(.administrationType(presentationData.theme, presentationData.strings.ChannelMembers_WhoCanAddMembers, selectedTypeValue)) diff --git a/TelegramUI/ChannelBannedMemberController.swift b/TelegramUI/ChannelBannedMemberController.swift index 719b40aeda..15da51cd01 100644 --- a/TelegramUI/ChannelBannedMemberController.swift +++ b/TelegramUI/ChannelBannedMemberController.swift @@ -172,7 +172,7 @@ private enum ChannelBannedMemberEntry: ItemListNodeEntry { func item(_ arguments: ChannelBannedMemberControllerArguments) -> ListViewItem { switch self { case let .info(theme, strings, peer, presence): - return ItemListAvatarAndNameInfoItem(account: arguments.account, theme: theme, strings: strings, peer: peer, presence: presence, cachedData: nil, state: ItemListAvatarAndNameInfoItemState(), sectionId: self.section, style: .blocks(withTopInset: true), editingNameUpdated: { _ in + return ItemListAvatarAndNameInfoItem(account: arguments.account, theme: theme, strings: strings, mode: .generic, peer: peer, presence: presence, cachedData: nil, state: ItemListAvatarAndNameInfoItemState(), sectionId: self.section, style: .blocks(withTopInset: true), editingNameUpdated: { _ in }, avatarTapped: { }) case let .rightItem(theme, _, text, right, flags, value, enabled): diff --git a/TelegramUI/ChannelBlacklistController.swift b/TelegramUI/ChannelBlacklistController.swift index 084f418e6b..60ce38adaf 100644 --- a/TelegramUI/ChannelBlacklistController.swift +++ b/TelegramUI/ChannelBlacklistController.swift @@ -136,7 +136,7 @@ private enum ChannelBlacklistEntry: ItemListNodeEntry { func item(_ arguments: ChannelBlacklistControllerArguments) -> ListViewItem { switch self { case let .add(theme, text): - return ItemListPeerActionItem(theme: theme, icon: PresentationResourcesItemList.plusIconImage(theme), title: text, sectionId: self.section, editing: false, action: { + return ItemListPeerActionItem(theme: theme, icon: PresentationResourcesItemList.addPersonIcon(theme), title: text, sectionId: self.section, editing: false, action: { arguments.addPeer() }) case let .peerItem(theme, strings, _, participant, editing, enabled): @@ -364,7 +364,7 @@ public func channelBlacklistController(account: Account, peerId: PeerId) -> View var emptyStateItem: ItemListControllerEmptyStateItem? if blacklist == nil { - emptyStateItem = ItemListLoadingIndicatorEmptyStateItem() + emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme) } let previous = previousBlacklist diff --git a/TelegramUI/ChannelInfoController.swift b/TelegramUI/ChannelInfoController.swift index 8aab72f4c1..9b1940d888 100644 --- a/TelegramUI/ChannelInfoController.swift +++ b/TelegramUI/ChannelInfoController.swift @@ -23,8 +23,10 @@ private final class ChannelInfoControllerArguments { let leaveChannel: () -> Void let deleteChannel: () -> Void let displayAddressNameContextMenu: (String) -> Void + let displayAboutContextMenu: (String) -> Void + let aboutLinkAction: (TextLinkItemActionType, TextLinkItem) -> Void - init(account: Account, avatarAndNameInfoContext: ItemListAvatarAndNameInfoItemContext, tapAvatarAction: @escaping () -> Void, changeProfilePhoto: @escaping () -> Void, updateEditingName: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, updateEditingDescriptionText: @escaping (String) -> Void, openChannelTypeSetup: @escaping () -> Void, changeNotificationMuteSettings: @escaping () -> Void, changeNotificationSoundSettings: @escaping () -> Void, openSharedMedia: @escaping () -> Void, openAdmins: @escaping () -> Void, openMembers: @escaping () -> Void, openBanned: @escaping () -> Void, reportChannel: @escaping () -> Void, leaveChannel: @escaping () -> Void, deleteChannel: @escaping () -> Void, displayAddressNameContextMenu: @escaping (String) -> Void) { + init(account: Account, avatarAndNameInfoContext: ItemListAvatarAndNameInfoItemContext, tapAvatarAction: @escaping () -> Void, changeProfilePhoto: @escaping () -> Void, updateEditingName: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, updateEditingDescriptionText: @escaping (String) -> Void, openChannelTypeSetup: @escaping () -> Void, changeNotificationMuteSettings: @escaping () -> Void, changeNotificationSoundSettings: @escaping () -> Void, openSharedMedia: @escaping () -> Void, openAdmins: @escaping () -> Void, openMembers: @escaping () -> Void, openBanned: @escaping () -> Void, reportChannel: @escaping () -> Void, leaveChannel: @escaping () -> Void, deleteChannel: @escaping () -> Void, displayAddressNameContextMenu: @escaping (String) -> Void, displayAboutContextMenu: @escaping (String) -> Void, aboutLinkAction: @escaping (TextLinkItemActionType, TextLinkItem) -> Void) { self.account = account self.avatarAndNameInfoContext = avatarAndNameInfoContext self.tapAvatarAction = tapAvatarAction @@ -42,6 +44,8 @@ private final class ChannelInfoControllerArguments { self.leaveChannel = leaveChannel self.deleteChannel = deleteChannel self.displayAddressNameContextMenu = displayAddressNameContextMenu + self.displayAboutContextMenu = displayAboutContextMenu + self.aboutLinkAction = aboutLinkAction } } @@ -53,11 +57,11 @@ private enum ChannelInfoSection: ItemListSectionId { } private enum ChannelInfoEntryTag { - case addressName + case about } private enum ChannelInfoEntry: ItemListNodeEntry { - case info(PresentationTheme, PresentationStrings, peer: Peer?, cachedData: CachedPeerData?, state: ItemListAvatarAndNameInfoItemState, updatingAvatar: TelegramMediaImageRepresentation?) + case info(PresentationTheme, PresentationStrings, peer: Peer?, cachedData: CachedPeerData?, state: ItemListAvatarAndNameInfoItemState, updatingAvatar: ItemListAvatarAndNameInfoItemUpdatingAvatar?) case about(theme: PresentationTheme, text: String, value: String) case addressName(theme: PresentationTheme, text: String, value: String) case channelPhotoSetup(theme: PresentationTheme, text: String) @@ -249,17 +253,21 @@ private enum ChannelInfoEntry: ItemListNodeEntry { func item(_ arguments: ChannelInfoControllerArguments) -> ListViewItem { switch self { case let .info(theme, strings, peer, cachedData, state, updatingAvatar): - return ItemListAvatarAndNameInfoItem(account: arguments.account, theme: theme, strings: strings, peer: peer, presence: nil, cachedData: cachedData, state: state, sectionId: self.section, style: .plain, editingNameUpdated: { editingName in + return ItemListAvatarAndNameInfoItem(account: arguments.account, theme: theme, strings: strings, mode: .generic, peer: peer, presence: nil, cachedData: cachedData, state: state, sectionId: self.section, style: .plain, editingNameUpdated: { editingName in arguments.updateEditingName(editingName) }, avatarTapped: { arguments.tapAvatarAction() }, context: arguments.avatarAndNameInfoContext, updatingImage: updatingAvatar) case let .about(theme, text, value): - return ItemListTextWithLabelItem(theme: theme, label: text, text: value, multiline: true, sectionId: self.section, action: nil) + return ItemListTextWithLabelItem(theme: theme, label: text, text: value, enabledEntitiyTypes: [.url, .mention, .hashtag], multiline: true, sectionId: self.section, action: nil, longTapAction: { + arguments.displayAboutContextMenu(value) + }, linkItemAction: { action, itemLink in + arguments.aboutLinkAction(action, itemLink) + }, tag: ChannelInfoEntryTag.about) case let .addressName(theme, text, value): - return ItemListTextWithLabelItem(theme: theme, label: text, text: "https://t.me/\(value)", multiline: false, sectionId: self.section, action: { + return ItemListTextWithLabelItem(theme: theme, label: text, text: "https://t.me/\(value)", enabledEntitiyTypes: [], multiline: false, sectionId: self.section, action: { arguments.displayAddressNameContextMenu("https://t.me/\(value)") - }, tag: ChannelInfoEntryTag.addressName) + }) case let .channelPhotoSetup(theme, text): return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { arguments.changeProfilePhoto() @@ -269,7 +277,7 @@ private enum ChannelInfoEntry: ItemListNodeEntry { arguments.openChannelTypeSetup() }) case let .channelDescriptionSetup(theme, placeholder, value): - return ItemListMultilineInputItem(theme: theme, text: value, placeholder: placeholder, sectionId: self.section, style: .plain, textUpdated: { updatedText in + return ItemListMultilineInputItem(theme: theme, text: value, placeholder: placeholder, maxLength: 1000, sectionId: self.section, style: .plain, textUpdated: { updatedText in arguments.updateEditingDescriptionText(updatedText) }, action: { @@ -315,11 +323,11 @@ private enum ChannelInfoEntry: ItemListNodeEntry { } private struct ChannelInfoState: Equatable { - let updatingAvatar: TelegramMediaImageRepresentation? + let updatingAvatar: ItemListAvatarAndNameInfoItemUpdatingAvatar? let editingState: ChannelInfoEditingState? let savingData: Bool - init(updatingAvatar: TelegramMediaImageRepresentation?, editingState: ChannelInfoEditingState?, savingData: Bool) { + init(updatingAvatar: ItemListAvatarAndNameInfoItemUpdatingAvatar?, editingState: ChannelInfoEditingState?, savingData: Bool) { self.updatingAvatar = updatingAvatar self.editingState = editingState self.savingData = savingData @@ -344,7 +352,7 @@ private struct ChannelInfoState: Equatable { return true } - func withUpdatedUpdatingAvatar(_ updatingAvatar: TelegramMediaImageRepresentation?) -> ChannelInfoState { + func withUpdatedUpdatingAvatar(_ updatingAvatar: ItemListAvatarAndNameInfoItemUpdatingAvatar?) -> ChannelInfoState { return ChannelInfoState(updatingAvatar: updatingAvatar, editingState: self.editingState, savingData: self.savingData) } @@ -499,7 +507,6 @@ public func channelInfoController(account: Account, peerId: PeerId) -> ViewContr var pushControllerImpl: ((ViewController) -> Void)? var presentControllerImpl: ((ViewController, Any?) -> Void)? var popToRootControllerImpl: (() -> Void)? - var displayAddressNameContextMenuImpl: ((String) -> Void)? let actionsDisposable = DisposableSet() @@ -523,10 +530,16 @@ public func channelInfoController(account: Account, peerId: PeerId) -> ViewContr actionsDisposable.add(updateAvatarDisposable) let currentAvatarMixin = Atomic(value: nil) + let navigateDisposable = MetaDisposable() + actionsDisposable.add(navigateDisposable) + var avatarGalleryTransitionArguments: ((AvatarGalleryEntry) -> GalleryTransitionArguments?)? let avatarAndNameInfoContext = ItemListAvatarAndNameInfoItemContext() var updateHiddenAvatarImpl: (() -> Void)? + var displayAboutContextMenuImpl: ((String) -> Void)? + var aboutLinkActionImpl: ((TextLinkItemActionType, TextLinkItem) -> Void)? + let arguments = ChannelInfoControllerArguments(account: account, avatarAndNameInfoContext: avatarAndNameInfoContext, tapAvatarAction: { let _ = (account.postbox.loadedPeerWithId(peerId) |> take(1) |> deliverOnMainQueue).start(next: { peer in if peer.profileImageRepresentations.isEmpty { @@ -545,30 +558,62 @@ public func channelInfoController(account: Account, peerId: PeerId) -> ViewContr })) }) }, changeProfilePhoto: { - /*let emptyController = LegacyEmptyController() - let navigationController = makeLegacyNavigationController(rootController: emptyController) - navigationController.setNavigationBarHidden(true, animated: false) - navigationController.navigationBar.transform = CGAffineTransform(translationX: -1000.0, y: 0.0) - - let legacyController = LegacyController(legacyController: navigationController, presentation: .custom) - - presentControllerImpl?(legacyController, nil) - - let mixin = TGMediaAvatarMenuMixin(context: LegacyControllerContext(controller: nil), parentController: emptyController, hasDeleteButton: false, personalPhoto: true, saveEditedPhotos: false, saveCapturedMedia: false)! - let _ = currentAvatarMixin.swap(mixin) - mixin.didDismiss = { [weak legacyController] in - legacyController?.dismiss() - } - mixin.didFinishWithImage = { image in - if let image = image { - if let data = UIImageJPEGRepresentation(image, 0.6) { - let resource = LocalFileMediaResource(fileId: arc4random64()) - account.postbox.mediaBox.storeResourceData(resource.id, data: data) - let representation = TelegramMediaImageRepresentation(dimensions: CGSize(width: 640.0, height: 640.0), resource: resource) - updateState { - $0.withUpdatedUpdatingAvatar(representation) + let _ = (account.postbox.modify { modifier -> Peer? in + return modifier.getPeer(peerId) + } |> deliverOnMainQueue).start(next: { peer in + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + + let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme) + legacyController.statusBar.statusBarStyle = .Ignore + + let emptyController = LegacyEmptyController(context: legacyController.context)! + let navigationController = makeLegacyNavigationController(rootController: emptyController) + navigationController.setNavigationBarHidden(true, animated: false) + navigationController.navigationBar.transform = CGAffineTransform(translationX: -1000.0, y: 0.0) + + legacyController.bind(controller: navigationController) + + presentControllerImpl?(legacyController, nil) + + var hasPhotos = false + if let peer = peer, !peer.profileImageRepresentations.isEmpty { + hasPhotos = true + } + + let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasDeleteButton: hasPhotos, personalPhoto: true, saveEditedPhotos: false, saveCapturedMedia: false)! + let _ = currentAvatarMixin.swap(mixin) + mixin.didFinishWithImage = { image in + if let image = image { + if let data = UIImageJPEGRepresentation(image, 0.6) { + let resource = LocalFileMediaResource(fileId: arc4random64()) + account.postbox.mediaBox.storeResourceData(resource.id, data: data) + let representation = TelegramMediaImageRepresentation(dimensions: CGSize(width: 640.0, height: 640.0), resource: resource) + updateState { + $0.withUpdatedUpdatingAvatar(.image(representation)) + } + updateAvatarDisposable.set((updatePeerPhoto(account: account, peerId: peerId, resource: resource) |> deliverOnMainQueue).start(next: { result in + switch result { + case .complete: + updateState { + $0.withUpdatedUpdatingAvatar(nil) + } + case .progress: + break + } + })) + } } - updateAvatarDisposable.set((updatePeerPhoto(account: account, peerId: peerId, resource: resource) |> deliverOnMainQueue).start(next: { result in + } + mixin.didFinishWithDelete = { + let _ = currentAvatarMixin.swap(nil) + updateState { + if let profileImage = peer?.smallProfileImage { + return $0.withUpdatedUpdatingAvatar(.image(profileImage)) + } else { + return $0.withUpdatedUpdatingAvatar(.none) + } + } + updateAvatarDisposable.set((updatePeerPhoto(account: account, peerId: peerId, resource: nil) |> deliverOnMainQueue).start(next: { result in switch result { case .complete: updateState { @@ -579,13 +624,17 @@ public func channelInfoController(account: Account, peerId: PeerId) -> ViewContr } })) } - } - } - mixin.didDismiss = { [weak legacyController] in - let _ = currentAvatarMixin.swap(nil) - legacyController?.dismiss() - } - mixin.present()*/ + mixin.didDismiss = { [weak legacyController] in + let _ = currentAvatarMixin.swap(nil) + legacyController?.dismiss() + } + let menuController = mixin.present() + if let menuController = menuController { + menuController.customRemoveFromParentViewController = { [weak legacyController] in + legacyController?.dismiss() + } + } + }) }, updateEditingName: { editingName in updateState { state in if let editingState = state.editingState { @@ -689,7 +738,12 @@ public func channelInfoController(account: Account, peerId: PeerId) -> ViewContr }, deleteChannel: { }, displayAddressNameContextMenu: { text in - displayAddressNameContextMenuImpl?(text) + let shareController = ShareController(account: account, subject: .url(text)) + presentControllerImpl?(shareController, nil) + }, displayAboutContextMenu: { text in + displayAboutContextMenuImpl?(text) + }, aboutLinkAction: { action, itemLink in + aboutLinkActionImpl?(action, itemLink) }) let globalNotificationsKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.globalNotifications])) @@ -783,7 +837,7 @@ public func channelInfoController(account: Account, peerId: PeerId) -> ViewContr text = about } updateState { state in - return state.withUpdatedEditingState(ChannelInfoEditingState(editingName: ItemListAvatarAndNameInfoItemName(channel.indexName), editingDescriptionText: text)) + return state.withUpdatedEditingState(ChannelInfoEditingState(editingName: ItemListAvatarAndNameInfoItemName(channel), editingDescriptionText: text)) } } }) @@ -808,36 +862,6 @@ public func channelInfoController(account: Account, peerId: PeerId) -> ViewContr popToRootControllerImpl = { [weak controller] in (controller?.navigationController as? NavigationController)?.popToRoot(animated: true) } - displayAddressNameContextMenuImpl = { [weak controller] text in - if let strongController = controller { - let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } - var resultItemNode: ListViewItemNode? - let _ = strongController.frameForItemNode({ itemNode in - if let itemNode = itemNode as? ItemListTextWithLabelItemNode { - if let tag = itemNode.tag as? ChannelInfoEntryTag { - if tag == .addressName { - resultItemNode = itemNode - return true - } - } - } - return false - }) - if let resultItemNode = resultItemNode { - let contextMenuController = ContextMenuController(actions: [ContextMenuAction(content: .text(presentationData.strings.Conversation_ContextMenuCopy), action: { - UIPasteboard.general.string = text - })]) - strongController.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak resultItemNode] in - if let resultItemNode = resultItemNode { - return (resultItemNode, resultItemNode.contentBounds.insetBy(dx: 0.0, dy: -2.0)) - } else { - return nil - } - })) - - } - } - } avatarGalleryTransitionArguments = { [weak controller] entry in if let controller = controller { var result: (ASDisplayNode, CGRect)? @@ -862,5 +886,40 @@ public func channelInfoController(account: Account, peerId: PeerId) -> ViewContr } } } + displayAboutContextMenuImpl = { [weak controller] text in + if let strongController = controller { + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + var resultItemNode: ListViewItemNode? + let _ = strongController.frameForItemNode({ itemNode in + if let itemNode = itemNode as? ItemListTextWithLabelItemNode { + if let tag = itemNode.tag as? ChannelInfoEntryTag { + if tag == .about { + resultItemNode = itemNode + return true + } + } + } + return false + }) + if let resultItemNode = resultItemNode { + let contextMenuController = ContextMenuController(actions: [ContextMenuAction(content: .text(presentationData.strings.Conversation_ContextMenuCopy), action: { + UIPasteboard.general.string = text + })]) + strongController.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak resultItemNode] in + if let resultItemNode = resultItemNode { + return (resultItemNode, resultItemNode.contentBounds.insetBy(dx: 0.0, dy: -2.0)) + } else { + return nil + } + })) + + } + } + } + aboutLinkActionImpl = { [weak controller] action, itemLink in + if let controller = controller { + handlePeerInfoAboutTextAction(account: account, navigateDisposable: navigateDisposable, controller: controller, action: action, itemLink: itemLink) + } + } return controller } diff --git a/TelegramUI/ChannelMembersController.swift b/TelegramUI/ChannelMembersController.swift index 7f91a856d1..3c5debfcda 100644 --- a/TelegramUI/ChannelMembersController.swift +++ b/TelegramUI/ChannelMembersController.swift @@ -10,12 +10,14 @@ private final class ChannelMembersControllerArguments { let addMember: () -> Void let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void let removePeer: (PeerId) -> Void + let openPeer: (Peer) -> Void - init(account: Account, addMember: @escaping () -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, removePeer: @escaping (PeerId) -> Void) { + init(account: Account, addMember: @escaping () -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, removePeer: @escaping (PeerId) -> Void, openPeer: @escaping (Peer) -> Void) { self.account = account self.addMember = addMember self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions self.removePeer = removePeer + self.openPeer = openPeer } } @@ -56,9 +58,9 @@ private enum ChannelMembersEntryStableId: Hashable { } private enum ChannelMembersEntry: ItemListNodeEntry { - case addMember - case addMemberInfo - case peerItem(Int32, RenderedChannelParticipant, ItemListPeerItemEditing, Bool) + case addMember(PresentationTheme, String) + case addMemberInfo(PresentationTheme, String) + case peerItem(Int32, PresentationTheme, PresentationStrings, RenderedChannelParticipant, ItemListPeerItemEditing, Bool) var section: ItemListSectionId { switch self { @@ -75,20 +77,36 @@ private enum ChannelMembersEntry: ItemListNodeEntry { return .index(0) case .addMemberInfo: return .index(1) - case let .peerItem(_, participant, _, _): + case let .peerItem(_, _, _, participant, _, _): return .peer(participant.peer.id) } } static func ==(lhs: ChannelMembersEntry, rhs: ChannelMembersEntry) -> Bool { switch lhs { - case .addMember, .addMemberInfo: - return lhs.stableId == rhs.stableId - case let .peerItem(lhsIndex, lhsParticipant, lhsEditing, lhsEnabled): - if case let .peerItem(rhsIndex, rhsParticipant, rhsEditing, rhsEnabled) = rhs { + case let .addMember(lhsTheme, lhsText): + if case let .addMember(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .addMemberInfo(lhsTheme, lhsText): + if case let .addMemberInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .peerItem(lhsIndex, lhsTheme, lhsStrings, lhsParticipant, lhsEditing, lhsEnabled): + if case let .peerItem(rhsIndex, rhsTheme, rhsStrings, rhsParticipant, rhsEditing, rhsEnabled) = rhs { if lhsIndex != rhsIndex { return false } + if lhsTheme !== rhsTheme { + return false + } + if lhsStrings !== rhsStrings { + return false + } if lhsParticipant != rhsParticipant { return false } @@ -116,9 +134,9 @@ private enum ChannelMembersEntry: ItemListNodeEntry { default: return true } - case let .peerItem(index, _, _, _): + case let .peerItem(index, _, _, _, _, _): switch rhs { - case let .peerItem(rhsIndex, _, _, _): + case let .peerItem(rhsIndex, _, _, _, _, _): return index < rhsIndex case .addMember, .addMemberInfo: return false @@ -128,14 +146,16 @@ private enum ChannelMembersEntry: ItemListNodeEntry { func item(_ arguments: ChannelMembersControllerArguments) -> ListViewItem { switch self { - case .addMember: - return ItemListActionItem(title: "Add Members", kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { + case let .addMember(theme, text): + return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.addMember() }) - case .addMemberInfo: - return ItemListTextItem(text: .plain("Only channel admins can see this list."), sectionId: self.section) - case let .peerItem(_, participant, editing, enabled): - return ItemListPeerItem(account: arguments.account, peer: participant.peer, presence: nil, text: .none, label: .none, editing: editing, switchValue: nil, enabled: enabled, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { previousId, id in + case let .addMemberInfo(theme, text): + return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + case let .peerItem(_, theme, strings, participant, editing, enabled): + return ItemListPeerItem(theme: theme, strings: strings, account: arguments.account, peer: participant.peer, presence: nil, text: .none, label: .none, editing: editing, switchValue: nil, enabled: enabled, sectionId: self.section, action: { + arguments.openPeer(participant.peer) + }, setPeerIdWithRevealedOptions: { previousId, id in arguments.setPeerIdWithRevealedOptions(previousId, id) }, removePeer: { peerId in arguments.removePeer(peerId) @@ -188,12 +208,12 @@ private struct ChannelMembersControllerState: Equatable { } } -private func ChannelMembersControllerEntries(account: Account, view: PeerView, state: ChannelMembersControllerState, participants: [RenderedChannelParticipant]?) -> [ChannelMembersEntry] { +private func ChannelMembersControllerEntries(account: Account, presentationData: PresentationData, view: PeerView, state: ChannelMembersControllerState, participants: [RenderedChannelParticipant]?) -> [ChannelMembersEntry] { var entries: [ChannelMembersEntry] = [] if let participants = participants { - entries.append(.addMember) - entries.append(.addMemberInfo) + entries.append(.addMember(presentationData.theme, presentationData.strings.Channel_Members_AddMembers)) + entries.append(.addMemberInfo(presentationData.theme, presentationData.strings.Channel_Members_AddMembersHelp)) var index: Int32 = 0 for participant in participants.sorted(by: { lhs, rhs in @@ -229,7 +249,7 @@ private func ChannelMembersControllerEntries(account: Account, view: PeerView, s editable = canEditMembers } } - entries.append(.peerItem(index, participant, ItemListPeerItemEditing(editable: editable, editing: state.editing, revealed: participant.peer.id == state.peerIdWithRevealedOptions), state.removingPeerId != participant.peer.id)) + entries.append(.peerItem(index, presentationData.theme, presentationData.strings, participant, ItemListPeerItemEditing(editable: editable, editing: state.editing, revealed: participant.peer.id == state.peerIdWithRevealedOptions), state.removingPeerId != participant.peer.id)) index += 1 } } @@ -245,6 +265,7 @@ public func channelMembersController(account: Account, peerId: PeerId) -> ViewCo } var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)? + var pushControllerImpl: ((ViewController) -> Void)? let actionsDisposable = DisposableSet() @@ -370,11 +391,15 @@ public func channelMembersController(account: Account, peerId: PeerId) -> ViewCo } })) + }, openPeer: { peer in + if let controller = peerInfoController(account: account, peer: peer) { + pushControllerImpl?(controller) + } }) let peerView = account.viewTracker.peerView(peerId) - let peersSignal: Signal<[RenderedChannelParticipant]?, NoError> = .single(nil) |> then(channelMembers(account: account, peerId: peerId) |> map { Optional($0) }) + let peersSignal: Signal<[RenderedChannelParticipant]?, NoError> = .single(nil) |> then(channelMembers(postbox: account.postbox, network: account.network, peerId: peerId) |> map { Optional($0) }) peersPromise.set(peersSignal) @@ -386,13 +411,13 @@ public func channelMembersController(account: Account, peerId: PeerId) -> ViewCo var rightNavigationButton: ItemListNavigationButton? if let peers = peers, !peers.isEmpty { if state.editing { - rightNavigationButton = ItemListNavigationButton(title: "Done", style: .bold, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Done, style: .bold, enabled: true, action: { updateState { state in return state.withUpdatedEditing(false) } }) } else { - rightNavigationButton = ItemListNavigationButton(title: "Edit", style: .regular, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Edit, style: .regular, enabled: true, action: { updateState { state in return state.withUpdatedEditing(true) } @@ -402,14 +427,14 @@ public func channelMembersController(account: Account, peerId: PeerId) -> ViewCo var emptyStateItem: ItemListControllerEmptyStateItem? if peers == nil { - emptyStateItem = ItemListLoadingIndicatorEmptyStateItem() + emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme) } let previous = previousPeers previousPeers = peers - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text("Members"), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: "Back"), animateChanges: true) - let listState = ItemListNodeState(entries: ChannelMembersControllerEntries(account: account, view: view, state: state, participants: peers), style: .blocks, emptyStateItem: emptyStateItem, animateChanges: previous != nil && peers != nil && previous!.count >= peers!.count) + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.Channel_Members_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) + let listState = ItemListNodeState(entries: ChannelMembersControllerEntries(account: account, presentationData: presentationData, view: view, state: state, participants: peers), style: .blocks, emptyStateItem: emptyStateItem, animateChanges: previous != nil && peers != nil && previous!.count >= peers!.count) return (controllerState, (listState, arguments)) } |> afterDisposed { @@ -422,5 +447,10 @@ public func channelMembersController(account: Account, peerId: PeerId) -> ViewCo controller.present(c, in: .window(.root), with: p) } } + pushControllerImpl = { [weak controller] c in + if let controller = controller { + (controller.navigationController as? NavigationController)?.pushViewController(c) + } + } return controller } diff --git a/TelegramUI/ChannelMembersSearchContainerNode.swift b/TelegramUI/ChannelMembersSearchContainerNode.swift index a9e945ef7d..fa85c42b14 100644 --- a/TelegramUI/ChannelMembersSearchContainerNode.swift +++ b/TelegramUI/ChannelMembersSearchContainerNode.swift @@ -47,7 +47,7 @@ private final class ChannelMembersSearchEntry: Comparable, Identifiable { func item(account: Account, theme: PresentationTheme, strings: PresentationStrings, peerSelected: @escaping (Peer) -> Void) -> ListViewItem { let peer = self.peer - return ContactsPeerItem(theme: theme, strings: strings, account: account, peer: self.peer, chatPeer: self.peer, status: .none, selection: .none, hasActiveRevealControls: false, index: nil, header: ChatListSearchItemHeader(type: self.section.chatListHeaderType, theme: theme, strings: strings, actionTitle: nil, action: nil), action: { _ in + return ContactsPeerItem(theme: theme, strings: strings, account: account, peer: self.peer, chatPeer: self.peer, status: .none, enabled: true, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: ChatListSearchItemHeader(type: self.section.chatListHeaderType, theme: theme, strings: strings, actionTitle: nil, action: nil), action: { _ in peerSelected(peer) }) } @@ -104,9 +104,9 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod let foundItems = searchQuery.get() |> mapToSignal { query -> Signal<[ChannelMembersSearchEntry]?, NoError> in if let query = query, !query.isEmpty { - let foundMembers = channelMembers(account: account, peerId: peerId, filter: .search(query)) + let foundMembers = channelMembers(postbox: account.postbox, network: account.network, peerId: peerId, filter: .search(query)) let foundContacts = account.postbox.searchContacts(query: query.lowercased()) - let foundRemotePeers: Signal<[Peer], NoError> = .single([]) |> then(searchPeers(account: account, query: query) + let foundRemotePeers: Signal<([FoundPeer], [FoundPeer]), NoError> = .single(([], [])) |> then(searchPeers(account: account, query: query) |> delay(0.2, queue: Queue.concurrentDefaultQueue())) return combineLatest(foundMembers, foundContacts, foundRemotePeers, themeAndStringsPromise.get()) @@ -132,7 +132,17 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod } } - for peer in foundRemotePeers { + for foundPeer in foundRemotePeers.0 { + let peer = foundPeer.peer + if !existingPeerIds.contains(peer.id) && peer is TelegramUser { + existingPeerIds.insert(peer.id) + entries.append(ChannelMembersSearchEntry(index: index, peer: peer, section: .global)) + index += 1 + } + } + + for foundPeer in foundRemotePeers.1 { + let peer = foundPeer.peer if !existingPeerIds.contains(peer.id) && peer is TelegramUser { existingPeerIds.insert(peer.id) entries.append(ChannelMembersSearchEntry(index: index, peer: peer, section: .global)) diff --git a/TelegramUI/ChannelMembersSearchController.swift b/TelegramUI/ChannelMembersSearchController.swift index 01b9f4a270..65f4fabaaf 100644 --- a/TelegramUI/ChannelMembersSearchController.swift +++ b/TelegramUI/ChannelMembersSearchController.swift @@ -4,7 +4,7 @@ import TelegramCore import Postbox import SwiftSignalKit -final class ChannelMembersSearchController: TelegramController { +final class ChannelMembersSearchController: ViewController { private let queue = Queue() private let account: Account @@ -26,7 +26,7 @@ final class ChannelMembersSearchController: TelegramController { self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } - super.init(account: account) + super.init(navigationBarTheme: NavigationBarTheme(rootControllerTheme: self.presentationData.theme)) self.title = self.presentationData.strings.Channel_Members_Title self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) diff --git a/TelegramUI/ChannelMembersSearchControllerNode.swift b/TelegramUI/ChannelMembersSearchControllerNode.swift index 4a0dec2f21..b394ebc424 100644 --- a/TelegramUI/ChannelMembersSearchControllerNode.swift +++ b/TelegramUI/ChannelMembersSearchControllerNode.swift @@ -41,7 +41,7 @@ class ChannelMembersSearchControllerNode: ASDisplayNode { self.addSubnode(self.listNode) - self.disposable = (channelMembers(account: account, peerId: peerId) + self.disposable = (channelMembers(postbox: account.postbox, network: account.network, peerId: peerId) |> deliverOnMainQueue).start(next: { [weak self] participants in if let strongSelf = self { var items: [ListViewItem] = [] @@ -52,7 +52,7 @@ class ChannelMembersSearchControllerNode: ASDisplayNode { })) for participant in participants { - items.append(ContactsPeerItem(theme: theme, strings: strings, account: account, peer: participant.peer, chatPeer: nil, status: .none, selection: .none, hasActiveRevealControls: false, index: nil, header: nil, action: { peer in + items.append(ContactsPeerItem(theme: theme, strings: strings, account: account, peer: participant.peer, chatPeer: nil, status: .none, enabled: true, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: nil, action: { peer in if let strongSelf = self { strongSelf.requestOpenPeerFromSearch?(peer) } diff --git a/TelegramUI/ChannelVisibilityController.swift b/TelegramUI/ChannelVisibilityController.swift index 87d855521f..6182c61916 100644 --- a/TelegramUI/ChannelVisibilityController.swift +++ b/TelegramUI/ChannelVisibilityController.swift @@ -12,20 +12,27 @@ private final class ChannelVisibilityControllerArguments { let displayPrivateLinkMenu: (String) -> Void let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void let revokePeerId: (PeerId) -> Void + let copyPrivateLink: () -> Void + let revokePrivateLink: () -> Void + let sharePrivateLink: () -> Void - init(account: Account, updateCurrentType: @escaping (CurrentChannelType) -> Void, updatePublicLinkText: @escaping (String?, String) -> Void, displayPrivateLinkMenu: @escaping (String) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, revokePeerId: @escaping (PeerId) -> Void) { + init(account: Account, updateCurrentType: @escaping (CurrentChannelType) -> Void, updatePublicLinkText: @escaping (String?, String) -> Void, displayPrivateLinkMenu: @escaping (String) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, revokePeerId: @escaping (PeerId) -> Void, copyPrivateLink: @escaping () -> Void, revokePrivateLink: @escaping () -> Void, sharePrivateLink: @escaping () -> Void) { self.account = account self.updateCurrentType = updateCurrentType self.updatePublicLinkText = updatePublicLinkText self.displayPrivateLinkMenu = displayPrivateLinkMenu self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions self.revokePeerId = revokePeerId + self.copyPrivateLink = copyPrivateLink + self.revokePrivateLink = revokePrivateLink + self.sharePrivateLink = sharePrivateLink } } private enum ChannelVisibilitySection: Int32 { case type case link + case linkActions } private enum ChannelVisibilityEntryTag { @@ -42,6 +49,9 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry { case privateLink(PresentationTheme, String, String?) case editablePublicLink(PresentationTheme, String) case privateLinkInfo(PresentationTheme, String) + case privateLinkCopy(PresentationTheme, String) + case privateLinkRevoke(PresentationTheme, String) + case privateLinkShare(PresentationTheme, String) case publicLinkInfo(PresentationTheme, String) case publicLinkStatus(PresentationTheme, String, AddressNameValidationStatus) @@ -54,6 +64,8 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry { return ChannelVisibilitySection.type.rawValue case .publicLinkAvailability, .privateLink, .editablePublicLink, .privateLinkInfo, .publicLinkInfo, .publicLinkStatus: return ChannelVisibilitySection.link.rawValue + case .privateLinkCopy, .privateLinkRevoke, .privateLinkShare: + return ChannelVisibilitySection.linkActions.rawValue case .existingLinksInfo, .existingLinkPeerItem: return ChannelVisibilitySection.link.rawValue } @@ -78,15 +90,20 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry { return 6 case .privateLinkInfo: return 7 - case .publicLinkStatus: + case .privateLinkCopy: return 8 - case .publicLinkInfo: + case .privateLinkRevoke: return 9 - - case .existingLinksInfo: + case .privateLinkShare: return 10 + case .publicLinkStatus: + return 11 + case .publicLinkInfo: + return 12 + case .existingLinksInfo: + return 13 case let .existingLinkPeerItem(index, _, _, _, _, _): - return 11 + index + return 14 + index } } @@ -140,6 +157,24 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry { } else { return false } + case let .privateLinkCopy(lhsTheme, lhsText): + if case let .privateLinkCopy(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .privateLinkRevoke(lhsTheme, lhsText): + if case let .privateLinkRevoke(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .privateLinkShare(lhsTheme, lhsText): + if case let .privateLinkShare(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } case let .publicLinkInfo(lhsTheme, lhsText): if case let .publicLinkInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true @@ -204,7 +239,7 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry { case let .typeInfo(theme, text): return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) case let .publicLinkAvailability(theme, text, value): - return ItemListActivityTextItem(displayActivity: value, text: NSAttributedString(string: text, textColor: value ? theme.list.freeTextColor : theme.list.freeTextErrorColor), sectionId: self.section) + return ItemListActivityTextItem(displayActivity: value, theme: theme, text: NSAttributedString(string: text, textColor: value ? theme.list.freeTextColor : theme.list.freeTextErrorColor), sectionId: self.section) case let .privateLink(theme, text, value): return ItemListActionItem(theme: theme, title: text, kind: value != nil ? .neutral : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: { if let value = value { @@ -219,6 +254,18 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry { }) case let .privateLinkInfo(theme, text): return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + case let .privateLinkCopy(theme, text): + return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { + arguments.copyPrivateLink() + }) + case let .privateLinkRevoke(theme, text): + return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { + arguments.revokePrivateLink() + }) + case let .privateLinkShare(theme, text): + return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { + arguments.sharePrivateLink() + }) case let .publicLinkInfo(theme, text): return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) case let .publicLinkStatus(theme, text, status): @@ -240,7 +287,7 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry { color = theme.list.freeTextColor displayActivity = true } - return ItemListActivityTextItem(displayActivity: displayActivity, text: NSAttributedString(string: text, textColor: color), sectionId: self.section) + return ItemListActivityTextItem(displayActivity: displayActivity, theme: theme, text: NSAttributedString(string: text, textColor: color), sectionId: self.section) case let .existingLinksInfo(theme, text): return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) case let .existingLinkPeerItem(_, theme, strings, peer, editing, enabled): @@ -248,7 +295,7 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry { if let addressName = peer.addressName { label = "t.me/" + addressName } - return ItemListPeerItem(account: arguments.account, peer: peer, presence: nil, text: .text(label), label: .none, editing: editing, switchValue: nil, enabled: enabled, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { previousId, id in + return ItemListPeerItem(theme: theme, strings: strings, account: arguments.account, peer: peer, presence: nil, text: .text(label), label: .none, editing: editing, switchValue: nil, enabled: enabled, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { previousId, id in arguments.setPeerIdWithRevealedOptions(previousId, id) }, removePeer: { peerId in arguments.revokePeerId(peerId) @@ -269,6 +316,7 @@ private struct ChannelVisibilityControllerState: Equatable { let updatingAddressName: Bool let revealedRevokePeerId: PeerId? let revokingPeerId: PeerId? + let revokingPrivateLink: Bool init() { self.selectedType = nil @@ -277,15 +325,17 @@ private struct ChannelVisibilityControllerState: Equatable { self.updatingAddressName = false self.revealedRevokePeerId = nil self.revokingPeerId = nil + self.revokingPrivateLink = false } - init(selectedType: CurrentChannelType?, editingPublicLinkText: String?, addressNameValidationStatus: AddressNameValidationStatus?, updatingAddressName: Bool, revealedRevokePeerId: PeerId?, revokingPeerId: PeerId?) { + init(selectedType: CurrentChannelType?, editingPublicLinkText: String?, addressNameValidationStatus: AddressNameValidationStatus?, updatingAddressName: Bool, revealedRevokePeerId: PeerId?, revokingPeerId: PeerId?, revokingPrivateLink: Bool) { self.selectedType = selectedType self.editingPublicLinkText = editingPublicLinkText self.addressNameValidationStatus = addressNameValidationStatus self.updatingAddressName = updatingAddressName self.revealedRevokePeerId = revealedRevokePeerId self.revokingPeerId = revokingPeerId + self.revokingPrivateLink = revokingPrivateLink } static func ==(lhs: ChannelVisibilityControllerState, rhs: ChannelVisibilityControllerState) -> Bool { @@ -307,36 +357,43 @@ private struct ChannelVisibilityControllerState: Equatable { if lhs.revokingPeerId != rhs.revokingPeerId { return false } + if lhs.revokingPrivateLink != rhs.revokingPrivateLink { + return false + } return true } func withUpdatedSelectedType(_ selectedType: CurrentChannelType?) -> ChannelVisibilityControllerState { - return ChannelVisibilityControllerState(selectedType: selectedType, editingPublicLinkText: self.editingPublicLinkText, addressNameValidationStatus: self.addressNameValidationStatus, updatingAddressName: self.updatingAddressName, revealedRevokePeerId: self.revealedRevokePeerId, revokingPeerId: self.revokingPeerId) + return ChannelVisibilityControllerState(selectedType: selectedType, editingPublicLinkText: self.editingPublicLinkText, addressNameValidationStatus: self.addressNameValidationStatus, updatingAddressName: self.updatingAddressName, revealedRevokePeerId: self.revealedRevokePeerId, revokingPeerId: self.revokingPeerId, revokingPrivateLink: self.revokingPrivateLink) } func withUpdatedEditingPublicLinkText(_ editingPublicLinkText: String?) -> ChannelVisibilityControllerState { - return ChannelVisibilityControllerState(selectedType: self.selectedType, editingPublicLinkText: editingPublicLinkText, addressNameValidationStatus: self.addressNameValidationStatus, updatingAddressName: self.updatingAddressName, revealedRevokePeerId: self.revealedRevokePeerId, revokingPeerId: self.revokingPeerId) + return ChannelVisibilityControllerState(selectedType: self.selectedType, editingPublicLinkText: editingPublicLinkText, addressNameValidationStatus: self.addressNameValidationStatus, updatingAddressName: self.updatingAddressName, revealedRevokePeerId: self.revealedRevokePeerId, revokingPeerId: self.revokingPeerId, revokingPrivateLink: self.revokingPrivateLink) } func withUpdatedAddressNameValidationStatus(_ addressNameValidationStatus: AddressNameValidationStatus?) -> ChannelVisibilityControllerState { - return ChannelVisibilityControllerState(selectedType: self.selectedType, editingPublicLinkText: self.editingPublicLinkText, addressNameValidationStatus: addressNameValidationStatus, updatingAddressName: self.updatingAddressName, revealedRevokePeerId: self.revealedRevokePeerId, revokingPeerId: self.revokingPeerId) + return ChannelVisibilityControllerState(selectedType: self.selectedType, editingPublicLinkText: self.editingPublicLinkText, addressNameValidationStatus: addressNameValidationStatus, updatingAddressName: self.updatingAddressName, revealedRevokePeerId: self.revealedRevokePeerId, revokingPeerId: self.revokingPeerId, revokingPrivateLink: self.revokingPrivateLink) } func withUpdatedUpdatingAddressName(_ updatingAddressName: Bool) -> ChannelVisibilityControllerState { - return ChannelVisibilityControllerState(selectedType: self.selectedType, editingPublicLinkText: self.editingPublicLinkText, addressNameValidationStatus: self.addressNameValidationStatus, updatingAddressName: updatingAddressName, revealedRevokePeerId: self.revealedRevokePeerId, revokingPeerId: self.revokingPeerId) + return ChannelVisibilityControllerState(selectedType: self.selectedType, editingPublicLinkText: self.editingPublicLinkText, addressNameValidationStatus: self.addressNameValidationStatus, updatingAddressName: updatingAddressName, revealedRevokePeerId: self.revealedRevokePeerId, revokingPeerId: self.revokingPeerId, revokingPrivateLink: self.revokingPrivateLink) } func withUpdatedRevealedRevokePeerId(_ revealedRevokePeerId: PeerId?) -> ChannelVisibilityControllerState { - return ChannelVisibilityControllerState(selectedType: self.selectedType, editingPublicLinkText: self.editingPublicLinkText, addressNameValidationStatus: self.addressNameValidationStatus, updatingAddressName: updatingAddressName, revealedRevokePeerId: revealedRevokePeerId, revokingPeerId: self.revokingPeerId) + return ChannelVisibilityControllerState(selectedType: self.selectedType, editingPublicLinkText: self.editingPublicLinkText, addressNameValidationStatus: self.addressNameValidationStatus, updatingAddressName: updatingAddressName, revealedRevokePeerId: revealedRevokePeerId, revokingPeerId: self.revokingPeerId, revokingPrivateLink: self.revokingPrivateLink) } func withUpdatedRevokingPeerId(_ revokingPeerId: PeerId?) -> ChannelVisibilityControllerState { - return ChannelVisibilityControllerState(selectedType: self.selectedType, editingPublicLinkText: self.editingPublicLinkText, addressNameValidationStatus: self.addressNameValidationStatus, updatingAddressName: updatingAddressName, revealedRevokePeerId: self.revealedRevokePeerId, revokingPeerId: revokingPeerId) + return ChannelVisibilityControllerState(selectedType: self.selectedType, editingPublicLinkText: self.editingPublicLinkText, addressNameValidationStatus: self.addressNameValidationStatus, updatingAddressName: updatingAddressName, revealedRevokePeerId: self.revealedRevokePeerId, revokingPeerId: revokingPeerId, revokingPrivateLink: self.revokingPrivateLink) + } + + func withUpdatedRevokingPrivateLink(_ revokingPrivateLink: Bool) -> ChannelVisibilityControllerState { + return ChannelVisibilityControllerState(selectedType: self.selectedType, editingPublicLinkText: self.editingPublicLinkText, addressNameValidationStatus: self.addressNameValidationStatus, updatingAddressName: updatingAddressName, revealedRevokePeerId: self.revealedRevokePeerId, revokingPeerId: self.revokingPeerId, revokingPrivateLink: revokingPrivateLink) } } -private func channelVisibilityControllerEntries(presentationData: PresentationData, view: PeerView, publicChannelsToRevoke: [Peer]?, state: ChannelVisibilityControllerState) -> [ChannelVisibilityEntry] { +private func channelVisibilityControllerEntries(presentationData: PresentationData, mode: ChannelVisibilityControllerMode, view: PeerView, publicChannelsToRevoke: [Peer]?, state: ChannelVisibilityControllerState) -> [ChannelVisibilityEntry] { var entries: [ChannelVisibilityEntry] = [] if let peer = view.peers[view.peerId] as? TelegramChannel { @@ -346,13 +403,17 @@ private func channelVisibilityControllerEntries(presentationData: PresentationDa } let selectedType: CurrentChannelType - if let current = state.selectedType { - selectedType = current + if case .privateLink = mode { + selectedType = .privateChannel } else { - if let addressName = peer.addressName, !addressName.isEmpty { - selectedType = .publicChannel + if let current = state.selectedType { + selectedType = current } else { - selectedType = .privateChannel + if let addressName = peer.addressName, !addressName.isEmpty { + selectedType = .publicChannel + } else { + selectedType = .privateChannel + } } } @@ -367,22 +428,27 @@ private func channelVisibilityControllerEntries(presentationData: PresentationDa } } - entries.append(.typeHeader(presentationData.theme, isGroup ? presentationData.strings.GroupInfo_GroupType : presentationData.strings.Channel_Edit_LinkItem)) - entries.append(.typePublic(presentationData.theme, presentationData.strings.Channel_Setup_TypePublic, selectedType == .publicChannel)) - entries.append(.typePrivate(presentationData.theme, presentationData.strings.Channel_Setup_TypePrivate, selectedType == .privateChannel)) + switch mode { + case .privateLink: + break + case .initialSetup, .generic: + entries.append(.typeHeader(presentationData.theme, isGroup ? presentationData.strings.GroupInfo_GroupType : presentationData.strings.Channel_Edit_LinkItem)) + entries.append(.typePublic(presentationData.theme, presentationData.strings.Channel_Setup_TypePublic, selectedType == .publicChannel)) + entries.append(.typePrivate(presentationData.theme, presentationData.strings.Channel_Setup_TypePrivate, selectedType == .privateChannel)) - switch selectedType { - case .publicChannel: - if isGroup { - entries.append(.typeInfo(presentationData.theme, "Public groups can be found in search, chat history is available to everyone and anyone can join.")) - } else { - entries.append(.typeInfo(presentationData.theme, "Public channels can be found in search and anyone can join.")) - } - case .privateChannel: - if isGroup { - entries.append(.typeInfo(presentationData.theme, "Private groups can only be joined if you were invited of have an invite link.")) - } else { - entries.append(.typeInfo(presentationData.theme, "Private channels can only be joined if you were invited of have an invite link.")) + switch selectedType { + case .publicChannel: + if isGroup { + entries.append(.typeInfo(presentationData.theme, presentationData.strings.Group_Setup_TypePublicHelp)) + } else { + entries.append(.typeInfo(presentationData.theme, presentationData.strings.Channel_Setup_TypePublicHelp)) + } + case .privateChannel: + if isGroup { + entries.append(.typeInfo(presentationData.theme, presentationData.strings.Group_Setup_TypePrivateHelp)) + } else { + entries.append(.typeInfo(presentationData.theme, presentationData.strings.Channel_Setup_TypePrivateHelp)) + } } } @@ -422,32 +488,44 @@ private func channelVisibilityControllerEntries(presentationData: PresentationDa case let .invalidFormat(error): switch error { case .startsWithDigit: - text = "Names can't start with a digit." + if isGroup { + text = presentationData.strings.Group_Username_InvalidStartsWithNumber + } else { + text = presentationData.strings.Channel_Username_InvalidStartsWithNumber + } case .startsWithUnderscore: - text = "Names can't start with an underscore." + text = presentationData.strings.Channel_Username_InvalidCharacters case .endsWithUnderscore: - text = "Names can't end with an underscore." + text = presentationData.strings.Channel_Username_InvalidCharacters case .tooShort: - text = "Names must have at least 5 characters." + if isGroup { + text = presentationData.strings.Group_Username_InvalidTooShort + } else { + text = presentationData.strings.Channel_Username_InvalidTooShort + } case .invalidCharacters: - text = "Sorry, this name is invalid." + text = presentationData.strings.Channel_Username_InvalidTaken } case let .availability(availability): switch availability { case .available: - text = "\(currentAddressName) is available." + text = presentationData.strings.Channel_Username_UsernameIsAvailable(currentAddressName).0 case .invalid: - text = "Sorry, this name is invalid." + text = presentationData.strings.Channel_Username_InvalidCharacters case .taken: - text = "\(currentAddressName) is already taken." + text = presentationData.strings.Channel_Username_InvalidTaken } case .checking: - text = "Checking name..." + text = presentationData.strings.Channel_Username_CheckingUsername } entries.append(.publicLinkStatus(presentationData.theme, text, status)) } - entries.append(.publicLinkInfo(presentationData.theme, "People can share this link with others and find your group using Telegram search.")) + if isGroup { + entries.append(.publicLinkInfo(presentationData.theme, presentationData.strings.Group_Username_CreatePublicLinkHelp)) + } else { + entries.append(.publicLinkInfo(presentationData.theme, presentationData.strings.Channel_Username_CreatePublicLinkHelp)) + } } case .privateChannel: let link = (view.cachedData as? CachedChannelData)?.exportedInvitation?.link @@ -455,14 +533,40 @@ private func channelVisibilityControllerEntries(presentationData: PresentationDa if let link = link { text = link } else { - text = "Loading..." + text = presentationData.strings.Channel_NotificationLoading } entries.append(.privateLink(presentationData.theme, text, link)) if isGroup { - entries.append(.publicLinkInfo(presentationData.theme, "People can join your group by following this link. You can revoke the link at any time.")) + entries.append(.privateLinkInfo(presentationData.theme, presentationData.strings.Group_Username_CreatePrivateLinkHelp)) } else { - entries.append(.publicLinkInfo(presentationData.theme, "People can join your channel by following this link. You can revoke the link at any time.")) + entries.append(.privateLinkInfo(presentationData.theme, presentationData.strings.Channel_Username_CreatePrivateLinkHelp)) } + switch mode { + case .initialSetup: + break + case .generic, .privateLink: + entries.append(.privateLinkCopy(presentationData.theme, presentationData.strings.GroupInfo_InviteLink_CopyLink)) + entries.append(.privateLinkRevoke(presentationData.theme, presentationData.strings.GroupInfo_InviteLink_RevokeLink)) + entries.append(.privateLinkShare(presentationData.theme, presentationData.strings.GroupInfo_InviteLink_ShareLink)) + } + } + } else if let _ = view.peers[view.peerId] as? TelegramGroup { + let link = (view.cachedData as? CachedGroupData)?.exportedInvitation?.link + let text: String + if let link = link { + text = link + } else { + text = presentationData.strings.Channel_NotificationLoading + } + entries.append(.privateLink(presentationData.theme, text, link)) + entries.append(.privateLinkInfo(presentationData.theme, presentationData.strings.Group_Username_CreatePrivateLinkHelp)) + switch mode { + case .initialSetup: + break + case .generic, .privateLink: + entries.append(.privateLinkCopy(presentationData.theme, presentationData.strings.GroupInfo_InviteLink_CopyLink)) + entries.append(.privateLinkRevoke(presentationData.theme, presentationData.strings.GroupInfo_InviteLink_RevokeLink)) + entries.append(.privateLinkShare(presentationData.theme, presentationData.strings.GroupInfo_InviteLink_ShareLink)) } } @@ -518,6 +622,7 @@ private func updatedAddressName(state: ChannelVisibilityControllerState, peer: T public enum ChannelVisibilityControllerMode { case initialSetup case generic + case privateLink } public func channelVisibilityController(account: Account, peerId: PeerId, mode: ChannelVisibilityControllerMode) -> ViewController { @@ -540,6 +645,7 @@ public func channelVisibilityController(account: Account, peerId: PeerId, mode: var dismissImpl: (() -> Void)? var nextImpl: (() -> Void)? var displayPrivateLinkMenuImpl: ((String) -> Void)? + var presentControllerImpl: ((ViewController, Any?) -> Void)? let actionsDisposable = DisposableSet() @@ -552,6 +658,9 @@ public func channelVisibilityController(account: Account, peerId: PeerId, mode: let revokeAddressNameDisposable = MetaDisposable() actionsDisposable.add(revokeAddressNameDisposable) + let revokeLinkDisposable = MetaDisposable() + actionsDisposable.add(revokeLinkDisposable) + actionsDisposable.add(ensuredExistingPeerExportedInvitation(account: account, peerId: peerId).start()) let arguments = ChannelVisibilityControllerArguments(account: account, updateCurrentType: { type in @@ -606,6 +715,54 @@ public func channelVisibilityController(account: Account, peerId: PeerId, mode: } peersDisablingAddressNameAssignment.set(.single([])) })) + }, copyPrivateLink: { + let _ = (account.postbox.modify { modifier -> String? in + if let cachedData = modifier.getPeerCachedData(peerId: peerId) { + if let cachedData = cachedData as? CachedChannelData { + return cachedData.exportedInvitation?.link + } else if let cachedData = cachedData as? CachedGroupData { + return cachedData.exportedInvitation?.link + } + } + return nil + } |> deliverOnMainQueue).start(next: { link in + if let link = link { + UIPasteboard.general.string = link + } + }) + }, revokePrivateLink: { + var revoke = false + updateState { state in + if !state.revokingPrivateLink { + revoke = true + return state.withUpdatedRevokingPrivateLink(true) + } else { + return state + } + } + if revoke { + revokeLinkDisposable.set((ensuredExistingPeerExportedInvitation(account: account, peerId: peerId, revokeExisted: true) |> deliverOnMainQueue).start(completed: { + updateState { + $0.withUpdatedRevokingPrivateLink(false) + } + })) + } + }, sharePrivateLink: { + let _ = (account.postbox.modify { modifier -> String? in + if let cachedData = modifier.getPeerCachedData(peerId: peerId) { + if let cachedData = cachedData as? CachedChannelData { + return cachedData.exportedInvitation?.link + } else if let cachedData = cachedData as? CachedGroupData { + return cachedData.exportedInvitation?.link + } + } + return nil + } |> deliverOnMainQueue).start(next: { link in + if let link = link { + let shareController = ShareController(account: account, subject: .url(link)) + presentControllerImpl?(shareController, nil) + } + }) }) let peerView = account.viewTracker.peerView(peerId) @@ -635,7 +792,7 @@ public func channelVisibilityController(account: Account, peerId: PeerId, mode: } } - rightNavigationButton = ItemListNavigationButton(title: mode == .initialSetup ? "Next" : "Done", style: state.updatingAddressName ? .activity : .bold, enabled: doneEnabled, action: { + rightNavigationButton = ItemListNavigationButton(title: mode == .initialSetup ? presentationData.strings.Common_Next : presentationData.strings.Common_Done, style: state.updatingAddressName ? .activity : .bold, enabled: doneEnabled, action: { var updatedAddressNameValue: String? updateState { state in updatedAddressNameValue = updatedAddressName(state: state, peer: peer) @@ -661,7 +818,7 @@ public func channelVisibilityController(account: Account, peerId: PeerId, mode: switch mode { case .initialSetup: nextImpl?() - case .generic: + case .generic, .privateLink: dismissImpl?() } })) @@ -669,7 +826,7 @@ public func channelVisibilityController(account: Account, peerId: PeerId, mode: switch mode { case .initialSetup: nextImpl?() - case .generic: + case .generic, .privateLink: dismissImpl?() } } @@ -687,14 +844,14 @@ public func channelVisibilityController(account: Account, peerId: PeerId, mode: switch mode { case .initialSetup: leftNavigationButton = nil - case .generic: - leftNavigationButton = ItemListNavigationButton(title: "Cancel", style: .regular, enabled: true, action: { + case .generic, .privateLink: + leftNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Cancel, style: .regular, enabled: true, action: { dismissImpl?() }) } - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(isGroup ? "Group Type" : "Channel Link"), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: "Back"), animateChanges: false) - let listState = ItemListNodeState(entries: channelVisibilityControllerEntries(presentationData: presentationData, view: view, publicChannelsToRevoke: publicChannelsToRevoke, state: state), style: .blocks, animateChanges: false) + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(isGroup ? presentationData.strings.GroupInfo_GroupType : presentationData.strings.Channel_TypeSetup_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) + let listState = ItemListNodeState(entries: channelVisibilityControllerEntries(presentationData: presentationData, mode: mode, view: view, publicChannelsToRevoke: publicChannelsToRevoke, state: state), style: .blocks, animateChanges: false) return (controllerState, (listState, arguments)) } |> afterDisposed { @@ -707,7 +864,7 @@ public func channelVisibilityController(account: Account, peerId: PeerId, mode: } nextImpl = { [weak controller] in if let controller = controller { - (controller.navigationController as? NavigationController)?.replaceAllButRootController(ChatController(account: account, peerId: peerId), animated: true) + (controller.navigationController as? NavigationController)?.replaceAllButRootController(ChatController(account: account, chatLocation: .peer(peerId)), animated: true) } } displayPrivateLinkMenuImpl = { [weak controller] text in @@ -725,7 +882,8 @@ public func channelVisibilityController(account: Account, peerId: PeerId, mode: return false }) if let resultItemNode = resultItemNode { - let contextMenuController = ContextMenuController(actions: [ContextMenuAction(content: .text("Copy"), action: { + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + let contextMenuController = ContextMenuController(actions: [ContextMenuAction(content: .text(presentationData.strings.Conversation_ContextMenuCopyLink), action: { UIPasteboard.general.string = text })]) strongController.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak resultItemNode] in @@ -738,5 +896,8 @@ public func channelVisibilityController(account: Account, peerId: PeerId, mode: } } } + presentControllerImpl = { [weak controller] c, a in + controller?.present(c, in: .window(.root), with: a) + } return controller } diff --git a/TelegramUI/ChatBotInfoItem.swift b/TelegramUI/ChatBotInfoItem.swift index 47d74a99c2..cb3883c3ca 100644 --- a/TelegramUI/ChatBotInfoItem.swift +++ b/TelegramUI/ChatBotInfoItem.swift @@ -22,12 +22,12 @@ final class ChatBotInfoItem: ListViewItem { self.strings = strings } - func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { let configure = { let node = ChatBotInfoItemNode() let nodeLayout = node.asyncLayout() - let (layout, apply) = nodeLayout(self, width) + let (layout, apply) = nodeLayout(self, params) node.contentSize = layout.contentSize node.insets = layout.insets @@ -45,13 +45,13 @@ final class ChatBotInfoItem: ListViewItem { } } - func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { if let node = node as? ChatBotInfoItemNode { Queue.mainQueue().async { let nodeLayout = node.asyncLayout() async { - let (layout, apply) = nodeLayout(self, width) + let (layout, apply) = nodeLayout(self, params) Queue.mainQueue().async { completion(layout, { apply(animation) @@ -98,11 +98,11 @@ final class ChatBotInfoItemNode: ListViewItemNode { self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tapGesture(_:)))) } - func asyncLayout() -> (_ item: ChatBotInfoItem, _ width: CGFloat) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { + func asyncLayout() -> (_ item: ChatBotInfoItem, _ width: ListViewItemLayoutParams) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { let makeTextLayout = TextNode.asyncLayout(self.textNode) let currentTextAndEntities = self.currentTextAndEntities let currentTheme = self.theme - return { [weak self] item, width in + return { [weak self] item, params in var updatedBackgroundImage: UIImage? if currentTheme !== item.theme { updatedBackgroundImage = PresentationResourcesChat.chatInfoItemBackgroundImage(item.theme) @@ -113,25 +113,25 @@ final class ChatBotInfoItemNode: ListViewItemNode { if text == item.text { updatedTextAndEntities = (text, entities) } else { - updatedTextAndEntities = (item.text, generateTextEntities(item.text)) + updatedTextAndEntities = (item.text, generateTextEntities(item.text, enabledTypes: .all)) } } else { - updatedTextAndEntities = (item.text, generateTextEntities(item.text)) + updatedTextAndEntities = (item.text, generateTextEntities(item.text, enabledTypes: .all)) } let attributedText = stringWithAppliedEntities(updatedTextAndEntities.0, entities: updatedTextAndEntities.1, baseColor: item.theme.chat.bubble.infoPrimaryTextColor, linkColor: item.theme.chat.bubble.infoLinkTextColor, baseFont: messageFont, boldFont: messageBoldFont, fixedFont: messageFixedFont) - let horizontalEdgeInset: CGFloat = 10.0 + let horizontalEdgeInset: CGFloat = 10.0 + params.leftInset let horizontalContentInset: CGFloat = 12.0 let verticalItemInset: CGFloat = 10.0 let verticalContentInset: CGFloat = 8.0 - let (textLayout, textApply) = makeTextLayout(attributedText, nil, 0, .end, CGSize(width: width - horizontalEdgeInset * 2.0 - horizontalContentInset * 2.0, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - horizontalEdgeInset * 2.0 - horizontalContentInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let backgroundFrame = CGRect(origin: CGPoint(x: floor((width - textLayout.size.width - horizontalContentInset * 2.0) / 2.0), y: verticalItemInset + 4.0), size: CGSize(width: textLayout.size.width + horizontalContentInset * 2.0, height: textLayout.size.height + verticalContentInset * 2.0)) + let backgroundFrame = CGRect(origin: CGPoint(x: floor((params.width - textLayout.size.width - horizontalContentInset * 2.0) / 2.0), y: verticalItemInset + 4.0), size: CGSize(width: textLayout.size.width + horizontalContentInset * 2.0, height: textLayout.size.height + verticalContentInset * 2.0)) let textFrame = CGRect(origin: CGPoint(x: backgroundFrame.origin.x + horizontalContentInset, y: backgroundFrame.origin.y + verticalContentInset), size: textLayout.size) - let itemLayout = ListViewItemNodeLayout(contentSize: CGSize(width: width, height: textLayout.size.height + verticalItemInset * 2.0 + verticalContentInset * 2.0 + 4.0), insets: UIEdgeInsets()) + let itemLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: textLayout.size.height + verticalItemInset * 2.0 + verticalContentInset * 2.0 + 4.0), insets: UIEdgeInsets()) return (itemLayout, { _ in if let strongSelf = self { strongSelf.theme = item.theme diff --git a/TelegramUI/ChatBotStartInputPanelNode.swift b/TelegramUI/ChatBotStartInputPanelNode.swift index 186df6dfe3..a5dd11d0d3 100644 --- a/TelegramUI/ChatBotStartInputPanelNode.swift +++ b/TelegramUI/ChatBotStartInputPanelNode.swift @@ -86,7 +86,7 @@ final class ChatBotStartInputPanelNode: ChatInputPanelNode { self.interfaceInteraction?.sendBotStart(presentationInterfaceState.botStartPayload) } - override func updateLayout(width: CGFloat, maxHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { if self.presentationInterfaceState != interfaceState { let previousState = self.presentationInterfaceState self.presentationInterfaceState = interfaceState @@ -96,10 +96,10 @@ final class ChatBotStartInputPanelNode: ChatInputPanelNode { let panelHeight: CGFloat = 47.0 - self.button.frame = CGRect(origin: CGPoint(x: floor((width - buttonSize.width) / 2.0), y: floor((panelHeight - buttonSize.height) / 2.0)), size: buttonSize) + self.button.frame = CGRect(origin: CGPoint(x: leftInset + floor((width - leftInset - rightInset - buttonSize.width) / 2.0), y: floor((panelHeight - buttonSize.height) / 2.0)), size: buttonSize) let indicatorSize = self.activityIndicator.bounds.size - self.activityIndicator.frame = CGRect(origin: CGPoint(x: width - indicatorSize.width - 12.0, y: floor((panelHeight - indicatorSize.height) / 2.0)), size: indicatorSize) + self.activityIndicator.frame = CGRect(origin: CGPoint(x: width - rightInset - indicatorSize.width - 12.0, y: floor((panelHeight - indicatorSize.height) / 2.0)), size: indicatorSize) return 47.0 } diff --git a/TelegramUI/ChatBubbleInstantVideoDecoration.swift b/TelegramUI/ChatBubbleInstantVideoDecoration.swift new file mode 100644 index 0000000000..923dbaf6f3 --- /dev/null +++ b/TelegramUI/ChatBubbleInstantVideoDecoration.swift @@ -0,0 +1,92 @@ +import Foundation +import AsyncDisplayKit +import Display +import SwiftSignalKit + +final class ChatBubbleInstantVideoDecoration: UniversalVideoDecoration { + let backgroundNode: ASDisplayNode? + let contentContainerNode: ASDisplayNode + let foregroundNode: ASDisplayNode? + + private let tapped: () -> Void + + private var contentNode: (ASDisplayNode & UniversalVideoContentNode)? + + private var validLayoutSize: CGSize? + + init(diameter: CGFloat, backgroundImage: UIImage?, tapped: @escaping () -> Void) { + self.tapped = tapped + + let backgroundNode = ASImageNode() + backgroundNode.isLayerBacked = true + backgroundNode.displaysAsynchronously = false + backgroundNode.displayWithoutProcessing = true + backgroundNode.image = backgroundImage + self.backgroundNode = backgroundNode + + self.contentContainerNode = ASDisplayNode() + self.contentContainerNode.clipsToBounds = true + self.contentContainerNode.cornerRadius = (diameter - 3.0) / 2.0 + + let foregroundNode = ASDisplayNode() + self.foregroundNode = foregroundNode + //foregroundNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + } + + func updateContentNode(_ contentNode: (UniversalVideoContentNode & ASDisplayNode)?) { + if self.contentNode !== contentNode { + let previous = self.contentNode + self.contentNode = contentNode + + if let previous = previous { + if previous.supernode === self.contentContainerNode { + previous.removeFromSupernode() + } + } + + if let contentNode = contentNode { + if contentNode.supernode !== self.contentContainerNode { + self.contentContainerNode.addSubnode(contentNode) + if let validLayoutSize = self.validLayoutSize { + contentNode.frame = CGRect(origin: CGPoint(), size: validLayoutSize) + contentNode.updateLayout(size: validLayoutSize, transition: .immediate) + } + } + } + } + } + + func updateContentNodeSnapshot(_ snapshot: UIView?) { + } + + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + self.validLayoutSize = size + + if let backgroundNode = self.backgroundNode { + transition.updateFrame(node: backgroundNode, frame: CGRect(origin: CGPoint(), size: size)) + } + if let foregroundNode = self.foregroundNode { + transition.updateFrame(node: foregroundNode, frame: CGRect(origin: CGPoint(), size: size)) + } + let contentFrame = CGRect(origin: CGPoint(x: 1.5, y: 1.5), size: CGSize(width: size.width - 3.0, height: size.height - 3.0)) + transition.updateFrame(node: self.contentContainerNode, frame: contentFrame) + self.contentContainerNode.subnodeTransform = CATransform3DMakeScale((contentFrame.width + 2.0) / contentFrame.width, (contentFrame.width + 2.0) / contentFrame.width, 1.0) + if let contentNode = self.contentNode { + transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(), size: size)) + contentNode.updateLayout(size: size, transition: transition) + } + } + + func setStatus(_ status: Signal) { + } + + @objc func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + //self.tapped() + } + } + + func tap() { + self.tapped() + } +} diff --git a/TelegramUI/ChatBubbleVideoDecoration.swift b/TelegramUI/ChatBubbleVideoDecoration.swift index 2daa7bab48..78fc60ceec 100644 --- a/TelegramUI/ChatBubbleVideoDecoration.swift +++ b/TelegramUI/ChatBubbleVideoDecoration.swift @@ -41,6 +41,9 @@ final class ChatBubbleVideoDecoration: UniversalVideoDecoration { } } + func updateContentNodeSnapshot(_ snapshot: UIView?) { + } + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { self.validLayoutSize = size diff --git a/TelegramUI/ChatButtonKeyboardInputNode.swift b/TelegramUI/ChatButtonKeyboardInputNode.swift index 47e900a15f..38ab2c15da 100644 --- a/TelegramUI/ChatButtonKeyboardInputNode.swift +++ b/TelegramUI/ChatButtonKeyboardInputNode.swift @@ -70,7 +70,7 @@ final class ChatButtonKeyboardInputNode: ChatInputNode { return defaultPortraitPanelHeight } - override func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: UIScreenPixel))) if self.theme !== interfaceState.theme { @@ -96,7 +96,7 @@ final class ChatButtonKeyboardInputNode: ChatInputNode { if let markup = validatedMarkup { let verticalInset: CGFloat = 10.0 - let sideInset: CGFloat = 6.0 + let sideInset: CGFloat = 6.0 + leftInset var buttonHeight: CGFloat = 43.0 let columnSpacing: CGFloat = 6.0 let rowSpacing: CGFloat = 5.0 @@ -148,7 +148,7 @@ final class ChatButtonKeyboardInputNode: ChatInputNode { transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: panelHeight))) self.scrollNode.view.contentSize = CGSize(width: width, height: rowsHeight) - return panelHeight + return panelHeight + bottomInset } else { return 0.0 } diff --git a/TelegramUI/ChatChannelSubscriberInputPanelNode.swift b/TelegramUI/ChatChannelSubscriberInputPanelNode.swift index d9b5c6d025..9b094392d9 100644 --- a/TelegramUI/ChatChannelSubscriberInputPanelNode.swift +++ b/TelegramUI/ChatChannelSubscriberInputPanelNode.swift @@ -54,7 +54,7 @@ final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { private var presentationInterfaceState: ChatPresentationInterfaceState? - private var layoutData: CGFloat? + private var layoutData: (CGFloat, CGFloat, CGFloat)? override init() { self.button = UIButton() @@ -108,8 +108,8 @@ final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { } } - override func updateLayout(width: CGFloat, maxHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { - self.layoutData = width + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { + self.layoutData = (width, leftInset, rightInset) if self.presentationInterfaceState != interfaceState { let previousState = self.presentationInterfaceState @@ -132,10 +132,10 @@ final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { let panelHeight: CGFloat = 47.0 let buttonSize = self.button.bounds.size - self.button.frame = CGRect(origin: CGPoint(x: floor((width - buttonSize.width) / 2.0), y: floor((panelHeight - buttonSize.height) / 2.0)), size: buttonSize) + self.button.frame = CGRect(origin: CGPoint(x: leftInset + floor((width - leftInset - rightInset - buttonSize.width) / 2.0), y: floor((panelHeight - buttonSize.height) / 2.0)), size: buttonSize) let indicatorSize = self.activityIndicator.bounds.size - self.activityIndicator.frame = CGRect(origin: CGPoint(x: width - indicatorSize.width - 12.0, y: floor((panelHeight - indicatorSize.height) / 2.0)), size: indicatorSize) + self.activityIndicator.frame = CGRect(origin: CGPoint(x: width - rightInset - indicatorSize.width - 12.0, y: floor((panelHeight - indicatorSize.height) / 2.0)), size: indicatorSize) return 47.0 } diff --git a/TelegramUI/ChatController.swift b/TelegramUI/ChatController.swift index fd4205f040..fb0fa6d584 100644 --- a/TelegramUI/ChatController.swift +++ b/TelegramUI/ChatController.swift @@ -12,24 +12,48 @@ public enum ChatControllerPeekActions { case remove(() -> Void) } -public class ChatController: TelegramController { +public enum ChatControllerPresentationMode { + case standard + case overlay +} + +public final class ChatControllerOverlayPresentationData { + public let expandData: (ASDisplayNode?, () -> Void) + public init(expandData: (ASDisplayNode?, () -> Void)) { + self.expandData = expandData + } +} + +private enum ChatLocationInfoData { + case peer(Promise) + case group(Promise) +} + +private enum ChatRecordingActivity { + case voice + case instantVideo + case none +} + +public final class ChatController: TelegramController { private var containerLayout = ContainerViewLayout() public var peekActions: ChatControllerPeekActions = .standard private let account: Account - public let peerId: PeerId + public let chatLocation: ChatLocation private let messageId: MessageId? private let botStart: ChatControllerInitialBotStart? private let peerDisposable = MetaDisposable() private let navigationActionDisposable = MetaDisposable() + private var networkStateDisposable: Disposable? private let messageIndexDisposable = MetaDisposable() - private let _peerReady = Promise() - private var didSetPeerReady = false - private let peerView = Promise() + private let _chatLocationInfoReady = Promise() + private var didSetChatLocationInfoReady = false + private let chatLocationInfoData: ChatLocationInfoData private var presentationInterfaceState: ChatPresentationInterfaceState @@ -41,6 +65,7 @@ public class ChatController: TelegramController { private var historyStateDisposable: Disposable? private let galleryHiddenMesageAndMediaDisposable = MetaDisposable() + private let temporaryHiddenGalleryMediaDisposable = MetaDisposable() private weak var secretMediaPreviewController: SecretMediaPreviewController? private var controllerInteraction: ChatControllerInteraction? @@ -65,9 +90,13 @@ public class ChatController: TelegramController { private var resolveUrlDisposable: MetaDisposable? - private var contextQueryState: (ChatPresentationInputQuery?, Disposable)? + private var contextQueryStates: [ChatPresentationInputQueryKind: (ChatPresentationInputQuery, Disposable)] = [:] + private var searchQuerySuggestionState: (ChatPresentationInputQuery?, Disposable)? private var urlPreviewQueryState: (String?, Disposable)? + private var editingUrlPreviewQueryState: (String?, Disposable)? + private var searchState: (String, SearchMessagesLocation)? + private var recordingModeFeedback: HapticFeedback? private var audioRecorderValue: ManagedAudioRecorder? private var audioRecorderFeedback: HapticFeedback? private var audioRecorder = Promise() @@ -89,8 +118,11 @@ public class ChatController: TelegramController { private var unpinMessageDisposable: MetaDisposable? - private let typingActivityPromise = Promise() - private var typingActivityDisposable: Disposable? + private let typingActivityPromise = Promise(false) + private var inputActivityDisposable: Disposable? + private var recordingActivityValue: ChatRecordingActivity = .none + private let recordingActivityPromise = ValuePromise(.none, ignoreRepeated: true) + private var recordingActivityDisposable: Disposable? private var searchDisposable: MetaDisposable? @@ -98,26 +130,43 @@ public class ChatController: TelegramController { let canReadHistory = ValuePromise(true, ignoreRepeated: true) + private var canReadHistoryValue = false + private var canReadHistoryDisposable: Disposable? + private var presentationData: PresentationData private var presentationDataDisposable: Disposable? private var automaticMediaDownloadSettings: AutomaticMediaDownloadSettings private var automaticMediaDownloadSettingsDisposable: Disposable? - public init(account: Account, peerId: PeerId, messageId: MessageId? = nil, botStart: ChatControllerInitialBotStart? = nil) { + private var applicationInForegroundDisposable: Disposable? + + private var raiseToListen: RaiseToListenManager? + + public init(account: Account, chatLocation: ChatLocation, messageId: MessageId? = nil, botStart: ChatControllerInitialBotStart? = nil, mode: ChatControllerPresentationMode = .standard) { self.account = account - self.peerId = peerId + self.chatLocation = chatLocation self.messageId = messageId self.botStart = botStart + switch chatLocation { + case .peer: + self.chatLocationInfoData = .peer(Promise()) + case .group: + self.chatLocationInfoData = .group(Promise()) + } + self.presentationData = (account.applicationContext as! TelegramApplicationContext).currentPresentationData.with { $0 } self.automaticMediaDownloadSettings = (account.applicationContext as! TelegramApplicationContext).currentAutomaticMediaDownloadSettings.with { $0 } - self.presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: self.presentationData.chatWallpaper, theme: self.presentationData.theme, strings: self.presentationData.strings, accountPeerId: account.peerId) + self.presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: self.presentationData.chatWallpaper, theme: self.presentationData.theme, strings: self.presentationData.strings, fontSize: self.presentationData.fontSize, accountPeerId: account.peerId, mode: mode, chatLocation: chatLocation) - super.init(account: account) - self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style + var enableMediaAccessoryPanel = true + if case .overlay = mode { + enableMediaAccessoryPanel = false + } + super.init(account: account, navigationBarTheme: NavigationBarTheme(rootControllerTheme: self.presentationData.theme), enableMediaAccessoryPanel: enableMediaAccessoryPanel) self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) @@ -131,68 +180,46 @@ public class ChatController: TelegramController { let controllerInteraction = ChatControllerInteraction(openMessage: { [weak self] id in if let strongSelf = self, strongSelf.isNodeLoaded, let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(id) { - var galleryMedia: Media? - var otherMedia: Media? - for media in message.media { - if let file = media as? TelegramMediaFile { - if !file.isAnimated { - galleryMedia = file + return openChatMessage(account: account, message: message, reverseMessageGalleryOrder: false, navigationController: strongSelf.navigationController as? NavigationController, dismissInput: { + self?.chatDisplayNode.dismissInput() + }, present: { c, a in + self?.present(c, in: .window(.root), with: a) + }, transitionNode: { messageId, media in + var selectedNode: ASDisplayNode? + if let strongSelf = self { + strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ChatMessageItemView { + if let result = itemNode.transitionNode(id: messageId, media: media) { + selectedNode = result + } + } } - } else if let image = media as? TelegramMediaImage { - galleryMedia = image - } else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { - if let file = content.file { - galleryMedia = file - } else if let image = content.image { - galleryMedia = image - } - } else if let mapMedia = media as? TelegramMediaMap { - galleryMedia = mapMedia - } else if let contactMedia = media as? TelegramMediaContact { - otherMedia = contactMedia } - } - - if let galleryMedia = galleryMedia { - if let mapMedia = galleryMedia as? TelegramMediaMap { - strongSelf.chatDisplayNode.dismissInput() - strongSelf.present(legacyLocationController(message: message, mapMedia: mapMedia, account: strongSelf.account, openPeer: { peer in - self?.openPeer(peerId: peer.id, navigation: .info, fromMessageId: nil) - }), in: .window(.root)) - } else if let file = galleryMedia as? TelegramMediaFile, file.isSticker { - for attribute in file.attributes { - if case let .Sticker(_, reference, _) = attribute { - if let reference = reference { - let controller = StickerPackPreviewController(account: strongSelf.account, stickerPack: reference) - controller.sendSticker = { file in - self?.controllerInteraction?.sendSticker(file) - } - strongSelf.chatDisplayNode.dismissInput() - strongSelf.present(controller, in: .window(.root)) - } - break - } - } - } else if let file = galleryMedia as? TelegramMediaFile, file.isMusic || file.isVoice || file.isInstantVideo { - if let applicationContext = strongSelf.account.applicationContext as? TelegramApplicationContext { - let player = ManagedAudioPlaylistPlayer(audioSessionManager: (strongSelf.account.applicationContext as! TelegramApplicationContext).mediaManager.audioSession, overlayMediaManager: (strongSelf.account.applicationContext as! TelegramApplicationContext).mediaManager.overlayMediaManager, mediaManager: (strongSelf.account.applicationContext as! TelegramApplicationContext).mediaManager, account: strongSelf.account, postbox: strongSelf.account.postbox, playlist: peerMessageHistoryAudioPlaylist(account: strongSelf.account, messageId: id)) - applicationContext.mediaManager.setPlaylistPlayer(player) - player.control(.navigation(.next)) - } - } else { - let gallery = GalleryController(account: strongSelf.account, messageId: id, replaceRootController: { controller, ready in - if let strongSelf = self { - (strongSelf.navigationController as? NavigationController)?.replaceTopController(controller, animated: false, ready: ready) - } - }, baseNavigationController: strongSelf.navigationController as? NavigationController) - - strongSelf.galleryHiddenMesageAndMediaDisposable.set(gallery.hiddenMedia.start(next: { [weak strongSelf] messageIdAndMedia in - if let strongSelf = strongSelf { - if let messageIdAndMedia = messageIdAndMedia { - strongSelf.controllerInteraction?.hiddenMedia = [messageIdAndMedia.0: [messageIdAndMedia.1]] - } else { - strongSelf.controllerInteraction?.hiddenMedia = [:] + return selectedNode + }, addToTransitionSurface: { view in + if let strongSelf = self { + strongSelf.chatDisplayNode.historyNode.view.superview?.insertSubview(view, aboveSubview: strongSelf.chatDisplayNode.historyNode.view) + } + }, openUrl: { url in + self?.openUrl(url) + }, openPeer: { peer, navigation in + self?.openPeer(peerId: peer.id, navigation: navigation, fromMessageId: nil) + }, callPeer: { peerId in + self?.controllerInteraction?.callPeer(peerId) + }, sendSticker: { file in + self?.controllerInteraction?.sendSticker(file) + }, setupTemporaryHiddenMedia: { signal, centralIndex, galleryMedia in + if let strongSelf = self { + strongSelf.temporaryHiddenGalleryMediaDisposable.set((signal |> deliverOnMainQueue).start(next: { entry in + if let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction { + var messageIdAndMedia: [MessageId: [Media]] = [:] + + if let entry = entry, entry.index == centralIndex { + messageIdAndMedia[message.id] = [galleryMedia] } + + controllerInteraction.hiddenMedia = messageIdAndMedia + strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in if let itemNode = itemNode as? ChatMessageItemView { itemNode.updateHiddenMedia() @@ -200,81 +227,10 @@ public class ChatController: TelegramController { } } })) - - strongSelf.chatDisplayNode.dismissInput() - strongSelf.present(gallery, in: .window(.root), with: GalleryControllerPresentationArguments(transitionArguments: { [weak self] messageId, media in - if let strongSelf = self { - var transitionNode: ASDisplayNode? - strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in - if let itemNode = itemNode as? ChatMessageItemView { - if let result = itemNode.transitionNode(id: messageId, media: media) { - transitionNode = result - } - } - } - if let transitionNode = transitionNode { - return GalleryTransitionArguments(transitionNode: transitionNode, addToTransitionSurface: { view in - if let strongSelf = self { - strongSelf.chatDisplayNode.historyNode.view.superview?.insertSubview(view, aboveSubview: strongSelf.chatDisplayNode.historyNode.view) - } - }) - } - } - return nil - })) } - } else if let contact = otherMedia as? TelegramMediaContact { - let _ = (strongSelf.account.postbox.modify { modifier -> Bool? in - if let peerId = contact.peerId { - return modifier.isPeerContact(peerId: peerId) - } else { - return nil - } - } |> deliverOnMainQueue).start(next: { isContact in - if let strongSelf = self { - let controller = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) - let dismissAction: () -> Void = { [weak controller] in - controller?.dismissAnimated() - } - var items: [ActionSheetItem] = [] - - if let peerId = contact.peerId { - items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_SendMessage, action: { - dismissAction() - if let strongSelf = self { - strongSelf.controllerInteraction?.openPeer(peerId, .chat(textInputState: nil), nil) - } - })) - if let isContact = isContact, !isContact { - items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_AddContact, action: { - dismissAction() - if let strongSelf = self { - let _ = addContactPeerInteractively(account: strongSelf.account, peerId: peerId, phone: contact.phoneNumber).start() - } - })) - } - items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.UserInfo_TelegramCall, action: { - dismissAction() - if let strongSelf = self { - strongSelf.controllerInteraction?.callPeer(peerId) - } - })) - } - items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.UserInfo_PhoneCall, action: { - dismissAction() - if let strongSelf = self { - strongSelf.account.telegramApplicationContext.applicationBindings.openUrl("tel:\(formatPhoneNumber(contact.phoneNumber).replacingOccurrences(of: " ", with: ""))") - } - })) - controller.setItemGroups([ - ActionSheetItemGroup(items: items), - ActionSheetItemGroup(items: [ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, action: { dismissAction() })]) - ]) - strongSelf.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) - } - }) - } + }) } + return false }, openSecretMessagePreview: { [weak self] messageId in if let strongSelf = self { var galleryMedia: Media? @@ -309,17 +265,27 @@ public class ChatController: TelegramController { } }, openMessageContextMenu: { [weak self] id, node, frame in if let strongSelf = self, strongSelf.isNodeLoaded { - if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(id) { - let _ = contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: strongSelf.presentationInterfaceState, account: strongSelf.account, message: message, interfaceInteraction: strongSelf.interfaceInteraction).start(next: { contextMenuController in + if let messages = strongSelf.chatDisplayNode.historyNode.messageGroupInCurrentHistoryView(id) { + var updatedMessages = messages + for i in 0 ..< updatedMessages.count { + if updatedMessages[i].id == id { + let message = updatedMessages.remove(at: i) + updatedMessages.insert(message, at: 0) + break + } + } + let _ = contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: strongSelf.presentationInterfaceState, account: strongSelf.account, messages: updatedMessages, interfaceInteraction: strongSelf.interfaceInteraction, debugStreamSingleVideo: { id in + self?.debugStreamSingleVideo(id) + }).start(next: { contextMenuController in if let strongSelf = self, let contextMenuController = contextMenuController { if let controllerInteraction = strongSelf.controllerInteraction { - controllerInteraction.highlightedState = ChatInterfaceHighlightedState(messageStableId: message.stableId) + controllerInteraction.highlightedState = ChatInterfaceHighlightedState(messageStableId: updatedMessages[0].stableId) strongSelf.updateItemNodesHighlightedStates(animated: true) } contextMenuController.dismissed = { if let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction { - if controllerInteraction.highlightedState?.messageStableId == message.stableId { + if controllerInteraction.highlightedState?.messageStableId == updatedMessages[0].stableId { controllerInteraction.highlightedState = nil strongSelf.updateItemNodesHighlightedStates(animated: true) } @@ -341,11 +307,9 @@ public class ChatController: TelegramController { self?.navigateToMessage(from: fromId, to: id) }, clickThroughMessage: { [weak self] in self?.chatDisplayNode.dismissInput() - }, toggleMessageSelection: { [weak self] id in + }, toggleMessagesSelection: { [weak self] ids, value in if let strongSelf = self, strongSelf.isNodeLoaded { - if let _ = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(id) { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withToggledSelectedMessage(id) } }) - } + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withToggledSelectedMessages(ids, value: value) } }) } }, sendMessage: { [weak self] text in if let strongSelf = self { @@ -357,11 +321,11 @@ public class ChatController: TelegramController { } }) var attributes: [MessageAttribute] = [] - let entities = generateTextEntities(text) + let entities = generateTextEntities(text, enabledTypes: .all) if !entities.isEmpty { attributes.append(TextEntitiesMessageAttribute(entities: entities)) } - strongSelf.sendMessages([.message(text: text, attributes: attributes, media: nil, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId)]) + strongSelf.sendMessages([.message(text: text, attributes: attributes, media: nil, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil)]) } }, sendSticker: { [weak self] file in if let strongSelf = self { @@ -372,7 +336,7 @@ public class ChatController: TelegramController { }) } }) - strongSelf.sendMessages([.message(text: "", attributes: [], media: file, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId)]) + strongSelf.sendMessages([.message(text: "", attributes: [], media: file, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil)]) } }, sendGif: { [weak self] file in if let strongSelf = self { @@ -383,7 +347,7 @@ public class ChatController: TelegramController { }) } }) - strongSelf.sendMessages([.message(text: "", attributes: [], media: file, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId)]) + strongSelf.sendMessages([.message(text: "", attributes: [], media: file, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil)]) } }, requestMessageActionCallback: { [weak self] messageId, data, isGame in if let strongSelf = self { @@ -466,9 +430,17 @@ public class ChatController: TelegramController { if let strongSelf = self { strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({}) var postAsReply = false - if !command.contains("@") && (strongSelf.peerId.namespace == Namespaces.Peer.CloudChannel || strongSelf.peerId.namespace == Namespaces.Peer.CloudGroup) { - postAsReply = true + if !command.contains("@") { + switch strongSelf.chatLocation { + case let .peer(peerId): + if (peerId.namespace == Namespaces.Peer.CloudChannel || peerId.namespace == Namespaces.Peer.CloudGroup) { + postAsReply = true + } + case .group: + postAsReply = true + } } + strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { @@ -477,63 +449,15 @@ public class ChatController: TelegramController { } }) var attributes: [MessageAttribute] = [] - let entities = generateTextEntities(command) + let entities = generateTextEntities(command, enabledTypes: .all) if !entities.isEmpty { attributes.append(TextEntitiesMessageAttribute(entities: entities)) } - strongSelf.sendMessages([.message(text: command, attributes: attributes, media: nil, replyToMessageId: (postAsReply && messageId != nil) ? messageId! : nil)]) + strongSelf.sendMessages([.message(text: command, attributes: attributes, media: nil, replyToMessageId: (postAsReply && messageId != nil) ? messageId! : nil, localGroupingKey: nil)]) } }, openInstantPage: { [weak self] messageId in - if let strongSelf = self, strongSelf.isNodeLoaded { - if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) { - for media in message.media { - if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { - if let _ = content.instantPage { - var textUrl: String? - if let pageUrl = URL(string: content.url) { - inner: for attribute in message.attributes { - if let attribute = attribute as? TextEntitiesMessageAttribute { - for entity in attribute.entities { - switch entity.type { - case let .TextUrl(url): - if let parsedUrl = URL(string: url) { - if pageUrl.scheme == parsedUrl.scheme && pageUrl.host == parsedUrl.host && pageUrl.path == parsedUrl.path { - textUrl = url - } - } - case .Url: - let nsText = message.text as NSString - var entityRange = NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound) - if entityRange.location + entityRange.length > nsText.length { - entityRange.location = max(0, nsText.length - entityRange.length) - entityRange.length = nsText.length - entityRange.location - } - let url = nsText.substring(with: entityRange) - if let parsedUrl = URL(string: url) { - if pageUrl.scheme == parsedUrl.scheme && pageUrl.host == parsedUrl.host && pageUrl.path == parsedUrl.path { - textUrl = url - } - } - default: - break - } - } - break inner - } - } - } - var anchor: String? - if let textUrl = textUrl, let anchorRange = textUrl.range(of: "#") { - anchor = String(textUrl[anchorRange.upperBound...]) - } - - let pageController = InstantPageController(account: strongSelf.account, webPage: webpage, anchor: anchor) - (strongSelf.navigationController as? NavigationController)?.pushViewController(pageController) - } - break - } - } - } + if let strongSelf = self, strongSelf.isNodeLoaded, let navigationController = strongSelf.navigationController as? NavigationController, let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) { + openChatInstantPage(account: strongSelf.account, message: message, navigationController: navigationController) } }, openHashtag: { [weak self] peerName, hashtag in if let strongSelf = self, !hashtag.isEmpty { @@ -549,8 +473,8 @@ public class ChatController: TelegramController { }) } }, openMessageShareMenu: { [weak self] id in - if let strongSelf = self, let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(id) { - let shareController = ShareController(account: strongSelf.account, subject: .message(message)) + if let strongSelf = self, let messages = strongSelf.chatDisplayNode.historyNode.messageGroupInCurrentHistoryView(id) { + let shareController = ShareController(account: strongSelf.account, subject: .messages(messages)) strongSelf.chatDisplayNode.dismissInput() strongSelf.present(shareController, in: .window(.root)) } @@ -580,26 +504,42 @@ public class ChatController: TelegramController { if let strongSelf = self { switch action { case let .url(url): + var cleanUrl = url + var canAddToReadingList = true + let mailtoString = "mailto:" + let telString = "tel:" + var openText = strongSelf.presentationData.strings.Conversation_LinkDialogOpen + if cleanUrl.hasPrefix(mailtoString) { + canAddToReadingList = false + cleanUrl = String(cleanUrl[cleanUrl.index(cleanUrl.startIndex, offsetBy: mailtoString.distance(from: mailtoString.startIndex, to: mailtoString.endIndex))...]) + } else if cleanUrl.hasPrefix(telString) { + canAddToReadingList = false + cleanUrl = String(cleanUrl[cleanUrl.index(cleanUrl.startIndex, offsetBy: telString.distance(from: telString.startIndex, to: telString.endIndex))...]) + openText = strongSelf.presentationData.strings.Conversation_Call + } let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) - actionSheet.setItemGroups([ActionSheetItemGroup(items: [ - ActionSheetTextItem(title: url), - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - if let strongSelf = self { - strongSelf.openUrl(url) - } - }), - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Web_CopyLink, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - UIPasteboard.general.string = url - }), - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_AddToReadingList, color: .accent, action: { [weak actionSheet] in + + var items: [ActionSheetItem] = [] + items.append(ActionSheetTextItem(title: cleanUrl)) + items.append(ActionSheetButtonItem(title: openText, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + if let strongSelf = self { + strongSelf.openUrl(url) + } + })) + items.append(ActionSheetButtonItem(title: canAddToReadingList ? strongSelf.presentationData.strings.ShareMenu_CopyShareLink : strongSelf.presentationData.strings.Conversation_ContextMenuCopy, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + UIPasteboard.general.string = cleanUrl + })) + if canAddToReadingList { + items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_AddToReadingList, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() if let link = URL(string: url) { let _ = try? SSReadingList.default()?.addItem(with: link, title: nil, previewText: nil) } - }) - ]), ActionSheetItemGroup(items: [ + })) + } + actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) @@ -659,7 +599,7 @@ public class ChatController: TelegramController { ActionSheetButtonItem(title: strongSelf.presentationData.strings.ShareMenu_Send, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() if let strongSelf = self { - strongSelf.sendMessages([.message(text: command, attributes: [], media: nil, replyToMessageId: nil)]) + strongSelf.sendMessages([.message(text: command, attributes: [], media: nil, replyToMessageId: nil, localGroupingKey: nil)]) } }), ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in @@ -713,38 +653,55 @@ public class ChatController: TelegramController { } } }, openSearch: { + }, setupReply: { [weak self] messageId in + self?.interfaceInteraction?.setupReplyMessage(messageId) + }, canSetupReply: { [weak self] in + if let strongSelf = self { + return canReplyInChat(strongSelf.presentationInterfaceState) + } + return false }, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings) self.controllerInteraction = controllerInteraction - self.chatTitleView = ChatTitleView(theme: self.presentationData.theme, strings: self.presentationData.strings) + self.chatTitleView = ChatTitleView(account: self.account, theme: self.presentationData.theme, strings: self.presentationData.strings) self.navigationItem.titleView = self.chatTitleView self.chatTitleView?.pressed = { [weak self] in if let strongSelf = self { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { - return $0.updatedTitlePanelContext { - if let index = $0.index(where: { - switch $0 { - case .chatInfo: - return true - default: - return false + if strongSelf.chatLocation == .peer(strongSelf.account.peerId) { + (strongSelf.navigationController as? NavigationController)?.pushViewController(PeerMediaCollectionController(account: strongSelf.account, peerId: strongSelf.account.peerId)) + } else { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { + return $0.updatedTitlePanelContext { + if let index = $0.index(where: { + switch $0 { + case .chatInfo: + return true + default: + return false + } + }) { + var updatedContexts = $0 + updatedContexts.remove(at: index) + return updatedContexts + } else { + var updatedContexts = $0 + updatedContexts.append(.chatInfo) + return updatedContexts.sorted() } - }) { - var updatedContexts = $0 - updatedContexts.remove(at: index) - return updatedContexts - } else { - var updatedContexts = $0 - updatedContexts.append(.chatInfo) - return updatedContexts.sorted() } - } - }) + }) + } } } - let chatInfoButtonItem = UIBarButtonItem(customDisplayNode: ChatAvatarNavigationNode())! + let chatInfoButtonItem: UIBarButtonItem + switch chatLocation { + case .peer: + chatInfoButtonItem = UIBarButtonItem(customDisplayNode: ChatAvatarNavigationNode())! + case .group: + chatInfoButtonItem = UIBarButtonItem(customDisplayNode: ChatMultipleAvatarsNavigationNode())! + } chatInfoButtonItem.target = self chatInfoButtonItem.action = #selector(self.rightNavigationButtonAction) self.chatInfoNavigationButton = ChatNavigationButton(action: .openChatInfo, buttonItem: chatInfoButtonItem) @@ -757,28 +714,55 @@ public class ChatController: TelegramController { } }) - self.peerView.set(account.viewTracker.peerView(peerId)) - - self.peerDisposable.set((self.peerView.get() - |> deliverOnMainQueue).start(next: { [weak self] peerView in - if let strongSelf = self { - if let peer = peerViewMainPeer(peerView) { - strongSelf.chatTitleView?.peerView = peerView - (strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.avatarNode.setPeer(account: strongSelf.account, peer: peer) - } - var peerIsMuted = false - if let notificationSettings = peerView.notificationSettings as? TelegramPeerNotificationSettings { - if case .muted = notificationSettings.muteState { - peerIsMuted = true - } - } - strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: false, { return $0.updatedPeer { _ in return peerView.peers[peerId] }.updatedPeerIsMuted(peerIsMuted) }) - if !strongSelf.didSetPeerReady { - strongSelf.didSetPeerReady = true - strongSelf._peerReady.set(.single(true)) - } + switch chatLocation { + case let .peer(peerId): + if case let .peer(peerView) = self.chatLocationInfoData { + peerView.set(account.viewTracker.peerView(peerId)) + self.peerDisposable.set((peerView.get() + |> deliverOnMainQueue).start(next: { [weak self] peerView in + if let strongSelf = self { + if let peer = peerViewMainPeer(peerView) { + strongSelf.chatTitleView?.titleContent = .peer(peerView) + (strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.avatarNode.setPeer(account: strongSelf.account, peer: peer) + } + var peerIsMuted = false + if let notificationSettings = peerView.notificationSettings as? TelegramPeerNotificationSettings { + if case .muted = notificationSettings.muteState { + peerIsMuted = true + } + } + strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: false, { return $0.updatedPeer { _ in return peerView.peers[peerId] }.updatedPeerIsMuted(peerIsMuted) }) + if !strongSelf.didSetChatLocationInfoReady { + strongSelf.didSetChatLocationInfoReady = true + strongSelf._chatLocationInfoReady.set(.single(true)) + } + } + })) } - })) + case let .group(groupId): + if case let .group(topPeersView) = self.chatLocationInfoData { + let key: PostboxViewKey = .chatListTopPeers(groupId: groupId) + topPeersView.set(account.postbox.combinedView(keys: [key]) + |> mapToSignal { view -> Signal in + if let entry = view.views[key] as? ChatListTopPeersView { + return .single(entry) + } + return .complete() + }) + self.peerDisposable.set((topPeersView.get() + |> deliverOnMainQueue).start(next: { [weak self] topPeersView in + if let strongSelf = self { + strongSelf.chatTitleView?.titleContent = .group(topPeersView.peers) + (strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatMultipleAvatarsNavigationNode)?.setPeers(account: strongSelf.account, peers: topPeersView.peers, animated: strongSelf.didSetChatLocationInfoReady) + + if !strongSelf.didSetChatLocationInfoReady { + strongSelf.didSetChatLocationInfoReady = true + strongSelf._chatLocationInfoReady.set(.single(true)) + } + } + })) + } + } self.botCallbackAlertMessageDisposable = (self.botCallbackAlertMessage.get() |> deliverOnMainQueue).start(next: { [weak self] message in @@ -846,6 +830,9 @@ public class ChatController: TelegramController { }) if let audioRecorder = audioRecorder { + if !audioRecorder.beginWithTone { + strongSelf.audioRecorderFeedback?.tap() + } audioRecorder.start() } } @@ -877,16 +864,21 @@ public class ChatController: TelegramController { strongSelf.videoRecorder.set(.single(nil)) } } + videoRecorder.onStop = { + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { + $0.updatedInputTextPanelState { panelState in + return panelState.withUpdatedMediaRecordingState(.video(status: .editing, isLocked: false)) + } + }) + } + } strongSelf.present(videoRecorder, in: .window(.root)) } if let previousVideoRecorderValue = previousVideoRecorderValue { previousVideoRecorderValue.dismissVideo() } - - /*if let videoRecorder = videoRecorder { - videoRecorder.start() - }*/ } } }) @@ -895,10 +887,27 @@ public class ChatController: TelegramController { self.startBot(botStart.payload) } - self.typingActivityDisposable = (self.typingActivityPromise.get() + self.inputActivityDisposable = (self.typingActivityPromise.get() |> deliverOnMainQueue).start(next: { [weak self] value in - if let strongSelf = self { - strongSelf.account.updateLocalInputActivity(peerId: strongSelf.peerId, activity: .typingText, isPresent: value) + if let strongSelf = self, case let .peer(peerId) = strongSelf.chatLocation { + strongSelf.account.updateLocalInputActivity(peerId: peerId, activity: .typingText, isPresent: value) + } + }) + + self.recordingActivityDisposable = (self.recordingActivityPromise.get() + |> deliverOnMainQueue).start(next: { [weak self] value in + if let strongSelf = self, case let .peer(peerId) = strongSelf.chatLocation { + switch value { + case .voice: + strongSelf.account.updateLocalInputActivity(peerId: peerId, activity: .recordingVoice, isPresent: true) + strongSelf.account.updateLocalInputActivity(peerId: peerId, activity: .recordingInstantVideo, isPresent: false) + case .instantVideo: + strongSelf.account.updateLocalInputActivity(peerId: peerId, activity: .recordingVoice, isPresent: false) + strongSelf.account.updateLocalInputActivity(peerId: peerId, activity: .recordingInstantVideo, isPresent: true) + case .none: + strongSelf.account.updateLocalInputActivity(peerId: peerId, activity: .recordingVoice, isPresent: false) + strongSelf.account.updateLocalInputActivity(peerId: peerId, activity: .recordingInstantVideo, isPresent: false) + } } }) @@ -926,6 +935,31 @@ public class ChatController: TelegramController { } } }) + + self.applicationInForegroundDisposable = (account.telegramApplicationContext.applicationBindings.applicationInForeground + |> distinctUntilChanged + |> deliverOn(Queue.mainQueue())).start(next: { [weak self] value in + if let strongSelf = self, strongSelf.isNodeLoaded { + if !value { + strongSelf.saveInterfaceState() + } + } + }) + + self.canReadHistoryDisposable = (combineLatest((self.account.applicationContext as! TelegramApplicationContext).applicationBindings.applicationInForeground, self.canReadHistory.get()) |> map { a, b in + return a && b + } |> deliverOnMainQueue).start(next: { [weak self] value in + if let strongSelf = self, strongSelf.canReadHistoryValue != value { + strongSelf.canReadHistoryValue = value + strongSelf.raiseToListen?.enabled = value + } + }) + + self.networkStateDisposable = (account.networkState |> deliverOnMainQueue).start(next: { [weak self] state in + if let strongSelf = self { + strongSelf.chatTitleView?.networkState = state + } + }) } required public init(coder aDecoder: NSCoder) { @@ -937,6 +971,7 @@ public class ChatController: TelegramController { self.messageIndexDisposable.dispose() self.navigationActionDisposable.dispose() self.galleryHiddenMesageAndMediaDisposable.dispose() + self.temporaryHiddenGalleryMediaDisposable.dispose() self.peerDisposable.dispose() self.messageContextDisposable.dispose() self.controllerNavigationDisposable.dispose() @@ -946,7 +981,9 @@ public class ChatController: TelegramController { self.enqueueMediaMessageDisposable.dispose() self.resolvePeerByNameDisposable?.dispose() self.botCallbackAlertMessageDisposable?.dispose() - self.contextQueryState?.1.dispose() + for (_, info) in self.contextQueryStates { + info.1.dispose() + } self.urlPreviewQueryState?.1.dispose() self.audioRecorderDisposable?.dispose() self.videoRecorderDisposable?.dispose() @@ -958,9 +995,13 @@ public class ChatController: TelegramController { self.peerInputActivitiesDisposable?.dispose() self.recentlyUsedInlineBotsDisposable?.dispose() self.unpinMessageDisposable?.dispose() - self.typingActivityDisposable?.dispose() + self.inputActivityDisposable?.dispose() + self.recordingActivityDisposable?.dispose() self.presentationDataDisposable?.dispose() self.searchDisposable?.dispose() + self.applicationInForegroundDisposable?.dispose() + self.canReadHistoryDisposable?.dispose() + self.networkStateDisposable?.dispose() } var chatDisplayNode: ChatControllerNode { @@ -976,7 +1017,7 @@ public class ChatController: TelegramController { } override public func loadDisplayNode() { - self.displayNode = ChatControllerNode(account: self.account, peerId: self.peerId, messageId: self.messageId, controllerInteraction: self.controllerInteraction!, chatPresentationInterfaceState: self.presentationInterfaceState, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, navigationBar: self.navigationBar!) + self.displayNode = ChatControllerNode(account: self.account, chatLocation: self.chatLocation, messageId: self.messageId, controllerInteraction: self.controllerInteraction!, chatPresentationInterfaceState: self.presentationInterfaceState, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, navigationBar: self.navigationBar!) let initialData = self.chatDisplayNode.historyNode.initialData |> take(1) @@ -1038,15 +1079,17 @@ public class ChatController: TelegramController { }) }) } - if let readStateData = combinedInitialData.readStateData, let notificationSettings = readStateData.notificationSettings { - var globalRemainingUnreadCount = readStateData.totalUnreadCount - if !notificationSettings.isRemovedFromTotalUnreadCount { - globalRemainingUnreadCount -= readStateData.unreadCount - } - if globalRemainingUnreadCount > 0 { - strongSelf.navigationItem.badge = "\(globalRemainingUnreadCount)" - } else { - strongSelf.navigationItem.badge = "" + if let readStateData = combinedInitialData.readStateData { + if case let .peer(peerId) = strongSelf.chatLocation, let peerReadStateData = readStateData[peerId], let notificationSettings = peerReadStateData.notificationSettings { + var globalRemainingUnreadCount = peerReadStateData.totalUnreadCount + if !notificationSettings.isRemovedFromTotalUnreadCount { + globalRemainingUnreadCount -= peerReadStateData.unreadCount + } + if globalRemainingUnreadCount > 0 { + strongSelf.navigationItem.badge = "\(globalRemainingUnreadCount)" + } else { + strongSelf.navigationItem.badge = "" + } } } } @@ -1141,14 +1184,14 @@ public class ChatController: TelegramController { self.historyStateDisposable = self.chatDisplayNode.historyNode.historyState.get().start(next: { [weak self] state in if let strongSelf = self { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: strongSelf.isViewLoaded && strongSelf.view.window != nil, { $0.updatedChatHistoryState(state) }) } }) - self.ready.set(combineLatest(self.chatDisplayNode.historyNode.historyState.get(), self._peerReady.get(), initialData) |> map { _, peerReady, _ in - return peerReady + self.ready.set(combineLatest(self.chatDisplayNode.historyNode.historyState.get(), self._chatLocationInfoReady.get(), initialData) |> map { _, chatLocationInfoReady, _ in + return chatLocationInfoReady }) self.chatDisplayNode.historyNode.contentPositionChanged = { [weak self] offset in @@ -1171,8 +1214,8 @@ public class ChatController: TelegramController { } } - self.chatDisplayNode.historyNode.scrolledToIndex = { [weak self] index in - if let strongSelf = self { + self.chatDisplayNode.historyNode.scrolledToIndex = { [weak self] toIndex in + if let strongSelf = self, case let .message(index) = toIndex { if let controllerInteraction = strongSelf.controllerInteraction { if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(index.id) { let highlightedState = ChatInterfaceHighlightedState(messageStableId: message.stableId) @@ -1208,7 +1251,7 @@ public class ChatController: TelegramController { if let strongSelf = self { var mappedTransition: (ChatHistoryListViewTransition, ListViewUpdateSizeAndInsets?)? - strongSelf.chatDisplayNode.containerLayoutUpdated(strongSelf.containerLayout, navigationBarHeight: strongSelf.navigationHeight, transition: .animated(duration: 0.4, curve: .spring), listViewTransaction: { updateSizeAndInsets in + strongSelf.chatDisplayNode.containerLayoutUpdated(strongSelf.containerLayout, navigationBarHeight: strongSelf.navigationHeight, transition: .animated(duration: 0.4, curve: .spring), listViewTransaction: { updateSizeAndInsets, _, _ in var options = transition.options let _ = options.insert(.Synchronous) let _ = options.insert(.LowLatency) @@ -1252,11 +1295,18 @@ public class ChatController: TelegramController { } self.chatDisplayNode.displayAttachmentMenu = { [weak self] in - if let strongSelf = self, let peer = strongSelf.presentationInterfaceState.peer { - if true { + guard let strongSelf = self else { + return + } + let _ = (strongSelf.account.postbox.modify { modifier -> GeneratedMediaStoreSettings in + let entry = modifier.getPreferencesEntry(key: ApplicationSpecificPreferencesKeys.generatedMediaStoreSettings) as? GeneratedMediaStoreSettings + return entry ?? GeneratedMediaStoreSettings.defaultSettings + } + |> deliverOnMainQueue).start(next: { [weak self] settings in + if let strongSelf = self, let peer = strongSelf.presentationInterfaceState.peer { strongSelf.chatDisplayNode.dismissInput() - let legacyController = LegacyController(presentation: .custom) + let legacyController = LegacyController(presentation: .custom, theme: strongSelf.presentationData.theme) legacyController.statusBar.statusBarStyle = .Ignore let emptyController = LegacyEmptyController(context: legacyController.context)! @@ -1264,7 +1314,7 @@ public class ChatController: TelegramController { navigationController.setNavigationBarHidden(true, animated: false) legacyController.bind(controller: navigationController) - let controller = legacyAttachmentMenu(account: strongSelf.account, peer: peer, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, parentController: legacyController, recentlyUsedInlineBots: strongSelf.recentlyUsedInlineBotsValue, openGallery: { + let controller = legacyAttachmentMenu(account: strongSelf.account, peer: peer, saveEditedPhotos: settings.storeEditedPhotos, allowGrouping: true, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, parentController: legacyController, recentlyUsedInlineBots: strongSelf.recentlyUsedInlineBotsValue, openGallery: { self?.presentMediaPicker(fileMode: false) }, openCamera: { cameraView, menuController in if let strongSelf = self, let peer = strongSelf.presentationInterfaceState.peer { @@ -1278,7 +1328,7 @@ public class ChatController: TelegramController { self?.presentMapPicker() }, openContacts: { if let strongSelf = self { - let contactsController = ContactSelectionController(account: strongSelf.account, title: { $0.DialogList_SelectContact }) + let contactsController = ContactSelectionController(account: strongSelf.account, title: { $0.Contacts_Title }) strongSelf.chatDisplayNode.dismissInput() strongSelf.present(contactsController, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) strongSelf.controllerNavigationDisposable.set((contactsController.result |> deliverOnMainQueue).start(next: { peerId in @@ -1296,7 +1346,7 @@ public class ChatController: TelegramController { }) } }) - let message = EnqueueMessage.message(text: "", attributes: [], media: media, replyToMessageId: replyMessageId) + let message = EnqueueMessage.message(text: "", attributes: [], media: media, replyToMessageId: replyMessageId, localGroupingKey: nil) strongSelf.sendMessages([message]) } })) @@ -1323,10 +1373,25 @@ public class ChatController: TelegramController { strongSelf.present(legacyController, in: .window(.root)) controller.present(in: emptyController, sourceView: nil, animated: true) - - return } - } + }) + } + + let postbox = self.account.postbox + self.chatDisplayNode.displayPasteMenu = { [weak self] images in + let _ = (postbox.modify { modifier -> GeneratedMediaStoreSettings in + let entry = modifier.getPreferencesEntry(key: ApplicationSpecificPreferencesKeys.generatedMediaStoreSettings) as? GeneratedMediaStoreSettings + return entry ?? GeneratedMediaStoreSettings.defaultSettings + } + |> deliverOnMainQueue).start(next: { [weak self] settings in + if let strongSelf = self, let peer = strongSelf.presentationInterfaceState.peer { + let controller = legacyPasteMenu(account: strongSelf.account, peer: peer, saveEditedPhotos: settings.storeEditedPhotos, allowGrouping: true, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, images: images, sendMessagesWithSignals: { signals in + self?.enqueueMediaMessages(signals: signals) + }) + strongSelf.chatDisplayNode.dismissInput() + strongSelf.present(controller, in: .window(.root)) + } + }) } self.chatDisplayNode.updateTypingActivity = { [weak self] in @@ -1337,12 +1402,22 @@ public class ChatController: TelegramController { self.chatDisplayNode.dismissUrlPreview = { [weak self] in if let strongSelf = self { - if let (link, _) = strongSelf.presentationInterfaceState.urlPreview { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { - $0.updatedInterfaceState { - $0.withUpdatedComposeDisableUrlPreview(link) - } - }) + if let _ = strongSelf.presentationInterfaceState.interfaceState.editMessage { + if let (link, _) = strongSelf.presentationInterfaceState.editingUrlPreview { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { + $0.updatedInterfaceState { + $0.withUpdatedEditMessage($0.editMessage.flatMap { $0.withUpdatedDisableUrlPreview(link) }) + } + }) + } + } else { + if let (link, _) = strongSelf.presentationInterfaceState.urlPreview { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { + $0.updatedInterfaceState { + $0.withUpdatedComposeDisableUrlPreview(link) + } + }) + } } } } @@ -1358,8 +1433,8 @@ public class ChatController: TelegramController { } self.chatDisplayNode.navigateButtons.mentionsPressed = { [weak self] in - if let strongSelf = self, strongSelf.isNodeLoaded { - let signal = earliestUnseenPersonalMentionMessage(postbox: strongSelf.account.postbox, network: strongSelf.account.network, peerId: strongSelf.peerId) + if let strongSelf = self, strongSelf.isNodeLoaded, case let .peer(peerId) = strongSelf.chatLocation { + let signal = earliestUnseenPersonalMentionMessage(postbox: strongSelf.account.postbox, network: strongSelf.account.network, peerId: peerId) strongSelf.navigationActionDisposable.set((signal |> deliverOnMainQueue).start(next: { result in if let strongSelf = self { switch result { @@ -1385,20 +1460,32 @@ public class ChatController: TelegramController { }, setupEditMessage: { [weak self] messageId in if let strongSelf = self, strongSelf.isNodeLoaded { if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withUpdatedEditMessage(ChatEditMessageState(messageId: messageId, inputState: ChatTextInputState(inputText: message.text))) } }) - strongSelf.chatDisplayNode.ensureInputViewFocused() + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in + var updated = state.updatedInterfaceState { + return $0.withUpdatedEditMessage(ChatEditMessageState(messageId: messageId, inputState: ChatTextInputState(inputText: message.text), disableUrlPreview: nil)) + } + updated = updated.updatedInputMode({ _ in + return .text + }) + for media in message.media { + if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { + updated = updated.updatedEditingUrlPreview((content.url, webpage)) + break + } + } + return updated + }) + //strongSelf.chatDisplayNode.ensureInputViewFocused() } } - }, beginMessageSelection: { [weak self] messageId in + }, beginMessageSelection: { [weak self] messageIds in if let strongSelf = self, strongSelf.isNodeLoaded { - if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true,{ $0.updatedInterfaceState { $0.withUpdatedSelectedMessage(message.id) } }) - } + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true,{ $0.updatedInterfaceState { $0.withUpdatedSelectedMessages(messageIds) } }) } }, deleteSelectedMessages: { [weak self] in if let strongSelf = self { if let messageIds = strongSelf.presentationInterfaceState.interfaceState.selectionState?.selectedIds, !messageIds.isEmpty { - strongSelf.messageContextDisposable.set((chatDeleteMessagesOptions(account: strongSelf.account, messageIds: messageIds) |> deliverOnMainQueue).start(next: { options in + strongSelf.messageContextDisposable.set((chatDeleteMessagesOptions(postbox: strongSelf.account.postbox, accountPeerId: strongSelf.account.peerId, messageIds: messageIds) |> deliverOnMainQueue).start(next: { options in if let strongSelf = self, !options.isEmpty { let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) var items: [ActionSheetItem] = [] @@ -1454,9 +1541,15 @@ public class ChatController: TelegramController { let controller = PeerSelectionController(account: strongSelf.account) controller.peerSelected = { [weak controller] peerId in if let strongSelf = self, let strongController = controller { - if peerId == strongSelf.peerId { + if case .peer(peerId) = strongSelf.chatLocation { strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedForwardMessageIds(forwardMessageIds).withoutSelectionState() }) }) strongController.dismiss() + } else if peerId == strongSelf.account.peerId { + let _ = enqueueMessages(account: strongSelf.account, peerId: peerId, messages: forwardMessageIds.map { id -> EnqueueMessage in + return .forward(source: id, grouping: .auto) + }).start() + strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withoutSelectionState() }) }) + strongController.dismiss() } else { let _ = (strongSelf.account.postbox.modify({ modifier -> Void in modifier.updatePeerChatInterfaceState(peerId, update: { currentState in @@ -1478,7 +1571,7 @@ public class ChatController: TelegramController { } })) - (strongSelf.navigationController as? NavigationController)?.replaceTopController(ChatController(account: strongSelf.account, peerId: peerId), animated: false, ready: ready) + (strongSelf.navigationController as? NavigationController)?.replaceTopController(ChatController(account: strongSelf.account, chatLocation: .peer(peerId)), animated: false, ready: ready) } }) } @@ -1488,6 +1581,28 @@ public class ChatController: TelegramController { strongSelf.present(controller, in: .window(.root)) } } + }, shareSelectedMessages: { [weak self] in + if let strongSelf = self, let selectedIds = strongSelf.presentationInterfaceState.interfaceState.selectionState?.selectedIds, !selectedIds.isEmpty { + let _ = (strongSelf.account.postbox.modify { modifier -> [Message] in + var messages: [Message] = [] + for id in selectedIds { + if let message = modifier.getMessage(id) { + messages.append(message) + } + } + return messages + } |> deliverOnMainQueue).start(next: { messages in + if let strongSelf = self, !messages.isEmpty { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ $0.withoutSelectionState() }) }) + + let shareController = ShareController(account: strongSelf.account, subject: .messages(messages.sorted(by: { lhs, rhs in + return MessageIndex(lhs) < MessageIndex(rhs) + })), externalShare: true, immediateExternalShare: true) + strongSelf.chatDisplayNode.dismissInput() + strongSelf.present(shareController, in: .window(.root)) + } + }) + } }, updateTextInputState: { [weak self] f in if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withUpdatedEffectiveInputState(f($0.effectiveInputState)) } }) @@ -1499,11 +1614,18 @@ public class ChatController: TelegramController { return $0.updatedInputMode({ _ in return updatedInputMode }).updatedInterfaceState({ $0.withUpdatedMessageActionsState({ $0.withUpdatedClosedButtonKeyboardMessageId(updatedClosedButtonKeyboardMessageId) }) }) }) } - }, editMessage: { [weak self] messageId, text in - if let strongSelf = self { + }, editMessage: { [weak self] in + if let strongSelf = self, let editMessage = strongSelf.presentationInterfaceState.interfaceState.editMessage { + var disableUrlPreview = false + if let (link, _) = strongSelf.presentationInterfaceState.editingUrlPreview { + if editMessage.disableUrlPreview == link { + disableUrlPreview = true + } + } + let editingMessage = strongSelf.editingMessage editingMessage.set(true) - strongSelf.editMessageDisposable.set((requestEditMessage(account: strongSelf.account, messageId: messageId, text: text) |> deliverOnMainQueue |> afterDisposed({ + strongSelf.editMessageDisposable.set((requestEditMessage(account: strongSelf.account, messageId: editMessage.messageId, text: editMessage.inputState.inputText, disableUrlPreview: disableUrlPreview) |> deliverOnMainQueue |> afterDisposed({ editingMessage.set(false) })).start(completed: { if let strongSelf = self { @@ -1511,7 +1633,7 @@ public class ChatController: TelegramController { } })) } - }, beginMessageSearch: { [weak self] in + }, beginMessageSearch: { [weak self] domain in if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { current in return current.updatedTitlePanelContext { @@ -1529,7 +1651,7 @@ public class ChatController: TelegramController { } else { return $0 } - }.updatedSearch(current.search == nil ? ChatSearchData() : current.search) + }.updatedSearch(current.search == nil ? ChatSearchData(domain: domain) : current.search?.withUpdatedDomain(domain).withUpdatedQuery("")) }) } }, dismissMessageSearch: { [weak self] in @@ -1540,67 +1662,13 @@ public class ChatController: TelegramController { } }, updateMessageSearch: { [weak self] query in if let strongSelf = self { - var begin = false strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { current in - if let data = current.search, data.query != query { - begin = true + if let data = current.search { return current.updatedSearch(data.withUpdatedQuery(query)) } else { return current } }) - if begin { - if query.isEmpty { - strongSelf.searching.set(false) - strongSelf.searchDisposable?.set(nil) - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { current in - if let data = current.search { - return current.updatedSearch(data.withUpdatedResultsState(nil)) - } else { - return current - } - }) - } else { - strongSelf.searching.set(true) - let searchDisposable: MetaDisposable - if let current = strongSelf.searchDisposable { - searchDisposable = current - } else { - searchDisposable = MetaDisposable() - strongSelf.searchDisposable = searchDisposable - } - searchDisposable.set((searchMessages(account: strongSelf.account, peerId: strongSelf.peerId, query: query) |> deliverOnMainQueue).start(next: { results in - if let strongSelf = self { - var navigateId: MessageId? - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { current in - if let data = current.search { - let messageIds = results.map({ $0.id }).sorted() - var currentId = messageIds.last - if let previousResultId = data.resultsState?.currentId { - for id in messageIds { - if id >= previousResultId { - currentId = id - break - } - } - } - navigateId = currentId - return current.updatedSearch(data.withUpdatedResultsState(ChatSearchResultsState(messageIds: messageIds, currentId: currentId))) - } else { - return current - } - }) - if let navigateId = navigateId { - strongSelf.navigateToMessage(from: nil, to: navigateId) - } - } - }, completed: { - if let strongSelf = self { - strongSelf.searching.set(false) - } - })) - } - } } }, navigateMessageSearch: { [weak self] action in if let strongSelf = self { @@ -1632,13 +1700,13 @@ public class ChatController: TelegramController { } } }, openCalendarSearch: { [weak self] in - if let strongSelf = self { + if let strongSelf = self, case let .peer(peerId) = strongSelf.chatLocation { strongSelf.chatDisplayNode.dismissInput() let controller = ChatDateSelectionSheet(theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, completion: { timestamp in if let strongSelf = self { strongSelf.loadingMessage.set(true) - strongSelf.messageIndexDisposable.set((searchMessageIdByTimestamp(account: strongSelf.account, peerId: strongSelf.peerId, timestamp: timestamp) |> deliverOnMainQueue).start(next: { messageId in + strongSelf.messageIndexDisposable.set((searchMessageIdByTimestamp(account: strongSelf.account, peerId: peerId, timestamp: timestamp) |> deliverOnMainQueue).start(next: { messageId in if let strongSelf = self { strongSelf.loadingMessage.set(false) if let messageId = messageId { @@ -1650,13 +1718,32 @@ public class ChatController: TelegramController { }) strongSelf.present(controller, in: .window(.root)) } + }, toggleMembersSearch: { [weak self] value in + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in + if value { + return state.updatedSearch(ChatSearchData(query: "", domain: .members, domainSuggestionContext: .none, resultsState: nil)) + } else if let search = state.search { + switch search.domain { + case .everything: + return state + case .members: + return state.updatedSearch(ChatSearchData(query: "", domain: .everything, domainSuggestionContext: .none, resultsState: nil)) + case .member: + return state.updatedSearch(ChatSearchData(query: "", domain: .members, domainSuggestionContext: .none, resultsState: nil)) + } + } else { + return state + } + }) + } }, navigateToMessage: { [weak self] messageId in self?.navigateToMessage(from: nil, to: messageId) }, openPeerInfo: { [weak self] in self?.navigationButtonAction(.openChatInfo) }, togglePeerNotifications: { [weak self] in - if let strongSelf = self { - let _ = togglePeerMuted(account: strongSelf.account, peerId: strongSelf.peerId).start() + if let strongSelf = self, case let .peer(peerId) = strongSelf.chatLocation { + let _ = togglePeerMuted(account: strongSelf.account, peerId: peerId).start() } }, sendContextResult: { [weak self] results, result in self?.enqueueChatContextResult(results, result) @@ -1678,11 +1765,11 @@ public class ChatController: TelegramController { } }) var attributes: [MessageAttribute] = [] - let entities = generateTextEntities(messageText) + let entities = generateTextEntities(messageText, enabledTypes: .all) if !entities.isEmpty { attributes.append(TextEntitiesMessageAttribute(entities: entities)) } - strongSelf.sendMessages([.message(text: messageText, attributes: attributes, media: nil, replyToMessageId: replyMessageId)]) + strongSelf.sendMessages([.message(text: messageText, attributes: attributes, media: nil, replyToMessageId: replyMessageId, localGroupingKey: nil)]) } } }, sendBotStart: { [weak self] payload in @@ -1690,44 +1777,57 @@ public class ChatController: TelegramController { strongSelf.startBot(payload) } }, botSwitchChatWithPayload: { [weak self] peerId, payload in - if let strongSelf = self { - strongSelf.openPeer(peerId: peerId, navigation: .withBotStartPayload(ChatControllerInitialBotStart(payload: payload, behavior: .automatic(returnToPeerId: strongSelf.peerId))), fromMessageId: nil) + if let strongSelf = self, case let .peer(currentPeerId) = strongSelf.chatLocation { + strongSelf.openPeer(peerId: peerId, navigation: .withBotStartPayload(ChatControllerInitialBotStart(payload: payload, behavior: .automatic(returnToPeerId: currentPeerId))), fromMessageId: nil) } }, beginMediaRecording: { [weak self] isVideo in if let strongSelf = self { if isVideo { strongSelf.requestVideoRecorder() } else { - strongSelf.requestAudioRecorder() + strongSelf.requestAudioRecorder(beginWithTone: false) } } - }, finishMediaRecording: { [weak self] sendMedia in - self?.dismissMediaRecorder(sendMedia: sendMedia) + }, finishMediaRecording: { [weak self] action in + self?.dismissMediaRecorder(action) }, stopMediaRecording: { [weak self] in self?.stopMediaRecorder() }, lockMediaRecording: { [weak self] in self?.lockMediaRecorder() + }, deleteRecordedMedia: { [weak self] in + self?.deleteMediaRecording() + }, sendRecordedMedia: { [weak self] in + self?.sendMediaRecording() }, switchMediaRecordingMode: { [weak self] in - self?.updateChatPresentationInterfaceState(interactive: true, { - return $0.updatedInterfaceState { current in - let mode: ChatTextInputMediaRecordingButtonMode - switch current.mediaRecordingMode { - case .audio: - mode = .video - case .video: - mode = .audio - } - return current.withUpdatedMediaRecordingMode(mode) + if let strongSelf = self { + if strongSelf.recordingModeFeedback == nil { + strongSelf.recordingModeFeedback = HapticFeedback() + strongSelf.recordingModeFeedback?.prepareImpact() } - }) + + strongSelf.recordingModeFeedback?.impact() + + strongSelf.updateChatPresentationInterfaceState(interactive: true, { + return $0.updatedInterfaceState { current in + let mode: ChatTextInputMediaRecordingButtonMode + switch current.mediaRecordingMode { + case .audio: + mode = .video + case .video: + mode = .audio + } + return current.withUpdatedMediaRecordingMode(mode) + } + }) + } }, setupMessageAutoremoveTimeout: { [weak self] in - if let strongSelf = self, strongSelf.peerId.namespace == Namespaces.Peer.SecretChat { + if let strongSelf = self, case let .peer(peerId) = strongSelf.chatLocation, peerId.namespace == Namespaces.Peer.SecretChat { strongSelf.chatDisplayNode.dismissInput() if let peer = strongSelf.presentationInterfaceState.peer as? TelegramSecretChat { let controller = ChatSecretAutoremoveTimerActionSheetController(theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, currentValue: peer.messageAutoremoveTimeout == nil ? 0 : peer.messageAutoremoveTimeout!, applyValue: { value in if let strongSelf = self { - let _ = setSecretChatMessageAutoremoveTimeoutInteractively(account: strongSelf.account, peerId: strongSelf.peerId, timeout: value == 0 ? nil : value).start() + let _ = setSecretChatMessageAutoremoveTimeoutInteractively(account: strongSelf.account, peerId: peer.id, timeout: value == 0 ? nil : value).start() } }) strongSelf.present(controller, in: .window(.root)) @@ -1742,15 +1842,22 @@ public class ChatController: TelegramController { }) } }) - strongSelf.sendMessages([.message(text: "", attributes: [], media: file, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId)]) + strongSelf.sendMessages([.message(text: "", attributes: [], media: file, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil)]) } }, unblockPeer: { [weak self] in self?.unblockPeer() }, pinMessage: { [weak self] messageId in - if let strongSelf = self { + if let strongSelf = self, case let .peer(currentPeerId) = strongSelf.chatLocation { if let peer = strongSelf.presentationInterfaceState.peer { if let channel = peer as? TelegramChannel { - if channel.hasAdminRights([.canPinMessages]) { + var canManagePin = false + if case .broadcast = channel.info { + canManagePin = channel.hasAdminRights([.canEditMessages]) + } else { + canManagePin = channel.hasAdminRights([.canPinMessages]) + } + + if canManagePin { let pinAction: (Bool) -> Void = { notify in if let strongSelf = self { let disposable: MetaDisposable @@ -1760,14 +1867,18 @@ public class ChatController: TelegramController { disposable = MetaDisposable() strongSelf.unpinMessageDisposable = disposable } - disposable.set(requestUpdatePinnedMessage(account: strongSelf.account, peerId: strongSelf.peerId, update: .pin(id: messageId, silent: !notify)).start()) + disposable.set(requestUpdatePinnedMessage(account: strongSelf.account, peerId: currentPeerId, update: .pin(id: messageId, silent: !notify)).start()) } } - strongSelf.present(standardTextAlertController(title: nil, text: strongSelf.presentationData.strings.Conversation_PinMessageAlertGroup, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Conversation_PinMessageAlert_OnlyPin, action: { - pinAction(false) - }), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Yes, action: { + if case .broadcast = channel.info { pinAction(true) - })]), in: .window(.root)) + } else { + strongSelf.present(standardTextAlertController(title: nil, text: strongSelf.presentationData.strings.Conversation_PinMessageAlertGroup, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Conversation_PinMessageAlert_OnlyPin, action: { + pinAction(false) + }), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Yes, action: { + pinAction(true) + })]), in: .window(.root)) + } } else { if let pinnedMessageId = strongSelf.presentationInterfaceState.pinnedMessage?.id { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { @@ -1782,7 +1893,14 @@ public class ChatController: TelegramController { if let strongSelf = self { if let peer = strongSelf.presentationInterfaceState.peer { if let channel = peer as? TelegramChannel { - if channel.hasAdminRights([.canPinMessages]) { + var canManagePin = false + if case .broadcast = channel.info { + canManagePin = channel.hasAdminRights([.canEditMessages]) + } else { + canManagePin = channel.hasAdminRights([.canPinMessages]) + } + + if canManagePin { strongSelf.present(standardTextAlertController(title: nil, text: strongSelf.presentationData.strings.Conversation_UnpinMessageAlert, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_No, action: {}), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Yes, action: { if let strongSelf = self { let disposable: MetaDisposable @@ -1792,7 +1910,7 @@ public class ChatController: TelegramController { disposable = MetaDisposable() strongSelf.unpinMessageDisposable = disposable } - disposable.set(requestUpdatePinnedMessage(account: strongSelf.account, peerId: strongSelf.peerId, update: .clear).start()) + disposable.set(requestUpdatePinnedMessage(account: strongSelf.account, peerId: peer.id, update: .clear).start()) } })]), in: .window(.root)) } else { @@ -1812,8 +1930,8 @@ public class ChatController: TelegramController { }, deleteChat: { [weak self] in self?.deleteChat(reportChatSpam: false) }, beginCall: { [weak self] in - if let strongSelf = self { - strongSelf.controllerInteraction?.callPeer(strongSelf.peerId) + if let strongSelf = self, case let .peer(peerId) = strongSelf.chatLocation { + strongSelf.controllerInteraction?.callPeer(peerId) } }, toggleMessageStickerStarred: { [weak self] messageId in if let strongSelf = self, let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) { @@ -1839,96 +1957,153 @@ public class ChatController: TelegramController { } |> switchToLatest).start() } } - }, presentController: { [weak self] controller in - self?.present(controller, in: .window(.root)) + }, presentController: { [weak self] controller, arguments in + self?.present(controller, in: .window(.root), with: arguments) + }, navigateFeed: { [weak self] in + if let strongSelf = self { + strongSelf.chatDisplayNode.historyNode.scrollToNextMessage() + } }, statuses: ChatPanelInterfaceInteractionStatuses(editingMessage: self.editingMessage.get(), startingBot: self.startingBot.get(), unblockingPeer: self.unblockingPeer.get(), searching: self.searching.get(), loadingMessage: self.loadingMessage.get())) - let unreadCountsKey: PostboxViewKey = .unreadCounts(items: [.peer(self.peerId), .total]) - let notificationSettingsKey: PostboxViewKey = .peerNotificationSettings(peerId: self.peerId) - self.chatUnreadCountDisposable = (self.account.postbox.combinedView(keys: [unreadCountsKey, notificationSettingsKey]) |> deliverOnMainQueue).start(next: { [weak self] views in - if let strongSelf = self { - var unreadCount: Int32 = 0 - var totalCount: Int32 = 0 - - if let view = views.views[unreadCountsKey] as? UnreadMessageCountsView { - if let count = view.count(for: .peer(strongSelf.peerId)) { - unreadCount = count - } - if let count = view.count(for: .total) { - totalCount = count - } - } - - strongSelf.chatDisplayNode.navigateButtons.unreadCount = unreadCount - - if let view = views.views[notificationSettingsKey] as? PeerNotificationSettingsView, let notificationSettings = view.notificationSettings { - var globalRemainingUnreadCount = totalCount - if !notificationSettings.isRemovedFromTotalUnreadCount { - globalRemainingUnreadCount -= unreadCount - } - - if globalRemainingUnreadCount > 0 { - strongSelf.navigationItem.badge = "\(globalRemainingUnreadCount)" - } else { - strongSelf.navigationItem.badge = "" - } - } - } - }) - - self.chatUnreadMentionCountDisposable = (self.account.viewTracker.unseenPersonalMessagesCount(peerId: self.peerId) |> deliverOnMainQueue).start(next: { [weak self] count in - if let strongSelf = self { - strongSelf.chatDisplayNode.navigateButtons.mentionCount = count - } - }) - - let postbox = self.account.postbox - let previousPeerCache = Atomic<[PeerId: Peer]>(value: [:]) - self.peerInputActivitiesDisposable = (self.account.peerInputActivities(peerId: peerId) - |> mapToSignal { activities -> Signal<[(Peer, PeerInputActivity)], NoError> in - var foundAllPeers = true - var cachedResult: [(Peer, PeerInputActivity)] = [] - previousPeerCache.with { dict -> Void in - for (peerId, activity) in activities { - if let peer = dict[peerId] { - cachedResult.append((peer, activity)) - } else { - foundAllPeers = false - break - } - } - } - if foundAllPeers { - return .single(cachedResult) - } else { - return postbox.modify { modifier -> [(Peer, PeerInputActivity)] in - var result: [(Peer, PeerInputActivity)] = [] - var peerCache: [PeerId: Peer] = [:] - for (peerId, activity) in activities { - if let peer = modifier.getPeer(peerId) { - result.append((peer, activity)) - peerCache[peerId] = peer + switch self.chatLocation { + case let .peer(peerId): + let unreadCountsKey: PostboxViewKey = .unreadCounts(items: [.peer(peerId), .total]) + let notificationSettingsKey: PostboxViewKey = .peerNotificationSettings(peerId: peerId) + self.chatUnreadCountDisposable = (self.account.postbox.combinedView(keys: [unreadCountsKey, notificationSettingsKey]) |> deliverOnMainQueue).start(next: { [weak self] views in + if let strongSelf = self { + var unreadCount: Int32 = 0 + var totalCount: Int32 = 0 + + if let view = views.views[unreadCountsKey] as? UnreadMessageCountsView { + if let count = view.count(for: .peer(peerId)) { + unreadCount = count + } + if let count = view.count(for: .total) { + totalCount = count + } + } + + strongSelf.chatDisplayNode.navigateButtons.unreadCount = unreadCount + + if let view = views.views[notificationSettingsKey] as? PeerNotificationSettingsView, let notificationSettings = view.notificationSettings { + var globalRemainingUnreadCount = totalCount + if !notificationSettings.isRemovedFromTotalUnreadCount { + globalRemainingUnreadCount -= unreadCount + } + + if globalRemainingUnreadCount > 0 { + strongSelf.navigationItem.badge = "\(globalRemainingUnreadCount)" + } else { + strongSelf.navigationItem.badge = "" } } - let _ = previousPeerCache.swap(peerCache) - return result } - } - } - |> deliverOnMainQueue).start(next: { [weak self] activities in - if let strongSelf = self { - strongSelf.chatTitleView?.inputActivities = (strongSelf.peerId, activities) - } - }) + }) + + self.chatUnreadMentionCountDisposable = (self.account.viewTracker.unseenPersonalMessagesCount(peerId: peerId) |> deliverOnMainQueue).start(next: { [weak self] count in + if let strongSelf = self { + strongSelf.chatDisplayNode.navigateButtons.mentionCount = count + } + }) + + let postbox = self.account.postbox + let previousPeerCache = Atomic<[PeerId: Peer]>(value: [:]) + self.peerInputActivitiesDisposable = (self.account.peerInputActivities(peerId: peerId) + |> mapToSignal { activities -> Signal<[(Peer, PeerInputActivity)], NoError> in + var foundAllPeers = true + var cachedResult: [(Peer, PeerInputActivity)] = [] + previousPeerCache.with { dict -> Void in + for (peerId, activity) in activities { + if let peer = dict[peerId] { + cachedResult.append((peer, activity)) + } else { + foundAllPeers = false + break + } + } + } + if foundAllPeers { + return .single(cachedResult) + } else { + return postbox.modify { modifier -> [(Peer, PeerInputActivity)] in + var result: [(Peer, PeerInputActivity)] = [] + var peerCache: [PeerId: Peer] = [:] + for (peerId, activity) in activities { + if let peer = modifier.getPeer(peerId) { + result.append((peer, activity)) + peerCache[peerId] = peer + } + } + let _ = previousPeerCache.swap(peerCache) + return result + } + } + } + |> deliverOnMainQueue).start(next: { [weak self] activities in + if let strongSelf = self { + strongSelf.chatTitleView?.inputActivities = (peerId, activities) + } + }) + + self.sentMessageEventsDisposable.set(self.account.pendingMessageManager.deliveredMessageEvents(peerId: peerId).start(next: { _ in + serviceSoundManager.playMessageDeliveredSound() + })) + case let .group(groupId): + let unreadCountsKey: PostboxViewKey = .unreadCounts(items: [.group(groupId), .total]) + //let notificationSettingsKey: PostboxViewKey = .peerNotificationSettings(peerId: peerId) + self.chatUnreadCountDisposable = (self.account.postbox.combinedView(keys: [unreadCountsKey]) |> deliverOnMainQueue).start(next: { [weak self] views in + if let strongSelf = self { + var unreadCount: Int32 = 0 + var totalCount: Int32 = 0 + + if let view = views.views[unreadCountsKey] as? UnreadMessageCountsView { + if let count = view.count(for: .group(groupId)) { + unreadCount = count + } + if let count = view.count(for: .total) { + totalCount = count + } + } + + strongSelf.chatDisplayNode.navigateButtons.unreadCount = unreadCount + } + }) + } self.interfaceInteraction = interfaceInteraction self.chatDisplayNode.interfaceInteraction = interfaceInteraction - self.displayNodeDidLoad() - - self.sentMessageEventsDisposable.set(self.account.pendingMessageManager.deliveredMessageEvents(peerId: self.peerId).start(next: { _ in - serviceSoundManager.playMessageDeliveredSound() + self.galleryHiddenMesageAndMediaDisposable.set(self.account.telegramApplicationContext.mediaManager.galleryHiddenMediaManager.hiddenIds().start(next: { [weak self] ids in + if let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction { + var messageIdAndMedia: [MessageId: [Media]] = [:] + + for id in ids { + if case let .chat(messageId, media) = id { + messageIdAndMedia[messageId] = [media] + } + } + + //if controllerInteraction.hiddenMedia != messageIdAndMedia { + controllerInteraction.hiddenMedia = messageIdAndMedia + + strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ChatMessageItemView { + itemNode.updateHiddenMedia() + } + } + //} + } })) + + self.chatDisplayNode.dismissAsOverlay = { [weak self] in + if let strongSelf = self { + strongSelf.chatDisplayNode.animateDismissAsOverlay(completion: { + self?.presentingViewController?.dismiss(animated: false, completion: nil) + }) + } + } + + self.displayNodeDidLoad() } override public func viewWillAppear(_ animated: Bool) { @@ -1948,16 +2123,45 @@ public class ChatController: TelegramController { self.recentlyUsedInlineBotsDisposable = (recentlyUsedInlineBots(postbox: self.account.postbox) |> deliverOnMainQueue).start(next: { [weak self] peers in self?.recentlyUsedInlineBotsValue = peers.filter({ $0.1 >= 0.14 }).map({ $0.0 }) }) + + if self.raiseToListen == nil { + self.raiseToListen = RaiseToListenManager(shouldActivate: { [weak self] in + if let strongSelf = self, strongSelf.isNodeLoaded && strongSelf.canReadHistoryValue, strongSelf.presentationInterfaceState.interfaceState.editMessage == nil, strongSelf.playlistStateAndType == nil { + if strongSelf.firstLoadedMessageToListen() != nil || strongSelf.chatDisplayNode.isTextInputPanelActive { + return true + } + } + return false + }, activate: { [weak self] in + self?.activateRaiseGesture() + }, deactivate: { [weak self] in + self?.deactivateRaiseGesture() + }) + self.raiseToListen?.enabled = self.canReadHistoryValue + } + + if let arguments = self.presentationArguments as? ChatControllerOverlayPresentationData { + //TODO clear arguments + self.chatDisplayNode.animateInAsOverlay(from: arguments.expandData.0, completion: { + arguments.expandData.1() + }) + } } override public func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) self.chatDisplayNode.historyNode.canReadHistory.set(.single(false)) - let timestamp = Int32(Date().timeIntervalSince1970) - let scrollState = self.chatDisplayNode.historyNode.immediateScrollState() - let interfaceState = self.presentationInterfaceState.interfaceState.withUpdatedTimestamp(timestamp).withUpdatedHistoryScrollState(scrollState) - let _ = updatePeerChatInterfaceState(account: account, peerId: self.peerId, state: interfaceState).start() + self.saveInterfaceState() + } + + private func saveInterfaceState() { + if case let .peer(peerId) = self.chatLocation { + let timestamp = Int32(Date().timeIntervalSince1970) + let scrollState = self.chatDisplayNode.historyNode.immediateScrollState() + let interfaceState = self.presentationInterfaceState.interfaceState.withUpdatedTimestamp(timestamp).withUpdatedHistoryScrollState(scrollState) + let _ = updatePeerChatInterfaceState(account: account, peerId: peerId, state: interfaceState).start() + } } override public func viewDidDisappear(_ animated: Bool) { @@ -1988,8 +2192,8 @@ public class ChatController: TelegramController { self.containerLayout = layout - self.chatDisplayNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition, listViewTransaction: { updateSizeAndInsets in - self.chatDisplayNode.historyNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets) + self.chatDisplayNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition, listViewTransaction: { updateSizeAndInsets, additionalScrollDistance, scrollToTop in + self.chatDisplayNode.historyNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets, additionalScrollDistance: additionalScrollDistance, scrollToTop: scrollToTop) }) } @@ -2004,7 +2208,7 @@ public class ChatController: TelegramController { }) } - if self.peerId.namespace == Namespaces.Peer.CloudChannel || self.peerId.namespace == Namespaces.Peer.CloudGroup { + if case let .peer(peerId) = self.chatLocation, peerId.namespace == Namespaces.Peer.CloudChannel || peerId.namespace == Namespaces.Peer.CloudGroup { if temporaryChatPresentationInterfaceState.interfaceState.replyMessageId == nil && temporaryChatPresentationInterfaceState.interfaceState.messageActionsState.processedSetupReplyMessageId != keyboardButtonsMessage.id { temporaryChatPresentationInterfaceState = temporaryChatPresentationInterfaceState.updatedInterfaceState({ $0.withUpdatedReplyMessageId(keyboardButtonsMessage.id).withUpdatedMessageActionsState({ $0.withUpdatedProcessedSetupReplyMessageId(keyboardButtonsMessage.id) }) }) } @@ -2029,18 +2233,60 @@ public class ChatController: TelegramController { let inputTextPanelState = inputTextPanelStateForChatPresentationInterfaceState(temporaryChatPresentationInterfaceState, account: self.account) var updatedChatPresentationInterfaceState = temporaryChatPresentationInterfaceState.updatedInputTextPanelState({ _ in return inputTextPanelState }) - if let (updatedContextQueryState, updatedContextQuerySignal) = contextQueryResultStateForChatInterfacePresentationState(updatedChatPresentationInterfaceState, account: self.account, currentQuery: self.contextQueryState?.0) { - self.contextQueryState?.1.dispose() + let contextQueryUpdates = contextQueryResultStateForChatInterfacePresentationState(updatedChatPresentationInterfaceState, account: self.account, currentQueryStates: &self.contextQueryStates) + + for (kind, update) in contextQueryUpdates { + switch update { + case .remove: + if let (_, disposable) = self.contextQueryStates[kind] { + disposable.dispose() + self.contextQueryStates.removeValue(forKey: kind) + + updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedInputQueryResult(queryKind: kind, { _ in + return nil + }) + } + case let .update(query, signal): + let currentQueryAndDisposable = self.contextQueryStates[kind] + currentQueryAndDisposable?.1.dispose() + + var inScope = true + var inScopeResult: ((ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?)? + self.contextQueryStates[kind] = (query, (signal |> deliverOnMainQueue).start(next: { [weak self] result in + if let strongSelf = self { + if Thread.isMainThread && inScope { + inScope = false + inScopeResult = result + } else { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { + $0.updatedInputQueryResult(queryKind: kind, { previousResult in + return result(previousResult) + }) + }) + } + } + })) + inScope = false + if let inScopeResult = inScopeResult { + updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedInputQueryResult(queryKind: kind, { previousResult in + return inScopeResult(previousResult) + }) + } + } + } + + if let (updatedSearchQuerySuggestionState, updatedSearchQuerySuggestionSignal) = searchQuerySuggestionResultStateForChatInterfacePresentationState(updatedChatPresentationInterfaceState, account: self.account, currentQuery: self.searchQuerySuggestionState?.0) { + self.searchQuerySuggestionState?.1.dispose() var inScope = true var inScopeResult: ((ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?)? - self.contextQueryState = (updatedContextQueryState, (updatedContextQuerySignal |> deliverOnMainQueue).start(next: { [weak self] result in + self.searchQuerySuggestionState = (updatedSearchQuerySuggestionState, (updatedSearchQuerySuggestionSignal |> deliverOnMainQueue).start(next: { [weak self] result in if let strongSelf = self { if Thread.isMainThread && inScope { inScope = false inScopeResult = result } else { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInputQueryResult { previousResult in + $0.updatedSearchQuerySuggestionResult { previousResult in return result(previousResult) } }) @@ -2049,13 +2295,13 @@ public class ChatController: TelegramController { })) inScope = false if let inScopeResult = inScopeResult { - updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedInputQueryResult { previousResult in + updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedSearchQuerySuggestionResult { previousResult in return inScopeResult(previousResult) } } } - if let (updatedUrlPreviewUrl, updatedUrlPreviewSignal) = urlPreviewStateForChatInterfacePresentationState(updatedChatPresentationInterfaceState, account: self.account, currentQuery: self.urlPreviewQueryState?.0) { + if let (updatedUrlPreviewUrl, updatedUrlPreviewSignal) = urlPreviewStateForInputText(updatedChatPresentationInterfaceState.interfaceState.composeInputState.inputText, account: self.account, currentQuery: self.urlPreviewQueryState?.0) { self.urlPreviewQueryState?.1.dispose() var inScope = true var inScopeResult: ((TelegramMediaWebpage?) -> TelegramMediaWebpage?)? @@ -2085,12 +2331,64 @@ public class ChatController: TelegramController { } } + if let (updatedEditingUrlPreviewUrl, updatedEditingUrlPreviewSignal) = urlPreviewStateForInputText(updatedChatPresentationInterfaceState.interfaceState.editMessage?.inputState.inputText, account: self.account, currentQuery: self.editingUrlPreviewQueryState?.0) { + self.editingUrlPreviewQueryState?.1.dispose() + var inScope = true + var inScopeResult: ((TelegramMediaWebpage?) -> TelegramMediaWebpage?)? + self.editingUrlPreviewQueryState = (updatedEditingUrlPreviewUrl, (updatedEditingUrlPreviewSignal |> deliverOnMainQueue).start(next: { [weak self] result in + if let strongSelf = self { + if Thread.isMainThread && inScope { + inScope = false + inScopeResult = result + } else { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { + if let updatedEditingUrlPreviewUrl = updatedEditingUrlPreviewUrl, let webpage = result($0.editingUrlPreview?.1) { + return $0.updatedEditingUrlPreview((updatedEditingUrlPreviewUrl, webpage)) + } else { + return $0.updatedEditingUrlPreview(nil) + } + }) + } + } + })) + inScope = false + if let inScopeResult = inScopeResult { + if let updatedEditingUrlPreviewUrl = updatedEditingUrlPreviewUrl, let webpage = inScopeResult(updatedChatPresentationInterfaceState.editingUrlPreview?.1) { + updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedEditingUrlPreview((updatedEditingUrlPreviewUrl, webpage)) + } else { + updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedEditingUrlPreview(nil) + } + } + } + + if let updated = self.updateSearch(updatedChatPresentationInterfaceState) { + updatedChatPresentationInterfaceState = updated + } + + let recordingActivityValue: ChatRecordingActivity + if let mediaRecordingState = updatedChatPresentationInterfaceState.inputTextPanelState.mediaRecordingState { + switch mediaRecordingState { + case .audio: + recordingActivityValue = .voice + case .video(ChatVideoRecordingStatus.recording, _): + recordingActivityValue = .instantVideo + default: + recordingActivityValue = .none + } + } else { + recordingActivityValue = .none + } + if recordingActivityValue != self.recordingActivityValue { + self.recordingActivityValue = recordingActivityValue + self.recordingActivityPromise.set(recordingActivityValue) + } + self.presentationInterfaceState = updatedChatPresentationInterfaceState if self.isNodeLoaded { self.chatDisplayNode.updateChatPresentationInterfaceState(updatedChatPresentationInterfaceState, animated: animated, interactive: interactive) } - if let button = leftNavigationButtonForChatInterfaceState(updatedChatPresentationInterfaceState.interfaceState, strings: updatedChatPresentationInterfaceState.strings, currentButton: self.leftNavigationButton, target: self, selector: #selector(self.leftNavigationButtonAction)) { + if let button = leftNavigationButtonForChatInterfaceState(updatedChatPresentationInterfaceState, strings: updatedChatPresentationInterfaceState.strings, currentButton: self.leftNavigationButton, target: self, selector: #selector(self.leftNavigationButtonAction)) { if self.leftNavigationButton != button { self.navigationItem.setLeftBarButton(button.buttonItem, animated: animated) self.leftNavigationButton = button @@ -2100,7 +2398,7 @@ public class ChatController: TelegramController { self.leftNavigationButton = nil } - if let button = rightNavigationButtonForChatInterfaceState(updatedChatPresentationInterfaceState.interfaceState, strings: updatedChatPresentationInterfaceState.strings, currentButton: self.rightNavigationButton, target: self, selector: #selector(self.rightNavigationButtonAction), chatInfoNavigationButton: self.chatInfoNavigationButton) { + if let button = rightNavigationButtonForChatInterfaceState(updatedChatPresentationInterfaceState, strings: updatedChatPresentationInterfaceState.strings, currentButton: self.rightNavigationButton, target: self, selector: #selector(self.rightNavigationButtonAction), chatInfoNavigationButton: self.chatInfoNavigationButton) { if self.rightNavigationButton != button { self.navigationItem.setRightBarButton(button.buttonItem, animated: animated) self.rightNavigationButton = button @@ -2112,11 +2410,19 @@ public class ChatController: TelegramController { if let controllerInteraction = self.controllerInteraction { if updatedChatPresentationInterfaceState.interfaceState.selectionState != controllerInteraction.selectionState { - //let animated = controllerInteraction.selectionState == nil || updatedChatPresentationInterfaceState.interfaceState.selectionState == nil controllerInteraction.selectionState = updatedChatPresentationInterfaceState.interfaceState.selectionState self.updateItemNodesSelectionStates(animated: animated) } } + + switch updatedChatPresentationInterfaceState.mode { + case .standard: + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style + self.deferScreenEdgeGestures = [] + case .overlay: + self.statusBar.statusBarStyle = .Hide + self.deferScreenEdgeGestures = [.top] + } } private func updateItemNodesSelectionStates(animated: Bool) { @@ -2152,63 +2458,82 @@ public class ChatController: TelegramController { case .cancelMessageSelection: self.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) case .clearHistory: - let actionSheet = ActionSheetController(presentationTheme: self.presentationData.theme) - actionSheet.setItemGroups([ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: self.presentationData.strings.Conversation_ClearAll, color: .destructive, action: { [weak self, weak actionSheet] in - actionSheet?.dismissAnimated() - if let strongSelf = self { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) - let _ = clearHistoryInteractively(postbox: strongSelf.account.postbox, peerId: strongSelf.peerId).start() - } - }) - ]), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ])]) - self.chatDisplayNode.dismissInput() - self.present(actionSheet, in: .window(.root)) - case .openChatInfo: - self.navigationActionDisposable.set((self.peerView.get() - |> take(1) - |> deliverOnMainQueue).start(next: { [weak self] peerView in - if let strongSelf = self, let peer = peerView.peers[peerView.peerId] { - if let infoController = peerInfoController(account: strongSelf.account, peer: peer) { - (strongSelf.navigationController as? NavigationController)?.pushViewController(infoController) + if case let .peer(peerId) = self.chatLocation { + let actionSheet = ActionSheetController(presentationTheme: self.presentationData.theme) + actionSheet.setItemGroups([ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: self.presentationData.strings.Conversation_ClearAll, color: .destructive, action: { [weak self, weak actionSheet] in + actionSheet?.dismissAnimated() + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) + let _ = clearHistoryInteractively(postbox: strongSelf.account.postbox, peerId: peerId).start() } + }) + ]), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + self.chatDisplayNode.dismissInput() + self.present(actionSheet, in: .window(.root)) + } + case .openChatInfo: + switch self.chatLocationInfoData { + case let .peer(peerView): + self.navigationActionDisposable.set((peerView.get() + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] peerView in + if let strongSelf = self, let peer = peerView.peers[peerView.peerId] { + if let infoController = peerInfoController(account: strongSelf.account, peer: peer) { + (strongSelf.navigationController as? NavigationController)?.pushViewController(infoController) + } + } + })) + case .group: + if case let .group(groupId) = self.chatLocation { + (self.navigationController as? NavigationController)?.pushViewController(ChatListController(account: self.account, groupId: groupId, controlsHistoryPreload: false)) } - })) - break + } + case .search: + self.interfaceInteraction?.beginMessageSearch(.everything) } } private func presentMediaPicker(fileMode: Bool) { - if let peer = self.presentationInterfaceState.peer { - let _ = legacyAssetPicker(fileMode: fileMode, peer: peer).start(next: { [weak self] generator in + let _ = (self.account.postbox.modify { modifier -> GeneratedMediaStoreSettings in + let entry = modifier.getPreferencesEntry(key: ApplicationSpecificPreferencesKeys.generatedMediaStoreSettings) as? GeneratedMediaStoreSettings + return entry ?? GeneratedMediaStoreSettings.defaultSettings + } + |> deliverOnMainQueue).start(next: { [weak self] settings in if let strongSelf = self { - let legacyController = LegacyController(presentation: .modal(animateIn: true)) - let controller = generator(legacyController.context) - legacyController.bind(controller: controller) - legacyController.deferScreenEdgeGestures = [.top] - - configureLegacyAssetPicker(controller, account: strongSelf.account, peer: peer) - controller.descriptionGenerator = legacyAssetPickerItemGenerator() - controller.completionBlock = { [weak self, weak legacyController] signals in - if let strongSelf = self, let legacyController = legacyController { - legacyController.dismiss() - strongSelf.enqueueMediaMessages(signals: signals) - } + if let peer = strongSelf.presentationInterfaceState.peer { + let _ = legacyAssetPicker(theme: strongSelf.presentationData.theme, fileMode: fileMode, peer: peer, saveEditedPhotos: settings.storeEditedPhotos, allowGrouping: true).start(next: { generator in + if let strongSelf = self { + let legacyController = LegacyController(presentation: .modal(animateIn: true), theme: strongSelf.presentationData.theme) + legacyController.statusBar.statusBarStyle = strongSelf.presentationData.theme.rootController.statusBar.style.style + let controller = generator(legacyController.context) + legacyController.bind(controller: controller) + legacyController.deferScreenEdgeGestures = [.top] + + configureLegacyAssetPicker(controller, account: strongSelf.account, peer: peer) + controller.descriptionGenerator = legacyAssetPickerItemGenerator() + controller.completionBlock = { [weak legacyController] signals in + if let strongSelf = self, let legacyController = legacyController { + legacyController.dismiss() + strongSelf.enqueueMediaMessages(signals: signals) + } + } + controller.dismissalBlock = { [weak legacyController] in + if let legacyController = legacyController { + legacyController.dismiss() + } + } + strongSelf.chatDisplayNode.dismissInput() + strongSelf.present(legacyController, in: .window(.root)) + } + }) } - controller.dismissalBlock = { [weak legacyController] in - if let legacyController = legacyController { - legacyController.dismiss() - } - } - strongSelf.chatDisplayNode.dismissInput() - strongSelf.present(legacyController, in: .window(.root)) } }) - } } private func presentMapPicker() { @@ -2223,32 +2548,36 @@ public class ChatController: TelegramController { }) } }) - let message: EnqueueMessage = .message(text: "", attributes: [], media: TelegramMediaMap(latitude: coordinate.latitude, longitude: coordinate.longitude, geoPlace: nil, venue: venue, liveBroadcastingTimeout: nil), replyToMessageId: replyMessageId) + let message: EnqueueMessage = .message(text: "", attributes: [], media: TelegramMediaMap(latitude: coordinate.latitude, longitude: coordinate.longitude, geoPlace: nil, venue: venue, liveBroadcastingTimeout: nil), replyToMessageId: replyMessageId, localGroupingKey: nil) strongSelf.sendMessages([message]) } - }), in: .window(.root)) + }, theme: self.presentationData.theme), in: .window(.root)) } private func sendMessages(_ messages: [EnqueueMessage]) { - let _ = enqueueMessages(account: self.account, peerId: self.peerId, messages: messages).start(next: { [weak self] _ in - self?.chatDisplayNode.historyNode.scrollToEndOfHistory() - }) + if case let .peer(peerId) = self.chatLocation { + let _ = enqueueMessages(account: self.account, peerId: peerId, messages: messages).start(next: { [weak self] _ in + self?.chatDisplayNode.historyNode.scrollToEndOfHistory() + }) + } } private func enqueueMediaMessages(signals: [Any]?) { - self.enqueueMediaMessageDisposable.set((legacyAssetPickerEnqueueMessages(account: self.account, peerId: self.peerId, signals: signals!) |> deliverOnMainQueue).start(next: { [weak self] messages in - if let strongSelf = self { - let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId - strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ - if let strongSelf = self { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } - }) - } - }) - strongSelf.sendMessages(messages.map { $0.withUpdatedReplyToMessageId(replyMessageId) }) - } - })) + if case let .peer(peerId) = self.chatLocation { + self.enqueueMediaMessageDisposable.set((legacyAssetPickerEnqueueMessages(account: self.account, peerId: peerId, signals: signals!) |> deliverOnMainQueue).start(next: { [weak self] messages in + if let strongSelf = self { + let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId + strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { + $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + }) + } + }) + strongSelf.sendMessages(messages.map { $0.withUpdatedReplyToMessageId(replyMessageId) }) + } + })) + } } private func enqueueChatContextResult(_ results: ChatContextResultCollection, _ result: ChatContextResult) { @@ -2265,59 +2594,137 @@ public class ChatController: TelegramController { } } - private func requestAudioRecorder() { + private func firstLoadedMessageToListen() -> Message? { + var messageToListen: Message? + self.chatDisplayNode.historyNode.forEachMessageInCurrentHistoryView { message in + if message.flags.contains(.Incoming) && message.tags.contains(.voiceOrInstantVideo) { + for attribute in message.attributes { + if let attribute = attribute as? ConsumableContentMessageAttribute, !attribute.consumed { + messageToListen = message + return false + } + } + } + return true + } + return messageToListen + } + + private func activateRaiseGesture() { + if let messageToListen = self.firstLoadedMessageToListen() { + let _ = self.controllerInteraction?.openMessage(messageToListen.id) + } else { + self.requestAudioRecorder(beginWithTone: true) + } + } + + private func deactivateRaiseGesture() { + self.dismissMediaRecorder(.preview) + } + + private func requestAudioRecorder(beginWithTone: Bool) { if self.audioRecorderValue == nil { if let applicationContext = self.account.applicationContext as? TelegramApplicationContext { if self.audioRecorderFeedback == nil { - //self.audioRecorderFeedback = HapticFeedback() + self.audioRecorderFeedback = HapticFeedback() self.audioRecorderFeedback?.prepareTap() } - self.audioRecorder.set(applicationContext.mediaManager.audioRecorder()) + self.audioRecorder.set(applicationContext.mediaManager.audioRecorder(beginWithTone: beginWithTone, beganWithTone: { _ in + })) } } } private func requestVideoRecorder() { + guard case let .peer(peerId) = self.chatLocation else { + return + } + if self.videoRecorderValue == nil { if let currentInputPanelFrame = self.chatDisplayNode.currentInputPanelFrame() { - self.videoRecorder.set(.single(legacyInstantVideoController(theme: self.presentationData.theme, panelFrame: currentInputPanelFrame, account: self.account, peerId: self.peerId))) + self.videoRecorder.set(.single(legacyInstantVideoController(theme: self.presentationData.theme, panelFrame: currentInputPanelFrame, account: self.account, peerId: peerId, send: { [weak self] message in + if let strongSelf = self { + let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId + strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { + $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + }) + } + }) + let updatedMessage = message.withUpdatedReplyToMessageId(replyMessageId) + strongSelf.sendMessages([updatedMessage]) + } + }))) } } } - private func dismissMediaRecorder(sendMedia: Bool) { + private func dismissMediaRecorder(_ action: ChatFinishMediaRecordingAction) { if let audioRecorderValue = self.audioRecorderValue { audioRecorderValue.stop() - if sendMedia { - let _ = (audioRecorderValue.takenRecordedData() |> deliverOnMainQueue).start(next: { [weak self] data in - if let strongSelf = self, let data = data { - if data.duration < 0.5 { - strongSelf.audioRecorderFeedback?.error() - strongSelf.audioRecorderFeedback = nil - } else { - var randomId: Int64 = 0 - arc4random_buf(&randomId, 8) - - let resource = LocalFileMediaResource(fileId: randomId) - - strongSelf.account.postbox.mediaBox.storeResourceData(resource.id, data: data.compressedData) - - var waveformBuffer: MemoryBuffer? - if let waveform = data.waveform { - waveformBuffer = MemoryBuffer(data: waveform) + switch action { + case .dismiss: + break + case .preview: + let _ = (audioRecorderValue.takenRecordedData() |> deliverOnMainQueue).start(next: { [weak self] data in + if let strongSelf = self, let data = data { + if data.duration < 0.5 { + strongSelf.audioRecorderFeedback?.error() + strongSelf.audioRecorderFeedback = nil + } else if let waveform = data.waveform { + var randomId: Int64 = 0 + arc4random_buf(&randomId, 8) + + let resource = LocalFileMediaResource(fileId: randomId, size: data.compressedData.count) + + strongSelf.account.postbox.mediaBox.storeResourceData(resource.id, data: data.compressedData) + + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { + $0.updatedRecordedMediaPreview(ChatRecordedMediaPreview(resource: resource, duration: Int32(data.duration), fileSize: Int32(data.compressedData.count), waveform: AudioWaveform(bitstream: waveform, bitsPerSample: 5))) + }) + strongSelf.audioRecorderFeedback = nil } - - strongSelf.sendMessages([.message(text: "", attributes: [], media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), resource: resource, previewRepresentations: [], mimeType: "audio/ogg", size: data.compressedData.count, attributes: [.Audio(isVoice: true, duration: Int(data.duration), title: nil, performer: nil, waveform: waveformBuffer)]), replyToMessageId: nil)]) - - strongSelf.audioRecorderFeedback?.success() - strongSelf.audioRecorderFeedback = nil } - } - }) + }) + case .send: + let _ = (audioRecorderValue.takenRecordedData() |> deliverOnMainQueue).start(next: { [weak self] data in + if let strongSelf = self, let data = data { + if data.duration < 0.5 { + strongSelf.audioRecorderFeedback?.error() + strongSelf.audioRecorderFeedback = nil + } else { + var randomId: Int64 = 0 + arc4random_buf(&randomId, 8) + + let resource = LocalFileMediaResource(fileId: randomId) + + strongSelf.account.postbox.mediaBox.storeResourceData(resource.id, data: data.compressedData) + + var waveformBuffer: MemoryBuffer? + if let waveform = data.waveform { + waveformBuffer = MemoryBuffer(data: waveform) + } + + strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { + $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + }) + } + }) + + strongSelf.sendMessages([.message(text: "", attributes: [], media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), resource: resource, previewRepresentations: [], mimeType: "audio/ogg", size: data.compressedData.count, attributes: [.Audio(isVoice: true, duration: Int(data.duration), title: nil, performer: nil, waveform: waveformBuffer)]), replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil)]) + + strongSelf.audioRecorderFeedback?.tap() + strongSelf.audioRecorderFeedback = nil + } + } + }) } self.audioRecorder.set(.single(nil)) } else if let videoRecorderValue = self.videoRecorderValue { - if sendMedia { + if case .send = action { videoRecorderValue.completeVideo() self.tempVideoRecorderValue = videoRecorderValue self.videoRecorder.set(.single(nil)) @@ -2329,8 +2736,12 @@ public class ChatController: TelegramController { private func stopMediaRecorder() { if let audioRecorderValue = self.audioRecorderValue { - audioRecorderValue.stop() - self.audioRecorder.set(.single(nil)) + if let _ = self.presentationInterfaceState.inputTextPanelState.mediaRecordingState { + self.dismissMediaRecorder(.preview) + } else { + audioRecorderValue.stop() + self.audioRecorder.set(.single(nil)) + } } else if let videoRecorderValue = self.videoRecorderValue { if videoRecorderValue.stopVideo() { self.updateChatPresentationInterfaceState(animated: true, interactive: true, { @@ -2356,9 +2767,135 @@ public class ChatController: TelegramController { self.videoRecorderValue?.lockVideo() } - private func navigateToMessage(from fromId: MessageId?, to toId: MessageId, rememberInStack: Bool = true) { + private func deleteMediaRecording() { + self.updateChatPresentationInterfaceState(animated: true, interactive: true, { + $0.updatedRecordedMediaPreview(nil) + }) + } + + private func sendMediaRecording() { + if let recordedMediaPreview = self.presentationInterfaceState.recordedMediaPreview { + let waveformBuffer = MemoryBuffer(data: recordedMediaPreview.waveform.samples) + + self.chatDisplayNode.setupSendActionOnViewUpdate({ [weak self] in + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { + $0.updatedRecordedMediaPreview(nil).updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + }) + } + }) + + var randomId: Int64 = 0 + arc4random_buf(&randomId, 8) + + self.sendMessages([.message(text: "", attributes: [], media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), resource: recordedMediaPreview.resource, previewRepresentations: [], mimeType: "audio/ogg", size: Int(recordedMediaPreview.fileSize), attributes: [.Audio(isVoice: true, duration: Int(recordedMediaPreview.duration), title: nil, performer: nil, waveform: waveformBuffer)]), replyToMessageId: self.presentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil)]) + } + } + + private func updateSearch(_ interfaceState: ChatPresentationInterfaceState) -> ChatPresentationInterfaceState? { + guard case let .peer(peerId) = self.chatLocation else { + return nil + } + + var queryAndLocation: (String, SearchMessagesLocation)? + if let search = interfaceState.search { + switch search.domain { + case .everything: + switch self.chatLocation { + case let .peer(peerId): + queryAndLocation = (search.query, .peer(peerId: peerId, fromId: nil, tags: nil)) + case let .group(groupId): + queryAndLocation = (search.query, .group(groupId)) + } + case .members: + queryAndLocation = nil + case let .member(peer): + switch self.chatLocation { + case let .peer(peerId): + queryAndLocation = (search.query, .peer(peerId: peerId, fromId: peer.id, tags: nil)) + case .group: + queryAndLocation = nil + } + } + } + + if queryAndLocation?.0 != self.searchState?.0 || queryAndLocation?.1 != self.searchState?.1 { + self.searchState = queryAndLocation + if let (query, location) = queryAndLocation { + var queryIsEmpty = false + if query.isEmpty { + if case let .peer(_, fromId, _) = location { + if fromId == nil { + queryIsEmpty = true + } + } + } + + if queryIsEmpty { + self.searching.set(false) + self.searchDisposable?.set(nil) + if let data = interfaceState.search { + return interfaceState.updatedSearch(data.withUpdatedResultsState(nil)) + } + } else { + self.searching.set(true) + let searchDisposable: MetaDisposable + if let current = self.searchDisposable { + searchDisposable = current + } else { + searchDisposable = MetaDisposable() + self.searchDisposable = searchDisposable + } + searchDisposable.set((searchMessages(account: self.account, location: location, query: query) |> deliverOnMainQueue).start(next: { [weak self] results in + if let strongSelf = self { + var navigateId: MessageId? + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { current in + if let data = current.search { + let messageIds = results.map({ $0.id }).sorted() + var currentId = messageIds.last + if let previousResultId = data.resultsState?.currentId { + for id in messageIds { + if id >= previousResultId { + currentId = id + break + } + } + } + navigateId = currentId + return current.updatedSearch(data.withUpdatedResultsState(ChatSearchResultsState(messageIds: messageIds, currentId: currentId))) + } else { + return current + } + }) + if let navigateId = navigateId { + strongSelf.navigateToMessage(from: nil, to: navigateId) + } + } + }, completed: { [weak self] in + if let strongSelf = self { + strongSelf.searching.set(false) + } + })) + } + } else { + self.searching.set(false) + self.searchDisposable?.set(nil) + + if let data = interfaceState.search { + return interfaceState.updatedSearch(data.withUpdatedResultsState(nil)) + } + } + } + return nil + } + + public func navigateToMessage(id: MessageId, animated: Bool, completion: (() -> Void)? = nil) { + self.navigateToMessage(from: nil, to: id, rememberInStack: false, animated: animated, completion: completion) + } + + private func navigateToMessage(from fromId: MessageId?, to toId: MessageId, rememberInStack: Bool = true, animated: Bool = true, completion: (() -> Void)? = nil) { if self.isNodeLoaded { - if toId.peerId == self.peerId { + if case let .peer(peerId) = self.chatLocation, toId.peerId == peerId { var fromIndex: MessageIndex? if let fromId = fromId, let message = self.chatDisplayNode.historyNode.messageInCurrentHistoryView(fromId) { @@ -2377,10 +2914,11 @@ public class ChatController: TelegramController { if let message = self.chatDisplayNode.historyNode.messageInCurrentHistoryView(toId) { self.loadingMessage.set(false) self.messageIndexDisposable.set(nil) - self.chatDisplayNode.historyNode.scrollToMessage(from: fromIndex, to: MessageIndex(message)) + self.chatDisplayNode.historyNode.scrollToMessage(from: fromIndex, to: MessageIndex(message), animated: animated) + completion?() } else { self.loadingMessage.set(true) - let historyView = chatHistoryViewForLocation(.InitialSearch(location: .id(toId), count: 50), account: self.account, peerId: self.peerId, fixedCombinedReadState: nil, tagMask: nil, additionalData: []) + let historyView = chatHistoryViewForLocation(.InitialSearch(location: .id(toId), count: 50), account: self.account, chatLocation: self.chatLocation, fixedCombinedReadStates: nil, tagMask: nil, additionalData: []) let signal = historyView |> mapToSignal { historyView -> Signal in switch historyView { @@ -2400,7 +2938,8 @@ public class ChatController: TelegramController { |> take(1) self.messageIndexDisposable.set((signal |> deliverOnMainQueue).start(next: { [weak self] index in if let strongSelf = self, let index = index { - strongSelf.chatDisplayNode.historyNode.scrollToMessage(from: fromIndex, to: index) + strongSelf.chatDisplayNode.historyNode.scrollToMessage(from: fromIndex, to: index, animated: animated) + completion?() } }, completed: { [weak self] in if let strongSelf = self { @@ -2408,15 +2947,20 @@ public class ChatController: TelegramController { } })) } + } else { + completion?() } } else { - (self.navigationController as? NavigationController)?.pushViewController(ChatController(account: self.account, peerId: toId.peerId, messageId: toId)) + completion?() + (self.navigationController as? NavigationController)?.pushViewController(ChatController(account: self.account, chatLocation: .peer(toId.peerId), messageId: toId)) } + } else { + completion?() } } private func openPeer(peerId: PeerId?, navigation: ChatControllerInteractionNavigateToPeer, fromMessageId: MessageId?) { - if peerId == self.peerId { + if case let .peer(currentPeerId) = self.chatLocation, peerId == currentPeerId { switch navigation { case .info: self.navigationButtonAction(.openChatInfo) @@ -2437,41 +2981,46 @@ public class ChatController: TelegramController { } } else { if let peerId = peerId { - switch navigation { - case .info: - let peerSignal: Signal - if let fromMessageId = fromMessageId { - peerSignal = loadedPeerFromMessage(account: self.account, peerId: peerId, messageId: fromMessageId) - } else { - peerSignal = self.account.postbox.loadedPeerWithId(peerId) |> map { Optional($0) } - } - self.navigationActionDisposable.set((peerSignal |> take(1) |> deliverOnMainQueue).start(next: { [weak self] peer in - if let strongSelf = self, let peer = peer { - if let infoController = peerInfoController(account: strongSelf.account, peer: peer) { - (strongSelf.navigationController as? NavigationController)?.pushViewController(infoController) + switch self.chatLocation { + case .peer: + switch navigation { + case .info: + let peerSignal: Signal + if let fromMessageId = fromMessageId { + peerSignal = loadedPeerFromMessage(account: self.account, peerId: peerId, messageId: fromMessageId) + } else { + peerSignal = self.account.postbox.loadedPeerWithId(peerId) |> map { Optional($0) } } - } - })) - case let .chat(textInputState): - if let textInputState = textInputState { - let _ = (self.account.postbox.modify({ modifier -> Void in - modifier.updatePeerChatInterfaceState(peerId, update: { currentState in - if let currentState = currentState as? ChatInterfaceState { - return currentState.withUpdatedComposeInputState(textInputState) - } else { - return ChatInterfaceState().withUpdatedComposeInputState(textInputState) + self.navigationActionDisposable.set((peerSignal |> take(1) |> deliverOnMainQueue).start(next: { [weak self] peer in + if let strongSelf = self, let peer = peer { + if let infoController = peerInfoController(account: strongSelf.account, peer: peer) { + (strongSelf.navigationController as? NavigationController)?.pushViewController(infoController) + } } - }) - })).start(completed: { [weak self] in - if let strongSelf = self { - (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, peerId: peerId, messageId: nil)) + })) + case let .chat(textInputState): + if let textInputState = textInputState { + let _ = (self.account.postbox.modify({ modifier -> Void in + modifier.updatePeerChatInterfaceState(peerId, update: { currentState in + if let currentState = currentState as? ChatInterfaceState { + return currentState.withUpdatedComposeInputState(textInputState) + } else { + return ChatInterfaceState().withUpdatedComposeInputState(textInputState) + } + }) + })).start(completed: { [weak self] in + if let strongSelf = self { + (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, chatLocation: .peer(peerId), messageId: nil)) + } + }) + } else { + (self.navigationController as? NavigationController)?.pushViewController(ChatController(account: self.account, chatLocation: .peer(peerId), messageId: nil)) } - }) - } else { - (self.navigationController as? NavigationController)?.pushViewController(ChatController(account: self.account, peerId: peerId, messageId: nil)) + case let .withBotStartPayload(botStart): + (self.navigationController as? NavigationController)?.pushViewController(ChatController(account: self.account, chatLocation: .peer(peerId), messageId: nil, botStart: botStart)) } - case let .withBotStartPayload(botStart): - (self.navigationController as? NavigationController)?.pushViewController(ChatController(account: self.account, peerId: peerId, messageId: nil, botStart: botStart)) + case .group: + (self.navigationController as? NavigationController)?.pushViewController(ChatController(account: self.account, chatLocation: .peer(peerId), messageId: fromMessageId, botStart: nil)) } } else { switch navigation { @@ -2482,7 +3031,7 @@ public class ChatController: TelegramController { let controller = PeerSelectionController(account: self.account) controller.peerSelected = { [weak self, weak controller] peerId in if let strongSelf = self, let strongController = controller { - if peerId == strongSelf.peerId { + if case let .peer(currentPeerId) = strongSelf.chatLocation, peerId == currentPeerId { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { return ($0.updatedInterfaceState { return $0.withUpdatedComposeInputState(textInputState) @@ -2512,7 +3061,7 @@ public class ChatController: TelegramController { } })) - (strongSelf.navigationController as? NavigationController)?.replaceTopController(ChatController(account: strongSelf.account, peerId: peerId), animated: false, ready: ready) + (strongSelf.navigationController as? NavigationController)?.replaceTopController(ChatController(account: strongSelf.account, chatLocation: .peer(peerId)), animated: false, ready: ready) } }) } @@ -2539,16 +3088,19 @@ public class ChatController: TelegramController { disposable.set((resolvePeerByName(account: self.account, name: name, ageLimit: 10) |> take(1) |> deliverOnMainQueue).start(next: { [weak self] peerId in if let strongSelf = self { if let peerId = peerId { - (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, peerId: peerId, messageId: nil)) + (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, chatLocation: .peer(peerId), messageId: nil)) } } })) } private func unblockPeer() { + guard case let .peer(peerId) = self.chatLocation else { + return + } let unblockingPeer = self.unblockingPeer unblockingPeer.set(true) - self.editMessageDisposable.set((requestUpdatePeerIsBlocked(account: self.account, peerId: self.peerId, isBlocked: false) |> afterDisposed({ + self.editMessageDisposable.set((requestUpdatePeerIsBlocked(account: self.account, peerId: peerId, isBlocked: false) |> afterDisposed({ Queue.mainQueue().async { unblockingPeer.set(false) } @@ -2559,11 +3111,11 @@ public class ChatController: TelegramController { if let peer = self.presentationInterfaceState.peer { let title: String if let _ = peer as? TelegramGroup { - title = self.presentationData.strings.Conversation_ReportSpamAndLeave + title = self.presentationData.strings.Conversation_ReportSpam } else if let _ = peer as? TelegramChannel { - title = self.presentationData.strings.Conversation_ReportSpamAndLeave + title = self.presentationData.strings.Conversation_ReportSpam } else { - title = self.presentationData.strings.Conversation_ReportSpamAndLeave + title = self.presentationData.strings.Conversation_ReportSpam } let actionSheet = ActionSheetController(presentationTheme: self.presentationData.theme) actionSheet.setItemGroups([ActionSheetItemGroup(items: [ @@ -2584,22 +3136,34 @@ public class ChatController: TelegramController { } private func dismissReportPeer() { - self.editMessageDisposable.set((TelegramCore.dismissReportPeer(account: self.account, peerId: self.peerId) |> afterDisposed({ + guard case let .peer(peerId) = self.chatLocation else { + return + } + self.editMessageDisposable.set((TelegramCore.dismissReportPeer(account: self.account, peerId: peerId) |> afterDisposed({ Queue.mainQueue().async { } })).start()) } private func deleteChat(reportChatSpam: Bool) { + guard case let .peer(peerId) = self.chatLocation else { + return + } self.chatDisplayNode.historyNode.disconnect() - let _ = removePeerChat(postbox: self.account.postbox, peerId: self.peerId, reportChatSpam: reportChatSpam).start() + let _ = removePeerChat(postbox: self.account.postbox, peerId: peerId, reportChatSpam: reportChatSpam).start() (self.navigationController as? NavigationController)?.popToRoot(animated: true) + + let _ = requestUpdatePeerIsBlocked(account: self.account, peerId: peerId, isBlocked: true).start() } private func startBot(_ payload: String?) { + guard case let .peer(peerId) = self.chatLocation else { + return + } + let startingBot = self.startingBot startingBot.set(true) - self.editMessageDisposable.set((requestStartBot(account: self.account, botPeerId: self.peerId, payload: payload) |> deliverOnMainQueue |> afterDisposed({ + self.editMessageDisposable.set((requestStartBot(account: self.account, botPeerId: peerId, payload: payload) |> deliverOnMainQueue |> afterDisposed({ startingBot.set(false) })).start(completed: { [weak self] in if let strongSelf = self { @@ -2620,9 +3184,7 @@ public class ChatController: TelegramController { if let strongSelf = self { switch result { case let .externalUrl(url): - if let applicationContext = strongSelf.account.applicationContext as? TelegramApplicationContext { - applicationContext.applicationBindings.openUrl(url) - } + openExternalUrl(url: url, presentationData: strongSelf.presentationData, applicationContext: strongSelf.account.telegramApplicationContext, navigationController: strongSelf.navigationController as? NavigationController) case let .peer(peerId): strongSelf.openPeer(peerId: peerId, navigation: .chat(textInputState: nil), fromMessageId: nil) case let .botStart(peerId, payload): @@ -2630,9 +3192,21 @@ public class ChatController: TelegramController { case let .groupBotStart(peerId, payload): break case let .channelMessage(peerId, messageId): - (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, peerId: peerId, messageId: messageId)) + if case .peer(peerId) = strongSelf.chatLocation { + strongSelf.navigateToMessage(from: nil, to: messageId) + } else { + (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, chatLocation: .peer(peerId), messageId: messageId)) + } case let .stickerPack(name): strongSelf.present(StickerPackPreviewController(account: strongSelf.account, stickerPack: .name(name)), in: .window(.root)) + case let .instantView(webpage, anchor): + (strongSelf.navigationController as? NavigationController)?.pushViewController(InstantPageController(account: strongSelf.account, webPage: webpage, anchor: anchor)) + case let .join(link): + strongSelf.present(JoinLinkPreviewController(account: strongSelf.account, link: link, navigateToPeer: { peerId in + if let strongSelf = self { + strongSelf.openPeer(peerId: peerId, navigation: .chat(textInputState: nil), fromMessageId: nil) + } + }), in: .window(.root)) } } })) @@ -2644,11 +3218,16 @@ public class ChatController: TelegramController { let notificationSettings: PeerNotificationSettings? let peer: Peer? } - let peerId = self.peerId + let chatLocation = self.chatLocation let data = Atomic(value: nil) let semaphore = DispatchSemaphore(value: 0) let _ = self.account.postbox.modify({ modifier -> Void in - let _ = data.swap(PreviewActionsData(notificationSettings: modifier.getPeerNotificationSettings(peerId), peer: modifier.getPeer(peerId))) + switch chatLocation { + case let .peer(peerId): + let _ = data.swap(PreviewActionsData(notificationSettings: modifier.getPeerNotificationSettings(peerId), peer: modifier.getPeer(peerId))) + case .group: + let _ = data.swap(PreviewActionsData(notificationSettings: nil, peer: nil)) + } semaphore.signal() }).start() semaphore.wait() @@ -2660,40 +3239,42 @@ public class ChatController: TelegramController { switch strongSelf.peekActions { case .standard: - if let _ = data.peer as? TelegramUser { - items.append(UIPreviewAction(title: "👍", style: .default, handler: { _, _ in - if let strongSelf = self { - let _ = enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: [.message(text: "👍", attributes: [], media: nil, replyToMessageId: nil)]).start() - } - })) - } + if let peer = data.peer { + if let _ = data.peer as? TelegramUser { + items.append(UIPreviewAction(title: "👍", style: .default, handler: { _, _ in + if let strongSelf = self { + let _ = enqueueMessages(account: strongSelf.account, peerId: peer.id, messages: [.message(text: "👍", attributes: [], media: nil, replyToMessageId: nil, localGroupingKey: nil)]).start() + } + })) + } - if let notificationSettings = data.notificationSettings as? TelegramPeerNotificationSettings { - if case .muted = notificationSettings.muteState { - items.append(UIPreviewAction(title: presentationData.strings.Conversation_Unmute, style: .default, handler: { _, _ in - if let strongSelf = self { - let _ = togglePeerMuted(account: strongSelf.account, peerId: strongSelf.peerId).start() - } - })) - } else { - let muteInterval: Int32 - if let _ = data.peer as? TelegramChannel { - muteInterval = Int32.max + if let notificationSettings = data.notificationSettings as? TelegramPeerNotificationSettings { + if case .muted = notificationSettings.muteState { + items.append(UIPreviewAction(title: presentationData.strings.Conversation_Unmute, style: .default, handler: { _, _ in + if let strongSelf = self { + let _ = togglePeerMuted(account: strongSelf.account, peerId: peer.id).start() + } + })) } else { - muteInterval = 1 * 60 * 60 - } - let title: String - if muteInterval == Int32.max { - title = presentationData.strings.Conversation_Mute - } else { - title = muteForIntervalString(strings: presentationData.strings, value: muteInterval) - } - - items.append(UIPreviewAction(title: title, style: .default, handler: { _, _ in - if let strongSelf = self { - let _ = mutePeer(account: strongSelf.account, peerId: strongSelf.peerId, for: muteInterval).start() + let muteInterval: Int32 + if let _ = data.peer as? TelegramChannel { + muteInterval = Int32.max + } else { + muteInterval = 1 * 60 * 60 } - })) + let title: String + if muteInterval == Int32.max { + title = presentationData.strings.Conversation_Mute + } else { + title = muteForIntervalString(strings: presentationData.strings, value: muteInterval) + } + + items.append(UIPreviewAction(title: title, style: .default, handler: { _, _ in + if let strongSelf = self { + let _ = mutePeer(account: strongSelf.account, peerId: peer.id, for: muteInterval).start() + } + })) + } } } case let .remove(action): @@ -2705,4 +3286,34 @@ public class ChatController: TelegramController { return items } } + + private func debugStreamSingleVideo(_ id: MessageId) { + let gallery = GalleryController(account: self.account, messageId: id, streamSingleVideo: true, replaceRootController: { [weak self] controller, ready in + if let strongSelf = self { + (strongSelf.navigationController as? NavigationController)?.replaceTopController(controller, animated: false, ready: ready) + } + }, baseNavigationController: self.navigationController as? NavigationController) + + self.chatDisplayNode.dismissInput() + self.present(gallery, in: .window(.root), with: GalleryControllerPresentationArguments(transitionArguments: { [weak self] messageId, media in + if let strongSelf = self { + var transitionNode: ASDisplayNode? + strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ChatMessageItemView { + if let result = itemNode.transitionNode(id: messageId, media: media) { + transitionNode = result + } + } + } + if let transitionNode = transitionNode { + return GalleryTransitionArguments(transitionNode: transitionNode, addToTransitionSurface: { view in + if let strongSelf = self { + strongSelf.chatDisplayNode.historyNode.view.superview?.insertSubview(view, aboveSubview: strongSelf.chatDisplayNode.historyNode.view) + } + }) + } + } + return nil + })) + } } diff --git a/TelegramUI/ChatControllerInteraction.swift b/TelegramUI/ChatControllerInteraction.swift index 0e5039f13d..40fa587fff 100644 --- a/TelegramUI/ChatControllerInteraction.swift +++ b/TelegramUI/ChatControllerInteraction.swift @@ -37,7 +37,7 @@ public enum ChatControllerInteractionLongTapAction { } public final class ChatControllerInteraction { - let openMessage: (MessageId) -> Void + let openMessage: (MessageId) -> Bool let openSecretMessagePreview: (MessageId) -> Void let closeSecretMessagePreview: () -> Void let openPeer: (PeerId?, ChatControllerInteractionNavigateToPeer, MessageId?) -> Void @@ -45,7 +45,7 @@ public final class ChatControllerInteraction { let openMessageContextMenu: (MessageId, ASDisplayNode, CGRect) -> Void let navigateToMessage: (MessageId, MessageId) -> Void let clickThroughMessage: () -> Void - let toggleMessageSelection: (MessageId) -> Void + let toggleMessagesSelection: ([MessageId], Bool) -> Void let sendMessage: (String) -> Void let sendSticker: (TelegramMediaFile) -> Void let sendGif: (TelegramMediaFile) -> Void @@ -63,13 +63,15 @@ public final class ChatControllerInteraction { let longTap: (ChatControllerInteractionLongTapAction) -> Void let openCheckoutOrReceipt: (MessageId) -> Void let openSearch: () -> Void + let setupReply: (MessageId) -> Void + let canSetupReply: () -> Bool var hiddenMedia: [MessageId: [Media]] = [:] var selectionState: ChatInterfaceSelectionState? var highlightedState: ChatInterfaceHighlightedState? var automaticMediaDownloadSettings: AutomaticMediaDownloadSettings - public init(openMessage: @escaping (MessageId) -> Void, openSecretMessagePreview: @escaping (MessageId) -> Void, closeSecretMessagePreview: @escaping () -> Void, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, MessageId?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (MessageId, ASDisplayNode, CGRect) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, clickThroughMessage: @escaping () -> Void, toggleMessageSelection: @escaping (MessageId) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (TelegramMediaFile) -> Void, sendGif: @escaping (TelegramMediaFile) -> Void, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, openUrl: @escaping (String) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (MessageId) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, automaticMediaDownloadSettings: AutomaticMediaDownloadSettings) { + public init(openMessage: @escaping (MessageId) -> Bool, openSecretMessagePreview: @escaping (MessageId) -> Void, closeSecretMessagePreview: @escaping () -> Void, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, MessageId?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (MessageId, ASDisplayNode, CGRect) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (TelegramMediaFile) -> Void, sendGif: @escaping (TelegramMediaFile) -> Void, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, openUrl: @escaping (String) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (MessageId) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping () -> Bool, automaticMediaDownloadSettings: AutomaticMediaDownloadSettings) { self.openMessage = openMessage self.openSecretMessagePreview = openSecretMessagePreview self.closeSecretMessagePreview = closeSecretMessagePreview @@ -78,7 +80,7 @@ public final class ChatControllerInteraction { self.openMessageContextMenu = openMessageContextMenu self.navigateToMessage = navigateToMessage self.clickThroughMessage = clickThroughMessage - self.toggleMessageSelection = toggleMessageSelection + self.toggleMessagesSelection = toggleMessagesSelection self.sendMessage = sendMessage self.sendSticker = sendSticker self.sendGif = sendGif @@ -96,6 +98,8 @@ public final class ChatControllerInteraction { self.longTap = longTap self.openCheckoutOrReceipt = openCheckoutOrReceipt self.openSearch = openSearch + self.setupReply = setupReply + self.canSetupReply = canSetupReply self.automaticMediaDownloadSettings = automaticMediaDownloadSettings } diff --git a/TelegramUI/ChatControllerNode.swift b/TelegramUI/ChatControllerNode.swift index 451fcee465..a7a75f5da7 100644 --- a/TelegramUI/ChatControllerNode.swift +++ b/TelegramUI/ChatControllerNode.swift @@ -12,17 +12,33 @@ private func shouldRequestLayoutOnPresentationInterfaceStateTransition(_ lhs: Ch return false } -class ChatControllerNode: ASDisplayNode { +private final class ChatControllerNodeView: UITracingLayerView, WindowInputAccessoryHeightProvider { + var inputAccessoryHeight: (() -> CGFloat)? + + func getWindowInputAccessoryHeight() -> CGFloat { + return self.inputAccessoryHeight?() ?? 0.0 + } +} + +class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { let account: Account - let peerId: PeerId + let chatLocation: ChatLocation let controllerInteraction: ChatControllerInteraction let navigationBar: NavigationBar + private var backgroundEffectNode: ASDisplayNode? + private var containerBackgroundNode: ASImageNode? + private var scrollContainerNode: ASScrollNode? + private var containerNode: ASDisplayNode? + private var overlayNavigationBar: ChatOverlayNavigationBar? + let backgroundNode: ASDisplayNode let historyNode: ChatHistoryListNode let loadingNode: ChatLoadingNode + private var validLayout: ContainerViewLayout? + private var searchNavigationNode: ChatSearchNavigationContentNode? private let inputPanelBackgroundNode: ASDisplayNode @@ -34,6 +50,7 @@ class ChatControllerNode: ASDisplayNode { private var inputPanelNode: ChatInputPanelNode? private var accessoryPanelNode: AccessoryPanelNode? private var inputContextPanelNode: ChatInputContextPanelNode? + private var overlayContextPanelNode: ChatInputContextPanelNode? private var inputNode: ChatInputNode? @@ -44,15 +61,35 @@ class ChatControllerNode: ASDisplayNode { private var ignoreUpdateHeight = false + private var animateInAsOverlayCompletion: (() -> Void)? + private var dismissAsOverlayCompletion: (() -> Void)? + private var dismissedAsOverlay = false + private var scheduledAnimateInAsOverlayFromNode: ASDisplayNode? + private var dismissAsOverlayLayout: ContainerViewLayout? + + private var hapticFeedback: HapticFeedback? + private var scrollViewDismissStatus = false + var chatPresentationInterfaceState: ChatPresentationInterfaceState var automaticMediaDownloadSettings: AutomaticMediaDownloadSettings + private let selectedMessagesPromise = Promise?>(nil) + var selectedMessages: Set? { + didSet { + if self.selectedMessages != oldValue { + self.selectedMessagesPromise.set(.single(self.selectedMessages)) + } + } + } + var requestUpdateChatInterfaceState: (Bool, (ChatInterfaceState) -> ChatInterfaceState) -> Void = { _, _ in } var displayAttachmentMenu: () -> Void = { } + var displayPasteMenu: ([UIImage]) -> Void = { _ in } var updateTypingActivity: () -> Void = { } var dismissUrlPreview: () -> Void = { } var setupSendActionOnViewUpdate: (@escaping () -> Void) -> Void = { _ in } var requestLayout: (ContainedViewLayoutTransition) -> Void = { _ in } + var dismissAsOverlay: () -> Void = { } var interfaceInteraction: ChatPanelInterfaceInteraction? @@ -73,9 +110,9 @@ class ChatControllerNode: ASDisplayNode { } } - init(account: Account, peerId: PeerId, messageId: MessageId?, controllerInteraction: ChatControllerInteraction, chatPresentationInterfaceState: ChatPresentationInterfaceState, automaticMediaDownloadSettings: AutomaticMediaDownloadSettings, navigationBar: NavigationBar) { + init(account: Account, chatLocation: ChatLocation, messageId: MessageId?, controllerInteraction: ChatControllerInteraction, chatPresentationInterfaceState: ChatPresentationInterfaceState, automaticMediaDownloadSettings: AutomaticMediaDownloadSettings, navigationBar: NavigationBar) { self.account = account - self.peerId = peerId + self.chatLocation = chatLocation self.controllerInteraction = controllerInteraction self.chatPresentationInterfaceState = chatPresentationInterfaceState self.automaticMediaDownloadSettings = automaticMediaDownloadSettings @@ -90,7 +127,7 @@ class ChatControllerNode: ASDisplayNode { self.titleAccessoryPanelContainer = ChatControllerTitlePanelNodeContainer() self.titleAccessoryPanelContainer.clipsToBounds = true - self.historyNode = ChatHistoryListNode(account: account, peerId: peerId, tagMask: nil, messageId: messageId, controllerInteraction: controllerInteraction) + self.historyNode = ChatHistoryListNode(account: account, chatLocation: chatLocation, tagMask: nil, messageId: messageId, controllerInteraction: controllerInteraction, selectedMessages: self.selectedMessagesPromise.get()) self.loadingNode = ChatLoadingNode(theme: chatPresentationInterfaceState.theme) self.inputPanelBackgroundNode = ASDisplayNode() @@ -106,10 +143,16 @@ class ChatControllerNode: ASDisplayNode { super.init() self.setViewBlock({ - return UITracingLayerView() + return ChatControllerNodeView() }) - self.backgroundColor = UIColor(rgb: 0xdee3e9) + (self.view as? ChatControllerNodeView)?.inputAccessoryHeight = { [weak self] in + if let strongSelf = self { + return strongSelf.getWindowInputAccessoryHeight() + } else { + return 0.0 + } + } assert(Queue.mainQueue().isCurrent()) @@ -164,7 +207,7 @@ class ChatControllerNode: ASDisplayNode { self.historyNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) self.textInputPanelNode = ChatTextInputPanelNode(theme: chatPresentationInterfaceState.theme, presentController: { [weak self] controller in - self?.interfaceInteraction?.presentController(controller) + self?.interfaceInteraction?.presentController(controller, nil) }) self.textInputPanelNode?.updateHeight = { [weak self] in if let strongSelf = self, let _ = strongSelf.inputPanelNode as? ChatTextInputPanelNode, !strongSelf.ignoreUpdateHeight { @@ -185,8 +228,8 @@ class ChatControllerNode: ASDisplayNode { if let editMessage = effectivePresentationInterfaceState.interfaceState.editMessage { let text = editMessage.inputState.inputText - if let interfaceInteraction = strongSelf.interfaceInteraction, !text.isEmpty { - interfaceInteraction.editMessage(editMessage.messageId, editMessage.inputState.inputText) + if let interfaceInteraction = strongSelf.interfaceInteraction { + interfaceInteraction.editMessage() } } else { let text = effectivePresentationInterfaceState.interfaceState.composeInputState.inputText @@ -204,7 +247,7 @@ class ChatControllerNode: ASDisplayNode { var messages: [EnqueueMessage] = [] if !text.isEmpty { var attributes: [MessageAttribute] = [] - let entities = generateTextEntities(text) + let entities = generateTextEntities(text, enabledTypes: .all) if !entities.isEmpty { attributes.append(TextEntitiesMessageAttribute(entities: entities)) } @@ -214,24 +257,30 @@ class ChatControllerNode: ASDisplayNode { } else { webpage = strongSelf.chatPresentationInterfaceState.urlPreview?.1 } - messages.append(.message(text: text, attributes: attributes, media: webpage, replyToMessageId: strongSelf.chatPresentationInterfaceState.interfaceState.replyMessageId)) + messages.append(.message(text: text, attributes: attributes, media: webpage, replyToMessageId: strongSelf.chatPresentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil)) } if let forwardMessageIds = strongSelf.chatPresentationInterfaceState.interfaceState.forwardMessageIds { for id in forwardMessageIds { - messages.append(.forward(source: id)) + messages.append(.forward(source: id, grouping: .auto)) } } - let _ = enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: messages).start(next: { _ in - if let strongSelf = self { - strongSelf.historyNode.scrollToEndOfHistory() - } - }) + if case let .peer(peerId) = strongSelf.chatLocation { + let _ = enqueueMessages(account: strongSelf.account, peerId: peerId, messages: messages).start(next: { _ in + if let strongSelf = self { + strongSelf.historyNode.scrollToEndOfHistory() + } + }) + } } } } } + self.textInputPanelNode?.pasteImages = { [weak self] images in + self?.displayPasteMenu(images) + } + self.textInputPanelNode?.displayAttachmentMenu = { [weak self] in self?.displayAttachmentMenu() } @@ -241,8 +290,104 @@ class ChatControllerNode: ASDisplayNode { } } - func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition, listViewTransaction: (ListViewUpdateSizeAndInsets) -> Void) { + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition protoTransition: ContainedViewLayoutTransition, listViewTransaction: + (ListViewUpdateSizeAndInsets, CGFloat, Bool) -> Void) { + let transition: ContainedViewLayoutTransition + if let _ = self.scheduledAnimateInAsOverlayFromNode { + transition = .immediate + } else { + transition = protoTransition + } + self.scheduledLayoutTransitionRequest = nil + if case .overlay = self.chatPresentationInterfaceState.mode { + if self.backgroundEffectNode == nil { + let backgroundEffectNode = ASDisplayNode() + switch self.chatPresentationInterfaceState.theme.inAppNotification.expandedNotification.backgroundType { + case .light: + backgroundEffectNode.backgroundColor = UIColor(white: 1.0, alpha: 0.8) + case .dark: + backgroundEffectNode.backgroundColor = UIColor(white: 0.0, alpha: 0.8) + } + self.insertSubnode(backgroundEffectNode, at: 0) + self.backgroundEffectNode = backgroundEffectNode + } + if self.scrollContainerNode == nil { + let scrollContainerNode = ASScrollNode() + scrollContainerNode.view.delaysContentTouches = false + //scrollContainerNode.view.canCancelContentTouches = false + //scrollContainerNode.view.panGestureRecognizer.cancelsTouchesInView = false + scrollContainerNode.view.delegate = self + scrollContainerNode.view.alwaysBounceVertical = true + if #available(iOSApplicationExtension 11.0, *) { + scrollContainerNode.view.contentInsetAdjustmentBehavior = .never + } + self.insertSubnode(scrollContainerNode, aboveSubnode: self.backgroundEffectNode!) + self.scrollContainerNode = scrollContainerNode + } + if self.containerBackgroundNode == nil { + let containerBackgroundNode = ASImageNode() + containerBackgroundNode.displaysAsynchronously = false + containerBackgroundNode.displayWithoutProcessing = true + containerBackgroundNode.image = PresentationResourcesRootController.inAppNotificationBackground(self.chatPresentationInterfaceState.theme) + self.scrollContainerNode?.addSubnode(containerBackgroundNode) + self.containerBackgroundNode = containerBackgroundNode + } + if self.containerNode == nil { + let containerNode = ASDisplayNode() + containerNode.clipsToBounds = true + containerNode.cornerRadius = 15.0 + containerNode.addSubnode(self.backgroundNode) + containerNode.addSubnode(self.historyNode) + self.containerNode = containerNode + self.scrollContainerNode?.addSubnode(containerNode) + self.navigationBar.isHidden = true + } + if self.overlayNavigationBar == nil { + let overlayNavigationBar = ChatOverlayNavigationBar(theme: self.chatPresentationInterfaceState.theme, close: { [weak self] in + self?.dismissAsOverlay() + }) + self.overlayNavigationBar = overlayNavigationBar + self.containerNode?.addSubnode(overlayNavigationBar) + } + } else { + if let backgroundEffectNode = self.backgroundEffectNode { + backgroundEffectNode.removeFromSupernode() + self.backgroundEffectNode = nil + } + if let scrollContainerNode = self.scrollContainerNode { + scrollContainerNode.removeFromSupernode() + self.scrollContainerNode = nil + } + if let containerNode = self.containerNode { + self.containerNode = nil + containerNode.removeFromSupernode() + self.insertSubnode(self.backgroundNode, at: 0) + self.insertSubnode(self.historyNode, aboveSubnode: self.backgroundNode) + self.navigationBar.isHidden = false + } + if let overlayNavigationBar = self.overlayNavigationBar { + overlayNavigationBar.removeFromSupernode() + self.overlayNavigationBar = nil + } + } + + var dismissedInputByDragging = false + if let validLayout = self.validLayout { + var wasDragging = false + if validLayout.inputHeight != nil && validLayout.inputHeightIsInteractivellyChanging { + wasDragging = true + } + if wasDragging { + if layout.inputHeight == 0.0 && validLayout.inputHeightIsInteractivellyChanging && !layout.inputHeightIsInteractivellyChanging { + dismissedInputByDragging = true + } + } + } + self.validLayout = layout + + let cleanInsets = layout.intrinsicInsets + var previousInputHeight: CGFloat = 0.0 if let (previousLayout, _) = self.containerLayoutAndNavigationBarHeight { previousInputHeight = previousLayout.insets(options: [.input]).bottom @@ -270,6 +415,7 @@ class ChatControllerNode: ASDisplayNode { self.searchNavigationNode = ChatSearchNavigationContentNode(theme: self.chatPresentationInterfaceState.theme, strings: self.chatPresentationInterfaceState.strings, interaction: interfaceInteraction) } self.navigationBar.setContentNode(self.searchNavigationNode, animated: transitionIsAnimated) + self.searchNavigationNode?.update(presentationInterfaceState: self.chatPresentationInterfaceState) if activate { self.searchNavigationNode?.activate() } @@ -305,10 +451,15 @@ class ChatControllerNode: ASDisplayNode { if self.inputNode != inputNode { dismissedInputNode = self.inputNode self.inputNode = inputNode + inputNode.alpha = 1.0 immediatelyLayoutInputNodeAndAnimateAppearance = true - self.insertSubnode(inputNode, belowSubnode: self.inputPanelBackgroundNode) + if let inputPanelNode = self.inputPanelNode, inputPanelNode.supernode != nil { + self.insertSubnode(inputNode, aboveSubnode: inputPanelNode) + } else { + self.insertSubnode(inputNode, aboveSubnode: self.inputPanelBackgroundNode) + } } - inputNodeHeight = inputNode.updateLayout(width: layout.size.width, transition: immediatelyLayoutInputNodeAndAnimateAppearance ? .immediate : transition, interfaceState: self.chatPresentationInterfaceState) + inputNodeHeight = inputNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: cleanInsets.bottom, transition: immediatelyLayoutInputNodeAndAnimateAppearance ? .immediate : transition, interfaceState: self.chatPresentationInterfaceState) } else if let inputNode = self.inputNode { dismissedInputNode = inputNode self.inputNode = nil @@ -317,15 +468,30 @@ class ChatControllerNode: ASDisplayNode { var insets: UIEdgeInsets if let inputNodeHeight = inputNodeHeight { insets = layout.insets(options: []) - insets.bottom += inputNodeHeight + insets.bottom = max(inputNodeHeight, insets.bottom) } else { insets = layout.insets(options: [.input]) } - insets.top += navigationBarHeight + if case .overlay = self.chatPresentationInterfaceState.mode { + insets.top = 44.0 + } else { + insets.top += navigationBarHeight + } + + var wrappingInsets = UIEdgeInsets() + if case .overlay = self.chatPresentationInterfaceState.mode { + wrappingInsets.left = 8.0 + layout.safeInsets.left + wrappingInsets.right = 8.0 + layout.safeInsets.right + wrappingInsets.top = 8.0 + if let statusBarHeight = layout.statusBarHeight, CGFloat(40.0).isLess(than: statusBarHeight) { + wrappingInsets.top += statusBarHeight + } + } var dismissedInputPanelNode: ASDisplayNode? var dismissedAccessoryPanelNode: ASDisplayNode? var dismissedInputContextPanelNode: ChatInputContextPanelNode? + var dismissedOverlayContextPanelNode: ChatInputContextPanelNode? var inputPanelSize: CGSize? var immediatelyLayoutInputPanelAndAnimateAppearance = false @@ -336,12 +502,12 @@ class ChatControllerNode: ASDisplayNode { } dismissedInputPanelNode = self.inputPanelNode immediatelyLayoutInputPanelAndAnimateAppearance = true - let inputPanelHeight = inputPanelNode.updateLayout(width: layout.size.width, maxHeight: layout.size.height - insets.top - insets.bottom, transition: .immediate, interfaceState: self.chatPresentationInterfaceState) + let inputPanelHeight = inputPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, maxHeight: layout.size.height - insets.top - insets.bottom, transition: .immediate, interfaceState: self.chatPresentationInterfaceState) inputPanelSize = CGSize(width: layout.size.width, height: inputPanelHeight) self.inputPanelNode = inputPanelNode - self.insertSubnode(inputPanelNode, aboveSubnode: self.navigateButtons) + self.insertSubnode(inputPanelNode, aboveSubnode: self.inputPanelBackgroundNode) } else { - let inputPanelHeight = inputPanelNode.updateLayout(width: layout.size.width, maxHeight: layout.size.height - insets.top - insets.bottom, transition: transition, interfaceState: self.chatPresentationInterfaceState) + let inputPanelHeight = inputPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, maxHeight: layout.size.height - insets.top - insets.bottom, transition: transition, interfaceState: self.chatPresentationInterfaceState) inputPanelSize = CGSize(width: layout.size.width, height: inputPanelHeight) } } else { @@ -350,14 +516,14 @@ class ChatControllerNode: ASDisplayNode { } if let inputMediaNode = self.inputMediaNode, inputMediaNode != self.inputNode { - let _ = inputMediaNode.updateLayout(width: layout.size.width, transition: .immediate, interfaceState: self.chatPresentationInterfaceState) + let _ = inputMediaNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: cleanInsets.bottom, transition: .immediate, interfaceState: self.chatPresentationInterfaceState) } transition.updateFrame(node: self.titleAccessoryPanelContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: 56.0))) var titleAccessoryPanelFrame: CGRect? if let titleAccessoryPanelNode = self.titleAccessoryPanelNode { - let panelHeight = titleAccessoryPanelNode.updateLayout(width: layout.size.width, transition: immediatelyLayoutTitleAccessoryPanelNodeAndAnimateAppearance ? .immediate : transition, interfaceState: self.chatPresentationInterfaceState) + let panelHeight = titleAccessoryPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: immediatelyLayoutTitleAccessoryPanelNodeAndAnimateAppearance ? .immediate : transition, interfaceState: self.chatPresentationInterfaceState) titleAccessoryPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: panelHeight)) insets.top += panelHeight } @@ -377,13 +543,18 @@ class ChatControllerNode: ASDisplayNode { } } - self.backgroundNode.frame = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) + let contentBounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width - wrappingInsets.left - wrappingInsets.right, height: layout.size.height - wrappingInsets.top - wrappingInsets.bottom) - self.historyNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) - self.historyNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0) + if let backgroundEffectNode = self.backgroundEffectNode { + transition.updateFrame(node: backgroundEffectNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + } - self.loadingNode.updateLayout(size: layout.size, insets: insets, transition: transition) - transition.updateFrame(node: self.loadingNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + transition.updateFrame(node: self.backgroundNode, frame: contentBounds) + transition.updateBounds(node: self.historyNode, bounds: CGRect(origin: CGPoint(), size: contentBounds.size)) + transition.updatePosition(node: self.historyNode, position: CGPoint(x: contentBounds.midX, y: contentBounds.midY)) + + self.loadingNode.updateLayout(size: contentBounds.size, insets: insets, transition: transition) + transition.updateFrame(node: self.loadingNode, frame: contentBounds) let listViewCurve: ListViewAnimationCurve if curve == 7 { @@ -436,19 +607,35 @@ class ChatControllerNode: ASDisplayNode { self.addSubnode(inputContextPanelNode) immediatelyLayoutInputContextPanelAndAnimateAppearance = true - } } else if let inputContextPanelNode = self.inputContextPanelNode { dismissedInputContextPanelNode = inputContextPanelNode self.inputContextPanelNode = nil } + var immediatelyLayoutOverlayContextPanelAndAnimateAppearance = false + if let overlayContextPanelNode = chatOverlayContextPanelForChatPresentationIntefaceState(self.chatPresentationInterfaceState, account: self.account, currentPanel: self.overlayContextPanelNode, interfaceInteraction: self.interfaceInteraction) { + if overlayContextPanelNode !== self.overlayContextPanelNode { + dismissedOverlayContextPanelNode = self.overlayContextPanelNode + self.overlayContextPanelNode = overlayContextPanelNode + + self.addSubnode(overlayContextPanelNode) + immediatelyLayoutOverlayContextPanelAndAnimateAppearance = true + } + } else if let overlayContextPanelNode = self.overlayContextPanelNode { + dismissedOverlayContextPanelNode = overlayContextPanelNode + self.overlayContextPanelNode = nil + } + var inputPanelsHeight: CGFloat = 0.0 var inputPanelFrame: CGRect? if self.inputPanelNode != nil { assert(inputPanelSize != nil) inputPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - insets.bottom - inputPanelsHeight - inputPanelSize!.height), size: CGSize(width: layout.size.width, height: inputPanelSize!.height)) + if self.dismissedAsOverlay { + inputPanelFrame = inputPanelFrame!.offsetBy(dx: 0.0, dy: inputPanelsHeight + inputPanelSize!.height) + } inputPanelsHeight += inputPanelSize!.height } @@ -456,15 +643,80 @@ class ChatControllerNode: ASDisplayNode { if self.accessoryPanelNode != nil { assert(accessoryPanelSize != nil) accessoryPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - insets.bottom - inputPanelsHeight - accessoryPanelSize!.height), size: CGSize(width: layout.size.width, height: accessoryPanelSize!.height)) + if self.dismissedAsOverlay { + accessoryPanelFrame = accessoryPanelFrame!.offsetBy(dx: 0.0, dy: inputPanelsHeight + accessoryPanelSize!.height) + } inputPanelsHeight += accessoryPanelSize!.height } - let inputBackgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - insets.bottom - inputPanelsHeight), size: CGSize(width: layout.size.width, height: inputPanelsHeight)) + if self.dismissedAsOverlay { + inputPanelsHeight = 0.0 + } - listViewTransaction(ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: insets.bottom + inputPanelsHeight + 4.0, left: insets.right, bottom: insets.top, right: insets.left), duration: duration, curve: listViewCurve)) + let inputBackgroundInset: CGFloat + if cleanInsets.bottom < insets.bottom { + inputBackgroundInset = 0.0 + } else { + inputBackgroundInset = cleanInsets.bottom + } + + let inputBackgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - insets.bottom - inputPanelsHeight), size: CGSize(width: layout.size.width, height: inputPanelsHeight + inputBackgroundInset)) + + let additionalScrollDistance: CGFloat = 0.0 + var scrollToTop = false + if dismissedInputByDragging { + if !self.historyNode.trackingOffset.isZero { + if self.historyNode.beganTrackingAtTopOrigin { + scrollToTop = true + } + } + } + + var contentBottomInset: CGFloat = inputPanelsHeight + 4.0 + + if let scrollContainerNode = self.scrollContainerNode { + transition.updateFrame(node: scrollContainerNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + } + + var containerInsets = insets + if let dismissAsOverlayLayout = self.dismissAsOverlayLayout { + if let inputNodeHeight = inputNodeHeight { + containerInsets = dismissAsOverlayLayout.insets(options: []) + containerInsets.bottom = max(inputNodeHeight, insets.bottom) + } else { + containerInsets = dismissAsOverlayLayout.insets(options: [.input]) + } + } + + if let containerNode = self.containerNode { + contentBottomInset += 8.0 + let containerNodeFrame = CGRect(origin: CGPoint(x: wrappingInsets.left, y: wrappingInsets.top), size: CGSize(width: contentBounds.size.width, height: contentBounds.size.height - containerInsets.bottom - inputPanelsHeight - 8.0)) + transition.updateFrame(node: containerNode, frame: containerNodeFrame) + + if let containerBackgroundNode = self.containerBackgroundNode { + transition.updateFrame(node: containerBackgroundNode, frame: CGRect(origin: CGPoint(x: containerNodeFrame.minX - 8.0, y: containerNodeFrame.minY - 8.0), size: CGSize(width: containerNodeFrame.size.width + 8.0 * 2.0, height: containerNodeFrame.size.height + 8.0 + 20.0))) + } + } + + if let overlayNavigationBar = self.overlayNavigationBar { + let barFrame = CGRect(origin: CGPoint(), size: CGSize(width: contentBounds.size.width, height: 44.0)) + transition.updateFrame(node: overlayNavigationBar, frame: barFrame) + overlayNavigationBar.updateLayout(size: barFrame.size, presentationInterfaceState: self.chatPresentationInterfaceState, transition: transition) + } + + var listInsets = UIEdgeInsets(top: containerInsets.bottom + contentBottomInset, left: containerInsets.right, bottom: containerInsets.top, right: containerInsets.left) + if case .standard = self.chatPresentationInterfaceState.mode { + listInsets.left += layout.safeInsets.left + listInsets.right += layout.safeInsets.right + } + + listViewTransaction(ListViewUpdateSizeAndInsets(size: contentBounds.size, insets: listInsets, duration: duration, curve: listViewCurve), additionalScrollDistance, scrollToTop) let navigateButtonsSize = self.navigateButtons.updateLayout(transition: transition) - let navigateButtonsFrame = CGRect(origin: CGPoint(x: layout.size.width - navigateButtonsSize.width - 6.0, y: layout.size.height - insets.bottom - inputPanelsHeight - navigateButtonsSize.height - 6.0), size: navigateButtonsSize) + var navigateButtonsFrame = CGRect(origin: CGPoint(x: layout.size.width - layout.safeInsets.right - navigateButtonsSize.width - 6.0, y: layout.size.height - containerInsets.bottom - inputPanelsHeight - navigateButtonsSize.height - 6.0), size: navigateButtonsSize) + if case .overlay = self.chatPresentationInterfaceState.mode { + navigateButtonsFrame = navigateButtonsFrame.offsetBy(dx: -8.0, dy: -8.0) + } transition.updateFrame(node: self.inputPanelBackgroundNode, frame: inputBackgroundFrame) transition.updateFrame(node: self.inputPanelBackgroundSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: inputBackgroundFrame.origin.y - UIScreenPixel), size: CGSize(width: inputBackgroundFrame.size.width, height: UIScreenPixel))) @@ -506,10 +758,21 @@ class ChatControllerNode: ASDisplayNode { let panelFrame = inputContextPanelNode.placement == .overTextInput ? inputContextPanelsOverMainPanelFrame : inputContextPanelsFrame if immediatelyLayoutInputContextPanelAndAnimateAppearance { inputContextPanelNode.frame = panelFrame - inputContextPanelNode.updateLayout(size: panelFrame.size, transition: .immediate, interfaceState: self.chatPresentationInterfaceState) + inputContextPanelNode.updateLayout(size: panelFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: .immediate, interfaceState: self.chatPresentationInterfaceState) } else if !inputContextPanelNode.frame.equalTo(panelFrame) { transition.updateFrame(node: inputContextPanelNode, frame: panelFrame) - inputContextPanelNode.updateLayout(size: panelFrame.size, transition: transition, interfaceState: self.chatPresentationInterfaceState) + inputContextPanelNode.updateLayout(size: panelFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: transition, interfaceState: self.chatPresentationInterfaceState) + } + } + + if let overlayContextPanelNode = self.overlayContextPanelNode { + let panelFrame = overlayContextPanelNode.placement == .overTextInput ? inputContextPanelsOverMainPanelFrame : inputContextPanelsFrame + if immediatelyLayoutOverlayContextPanelAndAnimateAppearance { + overlayContextPanelNode.frame = panelFrame + overlayContextPanelNode.updateLayout(size: panelFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: .immediate, interfaceState: self.chatPresentationInterfaceState) + } else if !overlayContextPanelNode.frame.equalTo(panelFrame) { + transition.updateFrame(node: overlayContextPanelNode, frame: panelFrame) + overlayContextPanelNode.updateLayout(size: panelFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: transition, interfaceState: self.chatPresentationInterfaceState) } } @@ -517,7 +780,10 @@ class ChatControllerNode: ASDisplayNode { let inputNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - inputNodeHeight), size: CGSize(width: layout.size.width, height: inputNodeHeight)) if immediatelyLayoutInputNodeAndAnimateAppearance { var adjustedForPreviousInputHeightFrame = inputNodeFrame - let heightDifference = inputNodeHeight - previousInputHeight + var heightDifference = inputNodeHeight - previousInputHeight + if previousInputHeight.isLessThanOrEqualTo(cleanInsets.bottom) { + heightDifference = inputNodeHeight + } adjustedForPreviousInputHeightFrame.origin.y += heightDifference inputNode.frame = adjustedForPreviousInputHeightFrame transition.updateFrame(node: inputNode, frame: inputNodeFrame) @@ -604,19 +870,75 @@ class ChatControllerNode: ASDisplayNode { }) } + if let dismissedOverlayContextPanelNode = dismissedOverlayContextPanelNode { + var frameCompleted = false + var animationCompleted = false + let completed = { [weak dismissedOverlayContextPanelNode] in + if let dismissedOverlayContextPanelNode = dismissedOverlayContextPanelNode, frameCompleted, animationCompleted { + dismissedOverlayContextPanelNode.removeFromSupernode() + } + } + let panelFrame = inputContextPanelsFrame + if false && !dismissedOverlayContextPanelNode.frame.equalTo(panelFrame) { + transition.updateFrame(node: dismissedOverlayContextPanelNode, frame: panelFrame, completion: { _ in + frameCompleted = true + completed() + }) + } else { + frameCompleted = true + } + + dismissedOverlayContextPanelNode.animateOut(completion: { + animationCompleted = true + completed() + }) + } + if let dismissedInputNode = dismissedInputNode { - transition.updateFrame(node: dismissedInputNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - insets.bottom), size: CGSize(width: layout.size.width, height: max(insets.bottom, dismissedInputNode.bounds.size.height))), force: true, completion: { [weak self, weak dismissedInputNode] completed in - if completed { + let targetY: CGFloat + if cleanInsets.bottom.isLess(than: insets.bottom) { + targetY = layout.size.height - insets.bottom + } else { + targetY = layout.size.height + } + transition.updateFrame(node: dismissedInputNode, frame: CGRect(origin: CGPoint(x: 0.0, y: targetY), size: CGSize(width: layout.size.width, height: max(insets.bottom, dismissedInputNode.bounds.size.height))), force: true, completion: { [weak self, weak dismissedInputNode] completed in + if completed, let dismissedInputNode = dismissedInputNode { if let strongSelf = self { if strongSelf.inputNode !== dismissedInputNode { - dismissedInputNode?.removeFromSupernode() + dismissedInputNode.alpha = 0.0 + dismissedInputNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak dismissedInputNode] completed in + if completed, let strongSelf = self, let dismissedInputNode = dismissedInputNode { + if strongSelf.inputNode !== dismissedInputNode { + dismissedInputNode.removeFromSupernode() + } + } + }) } } else { - dismissedInputNode?.removeFromSupernode() + dismissedInputNode.removeFromSupernode() } } }) } + + if let dismissAsOverlayCompletion = self.dismissAsOverlayCompletion { + self.dismissAsOverlayCompletion = nil + transition.updateBounds(node: self.navigateButtons, bounds: self.navigateButtons.bounds, force: true, completion: { _ in + dismissAsOverlayCompletion() + }) + } + + if let scheduledAnimateInAsOverlayFromNode = self.scheduledAnimateInAsOverlayFromNode { + self.scheduledAnimateInAsOverlayFromNode = nil + self.bounds = CGRect(origin: CGPoint(), size: self.bounds.size) + let animatedTransition: ContainedViewLayoutTransition + if case .animated = protoTransition { + animatedTransition = protoTransition + } else { + animatedTransition = .animated(duration: 0.4, curve: .spring) + } + self.performAnimateInAsOverlay(from: scheduledAnimateInAsOverlayFromNode, transition: animatedTransition) + } } private func chatPresentationInterfaceStateRequiresInputFocus(_ state: ChatPresentationInterfaceState) -> Bool { @@ -633,6 +955,8 @@ class ChatControllerNode: ASDisplayNode { } func updateChatPresentationInterfaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, animated: Bool, interactive: Bool) { + self.selectedMessages = chatPresentationInterfaceState.interfaceState.selectionState?.selectedIds + if let textInputPanelNode = self.textInputPanelNode { self.chatPresentationInterfaceState = self.chatPresentationInterfaceState.updatedInterfaceState { $0.withUpdatedEffectiveInputState(textInputPanelNode.inputTextState) } } @@ -646,9 +970,10 @@ class ChatControllerNode: ASDisplayNode { let keepSendButtonEnabled = chatPresentationInterfaceState.interfaceState.forwardMessageIds != nil || chatPresentationInterfaceState.interfaceState.editMessage != nil var extendedSearchLayout = false - if let inputQueryResult = chatPresentationInterfaceState.inputQueryResult { - if case .contextRequestResult = inputQueryResult { + loop: for (_, result) in chatPresentationInterfaceState.inputQueryResults { + if case let .contextRequestResult(peer, _) = result, peer != nil { extendedSearchLayout = true + break loop } } @@ -751,14 +1076,166 @@ class ChatControllerNode: ASDisplayNode { func loadInputPanels(theme: PresentationTheme, strings: PresentationStrings) { if self.inputMediaNode == nil { - let inputNode = ChatMediaInputNode(account: self.account, controllerInteraction: self.controllerInteraction, theme: theme, strings: strings) + let inputNode = ChatMediaInputNode(account: self.account, controllerInteraction: self.controllerInteraction, theme: theme, strings: strings, gifPaneIsActiveUpdated: { [weak self] value in + if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction { + interfaceInteraction.updateInputModeAndDismissedButtonKeyboardMessageId { state in + if case .media = state.inputMode { + if value { + return (.media(.gif), nil) + } else { + return (.media(.other), nil) + } + } else { + return (state.inputMode, nil) + } + } + } + }) inputNode.interfaceInteraction = interfaceInteraction self.inputMediaNode = inputNode - let _ = inputNode.updateLayout(width: self.bounds.size.width, transition: .immediate, interfaceState: self.chatPresentationInterfaceState) + if let validLayout = self.validLayout { + let _ = inputNode.updateLayout(width: validLayout.size.width, leftInset: validLayout.safeInsets.left, rightInset: validLayout.safeInsets.right, bottomInset: validLayout.intrinsicInsets.bottom, transition: .immediate, interfaceState: self.chatPresentationInterfaceState) + } } } func currentInputPanelFrame() -> CGRect? { return self.inputPanelNode?.frame } + + var isTextInputPanelActive: Bool { + return self.inputPanelNode is ChatTextInputPanelNode + } + + func getWindowInputAccessoryHeight() -> CGFloat { + var height = self.inputPanelBackgroundNode.bounds.size.height + if case .overlay = self.chatPresentationInterfaceState.mode { + height += 8.0 + } + return height + } + + func animateInAsOverlay(from fromNode: ASDisplayNode?, completion: @escaping () -> Void) { + if let inputPanelNode = self.inputPanelNode as? ChatTextInputPanelNode, let fromNode = fromNode { + if inputPanelNode.isFocused { + self.performAnimateInAsOverlay(from: fromNode, transition: .animated(duration: 0.4, curve: .spring)) + completion() + } else { + self.animateInAsOverlayCompletion = completion + self.bounds = CGRect(origin: CGPoint(x: -self.bounds.size.width * 2.0, y: 0.0), size: self.bounds.size) + self.scheduledAnimateInAsOverlayFromNode = fromNode + self.scheduleLayoutTransitionRequest(.immediate) + inputPanelNode.ensureFocused() + } + } else { + self.performAnimateInAsOverlay(from: fromNode, transition: .animated(duration: 0.4, curve: .spring)) + completion() + } + } + + private func performAnimateInAsOverlay(from fromNode: ASDisplayNode?, transition: ContainedViewLayoutTransition) { + if let containerBackgroundNode = self.containerBackgroundNode, let fromNode = fromNode { + let fromFrame = fromNode.view.convert(fromNode.bounds, to: self.view) + containerBackgroundNode.supernode?.insertSubnode(fromNode, aboveSubnode: containerBackgroundNode) + fromNode.frame = fromFrame + + fromNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak fromNode] _ in + fromNode?.removeFromSupernode() + }) + + transition.animateFrame(node: containerBackgroundNode, from: CGRect(origin: fromFrame.origin.offsetBy(dx: -8.0, dy: -8.0), size: CGSize(width: fromFrame.size.width + 8.0 * 2.0, height: fromFrame.size.height + 8.0 + 20.0))) + containerBackgroundNode.layer.animateSpring(from: 0.99 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5, initialVelocity: 1.0, damping: 10.0, removeOnCompletion: true, additive: false, completion: nil) + + if let containerNode = self.containerNode { + containerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + transition.animateFrame(node: containerNode, from: fromFrame) + transition.animatePositionAdditive(node: self.backgroundNode, offset: -containerNode.bounds.size.height) + transition.animatePositionAdditive(node: self.historyNode, offset: -containerNode.bounds.size.height) + + transition.updateFrame(node: fromNode, frame: CGRect(origin: containerNode.frame.origin, size: fromNode.frame.size)) + } + + self.backgroundEffectNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + + let inputPanelsOffset = self.bounds.size.height - self.inputPanelBackgroundNode.frame.minY + transition.animateFrame(node: self.inputPanelBackgroundNode, from: self.inputPanelBackgroundNode.frame.offsetBy(dx: 0.0, dy: inputPanelsOffset)) + transition.animateFrame(node: self.inputPanelBackgroundSeparatorNode, from: self.inputPanelBackgroundSeparatorNode.frame.offsetBy(dx: 0.0, dy: inputPanelsOffset)) + if let inputPanelNode = self.inputPanelNode { + transition.animateFrame(node: inputPanelNode, from: inputPanelNode.frame.offsetBy(dx: 0.0, dy: inputPanelsOffset)) + } + if let accessoryPanelNode = self.accessoryPanelNode { + transition.animateFrame(node: accessoryPanelNode, from: accessoryPanelNode.frame.offsetBy(dx: 0.0, dy: inputPanelsOffset)) + } + + if let _ = self.scrollContainerNode { + containerBackgroundNode.layer.animateSpring(from: 0.99 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.8, initialVelocity: 100.0, damping: 80.0, removeOnCompletion: true, additive: false, completion: nil) + self.containerNode?.layer.animateSpring(from: 0.99 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.8, initialVelocity: 100.0, damping: 80.0, removeOnCompletion: true, additive: false, completion: nil) + } + + self.navigateButtons.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + } else { + self.backgroundEffectNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + if let containerNode = self.containerNode { + containerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } + + if let animateInAsOverlayCompletion = self.animateInAsOverlayCompletion { + self.animateInAsOverlayCompletion = nil + animateInAsOverlayCompletion() + } + } + + func animateDismissAsOverlay(completion: @escaping () -> Void) { + if let containerNode = self.containerNode { + self.dismissedAsOverlay = true + self.dismissAsOverlayLayout = self.validLayout + + self.backgroundEffectNode?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.27, removeOnCompletion: false) + + self.containerBackgroundNode?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.27, removeOnCompletion: false) + self.containerBackgroundNode?.layer.animateScale(from: 1.0, to: 0.6, duration: 0.29, removeOnCompletion: false) + + containerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.27, removeOnCompletion: false) + containerNode.layer.animateScale(from: 1.0, to: 0.6, duration: 0.29, removeOnCompletion: false) + + self.navigateButtons.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) + + self.dismissAsOverlayCompletion = completion + self.scheduleLayoutTransitionRequest(.animated(duration: 0.4, curve: .spring)) + self.dismissInput() + } else { + completion() + } + } + + func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { + if let scrollContainerNode = self.scrollContainerNode, scrollView === scrollContainerNode.view { + if abs(scrollView.contentOffset.y) > 50.0 { + scrollView.isScrollEnabled = false + self.dismissAsOverlay() + } + } + } + + func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + if let scrollContainerNode = self.scrollContainerNode, scrollView === scrollContainerNode.view { + if self.hapticFeedback == nil { + self.hapticFeedback = HapticFeedback() + } + self.hapticFeedback?.prepareImpact() + } + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + if let scrollContainerNode = self.scrollContainerNode, scrollView === scrollContainerNode.view { + let dismissStatus = abs(scrollView.contentOffset.y) > 50.0 + if dismissStatus != self.scrollViewDismissStatus { + self.scrollViewDismissStatus = dismissStatus + if !self.dismissedAsOverlay { + self.hapticFeedback?.impact() + } + } + } + } } diff --git a/TelegramUI/ChatDocumentGalleryItem.swift b/TelegramUI/ChatDocumentGalleryItem.swift index e4a06855ab..c4bc9c0bd8 100644 --- a/TelegramUI/ChatDocumentGalleryItem.swift +++ b/TelegramUI/ChatDocumentGalleryItem.swift @@ -54,6 +54,9 @@ class ChatDocumentGalleryItem: GalleryItem { class ChatDocumentGalleryItemNode: GalleryItemNode { fileprivate let _title = Promise() + private let statusNodeContainer: HighlightableButtonNode + private let statusNode: RadialStatusNode + private let webView: UIView private var accountAndFile: (Account, TelegramMediaFile)? @@ -65,6 +68,10 @@ class ChatDocumentGalleryItemNode: GalleryItemNode { private let footerContentNode: ChatItemGalleryFooterContentNode + private var fetchDisposable = MetaDisposable() + private let statusDisposable = MetaDisposable() + private var status: MediaResourceStatus? + init(account: Account, theme: PresentationTheme, strings: PresentationStrings) { if #available(iOS 9.0, *) { let webView = WKWebView() @@ -78,19 +85,36 @@ class ChatDocumentGalleryItemNode: GalleryItemNode { } self.footerContentNode = ChatItemGalleryFooterContentNode(account: account, theme: theme, strings: strings) + self.statusNodeContainer = HighlightableButtonNode() + self.statusNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.5)) + self.statusNode.isHidden = true + super.init() self.view.addSubview(self.webView) + + self.statusNodeContainer.addSubnode(self.statusNode) + self.addSubnode(self.statusNodeContainer) + + self.statusNodeContainer.addTarget(self, action: #selector(self.statusPressed), forControlEvents: .touchUpInside) + + self.statusNodeContainer.isUserInteractionEnabled = false } deinit { self.dataDisposable.dispose() + self.fetchDisposable.dispose() + self.statusDisposable.dispose() } override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) - self.webView.frame = CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight), size: CGSize(width: layout.size.width, height: layout.size.height - navigationBarHeight)) + self.webView.frame = CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight), size: CGSize(width: layout.size.width, height: layout.size.height - navigationBarHeight - 44.0 - layout.insets(options: []).bottom)) + + let statusSize = CGSize(width: 50.0, height: 50.0) + transition.updateFrame(node: self.statusNodeContainer, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - statusSize.width) / 2.0), y: floor((layout.size.height - statusSize.height) / 2.0)), size: statusSize)) + transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(), size: statusSize)) } fileprivate func setMessage(_ message: Message) { @@ -98,7 +122,7 @@ class ChatDocumentGalleryItemNode: GalleryItemNode { } override func navigationStyle() -> Signal { - return .single(.light) + return .single(.dark) } func setFile(account: Account, file: TelegramMediaFile) { @@ -106,9 +130,58 @@ class ChatDocumentGalleryItemNode: GalleryItemNode { self.accountAndFile = (account, file) if updateFile { self.maybeLoadContent() + self.setupStatus(account: account, resource: file.resource) } } + private func setupStatus(account: Account, resource: MediaResource) { + self.statusDisposable.set((account.postbox.mediaBox.resourceStatus(resource) + |> deliverOnMainQueue).start(next: { [weak self] status in + if let strongSelf = self { + let previousStatus = strongSelf.status + strongSelf.status = status + switch status { + case .Remote: + strongSelf.statusNode.isHidden = false + strongSelf.statusNode.alpha = 1.0 + strongSelf.statusNodeContainer.isUserInteractionEnabled = true + strongSelf.statusNode.transitionToState(.download(.white), completion: {}) + case let .Fetching(isActive, progress): + strongSelf.statusNode.isHidden = false + strongSelf.statusNode.alpha = 1.0 + strongSelf.statusNodeContainer.isUserInteractionEnabled = true + var actualProgress = progress + if isActive { + actualProgress = max(actualProgress, 0.027) + } + strongSelf.statusNode.transitionToState(.progress(color: .white, value: CGFloat(actualProgress), cancelEnabled: true), completion: {}) + case .Local: + if let previousStatus = previousStatus, case .Fetching = previousStatus { + strongSelf.statusNode.transitionToState(.progress(color: .white, value: 1.0, cancelEnabled: true), completion: { + if let strongSelf = self { + strongSelf.statusNode.alpha = 0.0 + strongSelf.statusNodeContainer.isUserInteractionEnabled = false + strongSelf.statusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { _ in + if let strongSelf = self { + strongSelf.statusNode.transitionToState(.none, animated: false, completion: {}) + } + }) + } + }) + } else if !strongSelf.statusNode.isHidden && !strongSelf.statusNode.alpha.isZero { + strongSelf.statusNode.alpha = 0.0 + strongSelf.statusNodeContainer.isUserInteractionEnabled = false + strongSelf.statusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { _ in + if let strongSelf = self { + strongSelf.statusNode.transitionToState(.none, animated: false, completion: {}) + } + }) + } + } + } + })) + } + private func maybeLoadContent() { if let (account, file) = self.accountAndFile { var pathExtension: String? @@ -119,7 +192,7 @@ class ChatDocumentGalleryItemNode: GalleryItemNode { |> deliverOnMainQueue self.dataDisposable.set(data.start(next: { [weak self] data in if let strongSelf = self { - if data.size == file.size { + if data.complete { if let webView = strongSelf.webView as? WKWebView { if #available(iOS 9.0, *) { webView.loadFileURL(URL(fileURLWithPath: data.path), allowingReadAccessTo: URL(fileURLWithPath: data.path)) @@ -143,15 +216,14 @@ class ChatDocumentGalleryItemNode: GalleryItemNode { override func visibilityUpdated(isVisible: Bool) { super.visibilityUpdated(isVisible: isVisible) - /*if self.isVisible != isVisible { - self.isVisible = isVisible + if self.itemIsVisible != isVisible { + self.itemIsVisible = isVisible if isVisible { - self.maybeLoadContent() } else { - self.unloadContent() + self.fetchDisposable.set(nil) } - }*/ + } } override func title() -> Signal { @@ -168,6 +240,10 @@ class ChatDocumentGalleryItemNode: GalleryItemNode { let transform = CATransform3DScale(self.webView.layer.transform, transformedFrame.size.width / self.webView.layer.bounds.size.width, transformedFrame.size.height / self.webView.layer.bounds.size.height, 1.0) self.webView.layer.animate(from: NSValue(caTransform3D: transform), to: NSValue(caTransform3D: self.webView.layer.transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25) + + self.statusNodeContainer.layer.animatePosition(from: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), to: self.statusNodeContainer.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) + self.statusNodeContainer.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) + self.statusNodeContainer.layer.animateScale(from: 0.5, to: 1.0, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) } override func animateOut(to node: ASDisplayNode, addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) { @@ -215,9 +291,25 @@ class ChatDocumentGalleryItemNode: GalleryItemNode { boundsCompleted = true intermediateCompletion() }) + + self.statusNodeContainer.layer.animatePosition(from: self.statusNodeContainer.position, to: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + self.statusNodeContainer.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, timingFunction: kCAMediaTimingFunctionEaseIn, removeOnCompletion: false) } override func footerContent() -> Signal { return .single(self.footerContentNode) } + + @objc func statusPressed() { + if let (account, file) = self.accountAndFile, let status = self.status { + switch status { + case .Fetching: + account.postbox.mediaBox.cancelInteractiveResourceFetch(file.resource) + case .Remote: + self.fetchDisposable.set(account.postbox.mediaBox.fetchedResource(file.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .generic)).start()) + default: + break + } + } + } } diff --git a/TelegramUI/ChatEmptyItem.swift b/TelegramUI/ChatEmptyItem.swift index e11e8a7ba0..358b4e47bf 100644 --- a/TelegramUI/ChatEmptyItem.swift +++ b/TelegramUI/ChatEmptyItem.swift @@ -18,12 +18,12 @@ final class ChatEmptyItem: ListViewItem { self.tagMask = tagMask } - func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { let configure = { let node = ChatEmptyItemNode(rotated: self.tagMask == nil) let nodeLayout = node.asyncLayout() - let (layout, apply) = nodeLayout(self, width) + let (layout, apply) = nodeLayout(self, params) node.contentSize = layout.contentSize node.insets = layout.insets @@ -41,13 +41,13 @@ final class ChatEmptyItem: ListViewItem { } } - func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { if let node = node as? ChatEmptyItemNode { Queue.mainQueue().async { let nodeLayout = node.asyncLayout() async { - let (layout, apply) = nodeLayout(self, width) + let (layout, apply) = nodeLayout(self, params) Queue.mainQueue().async { completion(layout, { apply(animation) @@ -94,10 +94,11 @@ final class ChatEmptyItemNode: ListViewItemNode { self.wantsTrailingItemSpaceUpdates = true } - func asyncLayout() -> (_ item: ChatEmptyItem, _ width: CGFloat) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { + func asyncLayout() -> (_ item: ChatEmptyItem, _ params: ListViewItemLayoutParams) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { let makeTextLayout = TextNode.asyncLayout(self.textNode) let currentTheme = self.theme - return { [weak self] item, width in + return { [weak self] item, params in + let width = params.width var updatedBackgroundImage: UIImage? let iconImage: UIImage? = PresentationResourcesChat.chatEmptyItemIconImage(item.theme) @@ -132,7 +133,7 @@ final class ChatEmptyItemNode: ListViewItemNode { } let imageSpacing: CGFloat = 10.0 - let (textLayout, textApply) = makeTextLayout(attributedText, nil, 0, .end, CGSize(width: width - horizontalEdgeInset * 2.0 - horizontalContentInset * 2.0, height: CGFloat.greatestFiniteMagnitude), .center, nil, UIEdgeInsets()) + let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: width - horizontalEdgeInset * 2.0 - horizontalContentInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) let contentWidth = max(textLayout.size.width, 120.0) diff --git a/TelegramUI/ChatFeedNavigationInputPanelNode.swift b/TelegramUI/ChatFeedNavigationInputPanelNode.swift new file mode 100644 index 0000000000..0d76bb6501 --- /dev/null +++ b/TelegramUI/ChatFeedNavigationInputPanelNode.swift @@ -0,0 +1,68 @@ +import Foundation +import AsyncDisplayKit +import Display +import TelegramCore +import Postbox +import SwiftSignalKit + +final class ChatFeedNavigationInputPanelNode: ChatInputPanelNode { + private let button: HighlightableButtonNode + + private var presentationInterfaceState: ChatPresentationInterfaceState? + + private var theme: PresentationTheme + private var strings: PresentationStrings + + init(theme: PresentationTheme, strings: PresentationStrings) { + self.theme = theme + self.strings = strings + + self.button = HighlightableButtonNode() + + super.init() + + self.addSubnode(self.button) + + self.button.setAttributedTitle(NSAttributedString(string: "Show Next", font: Font.regular(17.0), textColor: theme.chat.inputPanel.panelControlAccentColor), for: []) + self.button.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: [.touchUpInside]) + } + + deinit { + } + + func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { + if self.theme !== theme || self.strings !== strings { + self.theme = theme + self.strings = strings + + self.button.setAttributedTitle(NSAttributedString(string: strings.Conversation_Unblock, font: Font.regular(17.0), textColor: theme.chat.inputPanel.panelControlAccentColor), for: []) + } + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if self.bounds.contains(point) { + return self.button.view + } else { + return nil + } + } + + @objc func buttonPressed() { + self.interfaceInteraction?.navigateFeed() + } + + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { + if self.presentationInterfaceState != interfaceState { + self.presentationInterfaceState = interfaceState + } + + let buttonSize = self.button.measure(CGSize(width: width - leftInset - rightInset - 80.0, height: 100.0)) + + let panelHeight: CGFloat = 47.0 + + self.button.frame = CGRect(origin: CGPoint(x: leftInset + floor((width - leftInset - rightInset - buttonSize.width) / 2.0), y: floor((panelHeight - buttonSize.height) / 2.0)), size: buttonSize) + + return 47.0 + } +} + diff --git a/TelegramUI/ChatHistoryEntriesForView.swift b/TelegramUI/ChatHistoryEntriesForView.swift index b06f2ebe66..5e363f869d 100644 --- a/TelegramUI/ChatHistoryEntriesForView.swift +++ b/TelegramUI/ChatHistoryEntriesForView.swift @@ -2,44 +2,73 @@ import Foundation import Postbox import TelegramCore -func chatHistoryEntriesForView(_ view: MessageHistoryView, includeUnreadEntry: Bool, includeEmptyEntry: Bool, includeChatInfoEntry: Bool, includeSearchEntry: Bool, theme: PresentationTheme, strings: PresentationStrings) -> [ChatHistoryEntry] { +func chatHistoryEntriesForView(_ view: MessageHistoryView, includeUnreadEntry: Bool, includeEmptyEntry: Bool, includeChatInfoEntry: Bool, includeSearchEntry: Bool, reverse: Bool, groupMessages: Bool, selectedMessages: Set?, presentationData: ChatPresentationData) -> [ChatHistoryEntry] { var entries: [ChatHistoryEntry] = [] + var groupBucket: [(Message, Bool, ChatHistoryMessageSelection)] = [] for entry in view.entries { switch entry { case let .HoleEntry(hole, _): + if !groupBucket.isEmpty { + entries.append(.MessageGroupEntry(groupBucket[0].0.groupInfo!, groupBucket, presentationData)) + groupBucket.removeAll() + } if view.tagMask == nil { - entries.append(.HoleEntry(hole, theme, strings)) + entries.append(.HoleEntry(hole, presentationData.theme, presentationData.strings)) } case let .MessageEntry(message, read, _, monthLocation): - var isClearHistory = false - if !message.media.isEmpty { - if let action = message.media[0] as? TelegramMediaAction, case .historyCleared = action.action { - isClearHistory = true + if groupMessages { + if !groupBucket.isEmpty && message.groupInfo != groupBucket[0].0.groupInfo { + entries.append(.MessageGroupEntry(groupBucket[0].0.groupInfo!, groupBucket, presentationData)) + groupBucket.removeAll() } - } - if !isClearHistory { - entries.append(.MessageEntry(message, theme, strings, read, monthLocation)) + if let _ = message.groupInfo { + let selection: ChatHistoryMessageSelection + if let selectedMessages = selectedMessages { + selection = .selectable(selected: selectedMessages.contains(message.id)) + } else { + selection = .none + } + groupBucket.append((message, read, selection)) + } else { + let selection: ChatHistoryMessageSelection + if let selectedMessages = selectedMessages { + selection = .selectable(selected: selectedMessages.contains(message.id)) + } else { + selection = .none + } + entries.append(.MessageEntry(message, presentationData, read, monthLocation, selection)) + } + } else { + let selection: ChatHistoryMessageSelection + if let selectedMessages = selectedMessages { + selection = .selectable(selected: selectedMessages.contains(message.id)) + } else { + selection = .none + } + entries.append(.MessageEntry(message, presentationData, read, monthLocation, selection)) } } } + if !groupBucket.isEmpty { + assert(groupMessages) + entries.append(.MessageGroupEntry(groupBucket[0].0.groupInfo!, groupBucket, presentationData)) + } + if let maxReadIndex = view.maxReadIndex, includeUnreadEntry { - var inserted = false var i = 0 - let unreadEntry: ChatHistoryEntry = .UnreadEntry(maxReadIndex, theme, strings) + let unreadEntry: ChatHistoryEntry = .UnreadEntry(maxReadIndex, presentationData.theme, presentationData.strings) for entry in entries { if entry > unreadEntry { - entries.insert(unreadEntry, at: i) - inserted = true - + if i == 0, case .HoleEntry = entry { + } else { + entries.insert(unreadEntry, at: i) + } break } i += 1 } - if !inserted { - //entries.append(.UnreadEntry(maxReadIndex)) - } } if includeChatInfoEntry { @@ -52,9 +81,9 @@ func chatHistoryEntriesForView(_ view: MessageHistoryView, includeUnreadEntry: B } } if let cachedPeerData = cachedPeerData as? CachedUserData, let botInfo = cachedPeerData.botInfo, !botInfo.description.isEmpty { - entries.insert(.ChatInfoEntry(botInfo.description, theme, strings), at: 0) + entries.insert(.ChatInfoEntry(botInfo.description, presentationData.theme, presentationData.strings), at: 0) } else if view.entries.isEmpty && includeEmptyEntry { - entries.insert(.EmptyChatInfoEntry(theme, strings, view.tagMask), at: 0) + entries.insert(.EmptyChatInfoEntry(presentationData.theme, presentationData.strings, view.tagMask), at: 0) } } } else if includeSearchEntry { @@ -67,14 +96,14 @@ func chatHistoryEntriesForView(_ view: MessageHistoryView, includeUnreadEntry: B } } if hasMessages { - entries.append(.SearchEntry(theme, strings)) - } else if view.entries.isEmpty { - if view.tagMask != nil { - entries.insert(.EmptyChatInfoEntry(theme, strings, view.tagMask), at: 0) - } + entries.append(.SearchEntry(presentationData.theme, presentationData.strings)) } } } - return entries + if reverse { + return entries.reversed() + } else { + return entries + } } diff --git a/TelegramUI/ChatHistoryEntry.swift b/TelegramUI/ChatHistoryEntry.swift index 77ff56f5bd..17470d5131 100644 --- a/TelegramUI/ChatHistoryEntry.swift +++ b/TelegramUI/ChatHistoryEntry.swift @@ -1,9 +1,32 @@ import Postbox import TelegramCore +public enum ChatHistoryMessageSelection: Equatable { + case none + case selectable(selected: Bool) + + public static func ==(lhs: ChatHistoryMessageSelection, rhs: ChatHistoryMessageSelection) -> Bool { + switch lhs { + case .none: + if case .none = rhs { + return true + } else { + return false + } + case let .selectable(selected): + if case .selectable(selected) = rhs { + return true + } else { + return false + } + } + } +} + enum ChatHistoryEntry: Identifiable, Comparable { case HoleEntry(MessageHistoryHole, PresentationTheme, PresentationStrings) - case MessageEntry(Message, PresentationTheme, PresentationStrings, Bool, MessageHistoryEntryMonthLocation?) + case MessageEntry(Message, ChatPresentationData, Bool, MessageHistoryEntryMonthLocation?, ChatHistoryMessageSelection) + case MessageGroupEntry(MessageGroupInfo, [(Message, Bool, ChatHistoryMessageSelection)], ChatPresentationData) case UnreadEntry(MessageIndex, PresentationTheme, PresentationStrings) case ChatInfoEntry(String, PresentationTheme, PresentationStrings) case EmptyChatInfoEntry(PresentationTheme, PresentationStrings, MessageTags?) @@ -15,6 +38,8 @@ enum ChatHistoryEntry: Identifiable, Comparable { return UInt64(hole.stableId) | ((UInt64(1) << 40)) case let .MessageEntry(message, _, _, _, _): return UInt64(message.stableId) | ((UInt64(2) << 40)) + case let .MessageGroupEntry(groupInfo, _, _): + return UInt64(groupInfo.stableId) | ((UInt64(2) << 40)) case .UnreadEntry: return UInt64(3) << 40 case .ChatInfoEntry: @@ -32,6 +57,8 @@ enum ChatHistoryEntry: Identifiable, Comparable { return hole.maxIndex case let .MessageEntry(message, _, _, _, _): return MessageIndex(message) + case let .MessageGroupEntry(_, messages, _): + return MessageIndex(messages[messages.count - 1].0) case let .UnreadEntry(index, _, _): return index case .ChatInfoEntry: @@ -51,13 +78,10 @@ enum ChatHistoryEntry: Identifiable, Comparable { } else { return false } - case let .MessageEntry(lhsMessage, lhsTheme, lhsStrings, lhsRead, _): + case let .MessageEntry(lhsMessage, lhsPresentationData, lhsRead, _, lhsSelection): switch rhs { - case let .MessageEntry(rhsMessage, rhsTheme, rhsStrings, rhsRead, _) where MessageIndex(lhsMessage) == MessageIndex(rhsMessage) && lhsMessage.flags == rhsMessage.flags && lhsRead == rhsRead: - if lhsTheme !== rhsTheme { - return false - } - if lhsStrings !== rhsStrings { + case let .MessageEntry(rhsMessage, rhsPresentationData, rhsRead, _, rhsSelection) where MessageIndex(lhsMessage) == MessageIndex(rhsMessage) && lhsMessage.flags == rhsMessage.flags && lhsRead == rhsRead: + if lhsPresentationData !== rhsPresentationData { return false } if lhsMessage.stableVersion != rhsMessage.stableVersion { @@ -83,10 +107,66 @@ enum ChatHistoryEntry: Identifiable, Comparable { } } } + if lhsSelection != rhsSelection { + return false + } return true default: return false } + case let .MessageGroupEntry(lhsGroupInfo, lhsMessages, lhsPresentationData): + if case let .MessageGroupEntry(rhsGroupInfo, rhsMessages, rhsPresentationData) = rhs, lhsGroupInfo == rhsGroupInfo, lhsPresentationData === rhsPresentationData, lhsMessages.count == rhsMessages.count { + for i in 0 ..< lhsMessages.count { + let (lhsMessage, lhsRead, lhsSelection) = lhsMessages[i] + let (rhsMessage, rhsRead, rhsSelection) = rhsMessages[i] + + if lhsMessage.id != rhsMessage.id { + return false + } + if lhsMessage.timestamp != rhsMessage.timestamp { + return false + } + if lhsMessage.flags != rhsMessage.flags { + return false + } + if lhsRead != rhsRead { + return false + } + if lhsSelection != rhsSelection { + return false + } + if lhsPresentationData !== rhsPresentationData { + return false + } + if lhsMessage.stableVersion != rhsMessage.stableVersion { + return false + } + if lhsMessage.media.count != rhsMessage.media.count { + return false + } + for i in 0 ..< lhsMessage.media.count { + if !lhsMessage.media[i].isEqual(rhsMessage.media[i]) { + return false + } + } + if lhsMessage.associatedMessages.count != rhsMessage.associatedMessages.count { + return false + } + if !lhsMessage.associatedMessages.isEmpty { + for (id, message) in lhsMessage.associatedMessages { + if let otherMessage = rhsMessage.associatedMessages[id] { + if otherMessage.stableVersion != message.stableVersion { + return false + } + } + } + } + } + + return true + } else { + return false + } case let .UnreadEntry(lhsIndex, lhsTheme, lhsStrings): if case let .UnreadEntry(rhsIndex, rhsTheme, rhsStrings) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings { return true diff --git a/TelegramUI/ChatHistoryGridNode.swift b/TelegramUI/ChatHistoryGridNode.swift index ad1b3ca50c..c84ab4483e 100644 --- a/TelegramUI/ChatHistoryGridNode.swift +++ b/TelegramUI/ChatHistoryGridNode.swift @@ -20,6 +20,8 @@ private func mappedInsertEntries(account: Account, peerId: PeerId, controllerInt switch entry.entry { case let .MessageEntry(message, _, _, _, _): return GridNodeInsertItem(index: entry.index, item: GridMessageItem(theme: theme, strings: strings, account: account, message: message, controllerInteraction: controllerInteraction), previousIndex: entry.previousIndex) + case .MessageGroupEntry: + return GridNodeInsertItem(index: entry.index, item: GridHoleItem(), previousIndex: entry.previousIndex) case .HoleEntry: return GridNodeInsertItem(index: entry.index, item: GridHoleItem(), previousIndex: entry.previousIndex) case .UnreadEntry: @@ -37,6 +39,8 @@ private func mappedUpdateEntries(account: Account, peerId: PeerId, controllerInt switch entry.entry { case let .MessageEntry(message, _, _, _, _): return GridNodeUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: GridMessageItem(theme: theme, strings: strings, account: account, message: message, controllerInteraction: controllerInteraction)) + case .MessageGroupEntry: + return GridNodeUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: GridHoleItem()) case .HoleEntry: return GridNodeUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: GridHoleItem()) case .UnreadEntry: @@ -49,7 +53,7 @@ private func mappedUpdateEntries(account: Account, peerId: PeerId, controllerInt } } -private func mappedChatHistoryViewListTransition(account: Account, peerId: PeerId, controllerInteraction: ChatControllerInteraction, transition: ChatHistoryViewTransition, from: ChatHistoryView?, theme: PresentationTheme, strings: PresentationStrings) -> ChatHistoryGridViewTransition { +private func mappedChatHistoryViewListTransition(account: Account, peerId: PeerId, controllerInteraction: ChatControllerInteraction, transition: ChatHistoryViewTransition, from: ChatHistoryView?, presentationData: ChatPresentationData) -> ChatHistoryGridViewTransition { var mappedScrollToItem: GridNodeScrollToItem? if let scrollToItem = transition.scrollToItem { let mappedPosition: GridNodeScrollToItemPosition @@ -120,7 +124,7 @@ private func mappedChatHistoryViewListTransition(account: Account, peerId: PeerI var topOffsetWithinMonth: Int = 0 if let lastEntry = transition.historyView.filteredEntries.last { switch lastEntry { - case let .MessageEntry(_, _, _, _, monthLocation): + case let .MessageEntry(_, _, _, monthLocation, _): if let monthLocation = monthLocation { topOffsetWithinMonth = Int(monthLocation.indexInMonth) } @@ -129,7 +133,7 @@ private func mappedChatHistoryViewListTransition(account: Account, peerId: PeerI } } - return ChatHistoryGridViewTransition(historyView: transition.historyView, topOffsetWithinMonth: topOffsetWithinMonth, deleteItems: transition.deleteItems.map { $0.index }, insertItems: mappedInsertEntries(account: account, peerId: peerId, controllerInteraction: controllerInteraction, entries: transition.insertEntries, theme: theme, strings: strings), updateItems: mappedUpdateEntries(account: account, peerId: peerId, controllerInteraction: controllerInteraction, entries: transition.updateEntries, theme: theme, strings: strings), scrollToItem: mappedScrollToItem, stationaryItems: stationaryItems) + return ChatHistoryGridViewTransition(historyView: transition.historyView, topOffsetWithinMonth: topOffsetWithinMonth, deleteItems: transition.deleteItems.map { $0.index }, insertItems: mappedInsertEntries(account: account, peerId: peerId, controllerInteraction: controllerInteraction, entries: transition.insertEntries, theme: presentationData.theme, strings: presentationData.strings), updateItems: mappedUpdateEntries(account: account, peerId: peerId, controllerInteraction: controllerInteraction, entries: transition.updateEntries, theme: presentationData.theme, strings: presentationData.strings), scrollToItem: mappedScrollToItem, stationaryItems: stationaryItems) } private func itemSizeForContainerLayout(size: CGSize) -> CGSize { @@ -172,7 +176,7 @@ public final class ChatHistoryGridNode: GridNode, ChatHistoryNode { private let galleryHiddenMesageAndMediaDisposable = MetaDisposable() private var presentationData: PresentationData - private let themeAndStringsPromise = Promise<(PresentationTheme, PresentationStrings)>() + private let chatPresentationDataPromise = Promise() public private(set) var loadState: ChatHistoryNodeLoadState? private var loadStateUpdated: ((ChatHistoryNodeLoadState) -> Void)? @@ -187,7 +191,7 @@ public final class ChatHistoryGridNode: GridNode, ChatHistoryNode { super.init() - self.themeAndStringsPromise.set(.single((self.presentationData.theme, self.presentationData.strings))) + self.chatPresentationDataPromise.set(.single((ChatPresentationData(theme: self.presentationData.theme, fontSize: self.presentationData.fontSize, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, timeFormat: self.presentationData.timeFormat)))) self.floatingSections = true @@ -198,12 +202,12 @@ public final class ChatHistoryGridNode: GridNode, ChatHistoryNode { let historyViewUpdate = self.chatHistoryLocation |> distinctUntilChanged |> mapToSignal { location in - return chatHistoryViewForLocation(location, account: account, peerId: peerId, fixedCombinedReadState: nil, tagMask: tagMask, additionalData: [], orderStatistics: [.locationWithinMonth]) + return chatHistoryViewForLocation(location, account: account, chatLocation: .peer(peerId), fixedCombinedReadStates: nil, tagMask: tagMask, additionalData: [], orderStatistics: [.locationWithinMonth]) } let previousView = Atomic(value: nil) - let historyViewTransition = combineLatest(historyViewUpdate, self.themeAndStringsPromise.get()) |> mapToQueue { [weak self] update, themeAndStrings -> Signal in + let historyViewTransition = combineLatest(historyViewUpdate, self.chatPresentationDataPromise.get()) |> mapToQueue { [weak self] update, chatPresentationData -> Signal in switch update { case .Loading: Queue.mainQueue().async { [weak self] in @@ -242,10 +246,10 @@ public final class ChatHistoryGridNode: GridNode, ChatHistoryNode { } } - let processedView = ChatHistoryView(originalView: view, filteredEntries: chatHistoryEntriesForView(view, includeUnreadEntry: false, includeEmptyEntry: false, includeChatInfoEntry: false, includeSearchEntry: false, theme: themeAndStrings.0, strings: themeAndStrings.1)) + let processedView = ChatHistoryView(originalView: view, filteredEntries: chatHistoryEntriesForView(view, includeUnreadEntry: false, includeEmptyEntry: false, includeChatInfoEntry: false, includeSearchEntry: false, reverse: false, groupMessages: false, selectedMessages: nil, presentationData: chatPresentationData)) let previous = previousView.swap(processedView) - return preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, account: account, peerId: peerId, controllerInteraction: controllerInteraction, scrollPosition: scrollPosition, initialData: nil, keyboardButtonsMessage: nil, cachedData: nil, cachedDataMessages: nil, readStateData: nil) |> map({ mappedChatHistoryViewListTransition(account: account, peerId: peerId, controllerInteraction: controllerInteraction, transition: $0, from: previous, theme: themeAndStrings.0, strings: themeAndStrings.1) }) |> runOn(prepareOnMainQueue ? Queue.mainQueue() : messageViewQueue) + return preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, reverse: false, account: account, chatLocation: .peer(peerId), controllerInteraction: controllerInteraction, scrollPosition: scrollPosition, initialData: nil, keyboardButtonsMessage: nil, cachedData: nil, cachedDataMessages: nil, readStateData: nil) |> map({ mappedChatHistoryViewListTransition(account: account, peerId: peerId, controllerInteraction: controllerInteraction, transition: $0, from: previous, presentationData: chatPresentationData) }) |> runOn(prepareOnMainQueue ? Queue.mainQueue() : messageViewQueue) } } @@ -265,42 +269,16 @@ public final class ChatHistoryGridNode: GridNode, ChatHistoryNode { } self.visibleItemsUpdated = { [weak self] visibleItems in - if let strongSelf = self, let historyView = strongSelf.historyView, let top = visibleItems.top, let bottom = visibleItems.bottom { + if let strongSelf = self, let historyView = strongSelf.historyView, let top = visibleItems.top, let bottom = visibleItems.bottom, let visibleTop = visibleItems.topVisible, let visibleBottom = visibleItems.bottomVisible { if top.0 < 5 && historyView.originalView.laterId != nil { - let lastEntry = historyView.filteredEntries[historyView.filteredEntries.count - 1 - top.0] - strongSelf._chatHistoryLocation.set(ChatHistoryLocation.Navigation(index: lastEntry.index, anchorIndex: historyView.originalView.anchorIndex)) + let lastEntry = historyView.filteredEntries[historyView.filteredEntries.count - 1 - visibleTop.0] + strongSelf._chatHistoryLocation.set(ChatHistoryLocation.Navigation(index: .message(lastEntry.index), anchorIndex: .message(lastEntry.index), count: 200)) } else if bottom.0 >= historyView.filteredEntries.count - 5 && historyView.originalView.earlierId != nil { - let firstEntry = historyView.filteredEntries[historyView.filteredEntries.count - 1 - bottom.0] - strongSelf._chatHistoryLocation.set(ChatHistoryLocation.Navigation(index: firstEntry.index, anchorIndex: historyView.originalView.anchorIndex)) + let firstEntry = historyView.filteredEntries[historyView.filteredEntries.count - 1 - visibleBottom.0] + strongSelf._chatHistoryLocation.set(ChatHistoryLocation.Navigation(index: .message(firstEntry.index), anchorIndex: .message(firstEntry.index), count: 200)) } } } - - /*self.displayedItemRangeChanged = { [weak self] displayedRange in - if let strongSelf = self { - /*if let transactionTag = strongSelf.listViewTransactionTag { - strongSelf.messageViewQueue.dispatch { - if transactionTag == strongSelf.historyViewTransactionTag { - if let range = range, historyView = strongSelf.historyView, firstEntry = historyView.filteredEntries.first, lastEntry = historyView.filteredEntries.last { - if range.firstIndex < 5 && historyView.originalView.laterId != nil { - strongSelf._chatHistoryLocation.set(.single(ChatHistoryLocation.Navigation(index: lastEntry.index, anchorIndex: historyView.originalView.anchorIndex))) - } else if range.lastIndex >= historyView.filteredEntries.count - 5 && historyView.originalView.earlierId != nil { - strongSelf._chatHistoryLocation.set(.single(ChatHistoryLocation.Navigation(index: firstEntry.index, anchorIndex: historyView.originalView.anchorIndex))) - } else { - //strongSelf.account.postbox.updateMessageHistoryViewVisibleRange(messageView.id, earliestVisibleIndex: viewEntries[viewEntries.count - 1 - range.lastIndex].index, latestVisibleIndex: viewEntries[viewEntries.count - 1 - range.firstIndex].index) - } - } - } - } - }*/ - - if let visible = displayedRange.visibleRange, let historyView = strongSelf.historyView { - if let messageId = maxIncomingMessageIdForEntries(historyView.filteredEntries, indexRange: (historyView.filteredEntries.count - 1 - visible.lastIndex, historyView.filteredEntries.count - 1 - visible.firstIndex)) { - strongSelf.updateMaxVisibleReadIncomingMessageId(messageId) - } - } - } - }*/ } required public init?(coder aDecoder: NSCoder) { @@ -316,15 +294,15 @@ public final class ChatHistoryGridNode: GridNode, ChatHistoryNode { } public func scrollToStartOfHistory() { - self._chatHistoryLocation.set(ChatHistoryLocation.Scroll(index: MessageIndex.lowerBound(peerId: self.peerId), anchorIndex: MessageIndex.lowerBound(peerId: self.peerId), sourceIndex: MessageIndex.upperBound(peerId: self.peerId), scrollPosition: .bottom(0.0), animated: true)) + self._chatHistoryLocation.set(ChatHistoryLocation.Scroll(index: .lowerBound, anchorIndex: .lowerBound, sourceIndex: .upperBound, scrollPosition: .bottom(0.0), animated: true)) } public func scrollToEndOfHistory() { - self._chatHistoryLocation.set(ChatHistoryLocation.Scroll(index: MessageIndex.upperBound(peerId: self.peerId), anchorIndex: MessageIndex.upperBound(peerId: self.peerId), sourceIndex: MessageIndex.lowerBound(peerId: self.peerId), scrollPosition: .top(0.0), animated: true)) + self._chatHistoryLocation.set(ChatHistoryLocation.Scroll(index: .upperBound, anchorIndex: .upperBound, sourceIndex: .lowerBound, scrollPosition: .top(0.0), animated: true)) } public func scrollToMessage(from fromIndex: MessageIndex, to toIndex: MessageIndex) { - self._chatHistoryLocation.set(ChatHistoryLocation.Scroll(index: toIndex, anchorIndex: toIndex, sourceIndex: fromIndex, scrollPosition: .center(.bottom), animated: true)) + self._chatHistoryLocation.set(ChatHistoryLocation.Scroll(index: .message(toIndex), anchorIndex: .message(toIndex), sourceIndex: .message(fromIndex), scrollPosition: .center(.bottom), animated: true)) } public func messageInCurrentHistoryView(_ id: MessageId) -> Message? { diff --git a/TelegramUI/ChatHistoryListNode.swift b/TelegramUI/ChatHistoryListNode.swift index 322273600f..4ce4b8d1bf 100644 --- a/TelegramUI/ChatHistoryListNode.swift +++ b/TelegramUI/ChatHistoryListNode.swift @@ -5,15 +5,32 @@ import Display import AsyncDisplayKit import TelegramCore -public enum ChatHistoryListMode { +public enum ChatHistoryListMode: Equatable { case bubbles - case list + case list(search: Bool, reversed: Bool) + + public static func ==(lhs: ChatHistoryListMode, rhs: ChatHistoryListMode) -> Bool { + switch lhs { + case .bubbles: + if case .bubbles = rhs { + return true + } else { + return false + } + case let .list(search, reversed): + if case .list(search, reversed) = rhs { + return true + } else { + return false + } + } + } } enum ChatHistoryViewScrollPosition { case unread(index: MessageIndex) case positionRestoration(index: MessageIndex, relativeOffset: CGFloat) - case index(index: MessageIndex, position: ListViewScrollPosition, directionHint: ListViewScrollToItemDirectionHint, animated: Bool) + case index(index: MessageHistoryAnchorIndex, position: ListViewScrollPosition, directionHint: ListViewScrollToItemDirectionHint, animated: Bool) } enum ChatHistoryViewUpdateType { @@ -32,7 +49,7 @@ public struct ChatHistoryCombinedInitialData { let buttonKeyboardMessage: Message? let cachedData: CachedPeerData? let cachedDataMessages: [MessageId: Message]? - let readStateData: ChatHistoryCombinedInitialReadStateData? + let readStateData: [PeerId: ChatHistoryCombinedInitialReadStateData]? } enum ChatHistoryViewUpdate { @@ -78,8 +95,8 @@ struct ChatHistoryViewTransition { let keyboardButtonsMessage: Message? let cachedData: CachedPeerData? let cachedDataMessages: [MessageId: Message]? - let readStateData: ChatHistoryCombinedInitialReadStateData? - let scrolledToIndex: MessageIndex? + let readStateData: [PeerId: ChatHistoryCombinedInitialReadStateData]? + let scrolledToIndex: MessageHistoryAnchorIndex? let animateIn: Bool } @@ -95,8 +112,8 @@ struct ChatHistoryListViewTransition { let keyboardButtonsMessage: Message? let cachedData: CachedPeerData? let cachedDataMessages: [MessageId: Message]? - let readStateData: ChatHistoryCombinedInitialReadStateData? - let scrolledToIndex: MessageIndex? + let readStateData: [PeerId: ChatHistoryCombinedInitialReadStateData]? + let scrolledToIndex: MessageHistoryAnchorIndex? let animateIn: Bool } @@ -110,21 +127,39 @@ private func maxMessageIndexForEntries(_ entries: [ChatHistoryEntry], indexRange if message.flags.contains(.Incoming) { return (MessageIndex(message), overall) } + } else if case let .MessageGroupEntry(_, messages, _) = entries[i] { + let index = MessageIndex(messages[messages.count - 1].0) + if overall == nil { + overall = index + } + if messages[messages.count - 1].0.flags.contains(.Incoming) { + return (index, overall) + } } } return (nil, overall) } -private func mappedInsertEntries(account: Account, peerId: PeerId, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, entries: [ChatHistoryViewTransitionInsertEntry]) -> [ListViewInsertItem] { +private func mappedInsertEntries(account: Account, chatLocation: ChatLocation, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, entries: [ChatHistoryViewTransitionInsertEntry]) -> [ListViewInsertItem] { return entries.map { entry -> ListViewInsertItem in switch entry.entry { - case let .MessageEntry(message, theme, strings, read, _): + case let .MessageEntry(message, presentationData, read, _, selection): let item: ListViewItem switch mode { case .bubbles: - item = ChatMessageItem(theme: theme, strings: strings, account: account, peerId: peerId, controllerInteraction: controllerInteraction, message: message, read: read) - case .list: - item = ListMessageItem(theme: theme, account: account, peerId: peerId, controllerInteraction: controllerInteraction, message: message) + item = ChatMessageItem(presentationData: presentationData, account: account, chatLocation: chatLocation, controllerInteraction: controllerInteraction, content: .message(message: message, read: read, selection: selection)) + case let .list(search, _): + item = ListMessageItem(theme: presentationData.theme, strings: presentationData.strings, account: account, chatLocation: chatLocation, controllerInteraction: controllerInteraction, message: message, selection: selection, displayHeader: search) + } + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: item, directionHint: entry.directionHint) + case let .MessageGroupEntry(_, messages, presentationData): + let item: ListViewItem + switch mode { + case .bubbles: + item = ChatMessageItem(presentationData: presentationData, account: account, chatLocation: chatLocation, controllerInteraction: controllerInteraction, content: .group(messages: messages)) + case let .list(search, _): + assertionFailure() + item = ListMessageItem(theme: presentationData.theme, strings: presentationData.strings, account: account, chatLocation: chatLocation, controllerInteraction: controllerInteraction, message: messages[0].0, selection: .none, displayHeader: search) } return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: item, directionHint: entry.directionHint) case let .HoleEntry(_, theme, strings): @@ -150,16 +185,26 @@ private func mappedInsertEntries(account: Account, peerId: PeerId, controllerInt } } -private func mappedUpdateEntries(account: Account, peerId: PeerId, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, entries: [ChatHistoryViewTransitionUpdateEntry]) -> [ListViewUpdateItem] { +private func mappedUpdateEntries(account: Account, chatLocation: ChatLocation, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, entries: [ChatHistoryViewTransitionUpdateEntry]) -> [ListViewUpdateItem] { return entries.map { entry -> ListViewUpdateItem in switch entry.entry { - case let .MessageEntry(message, theme, strings, read, _): + case let .MessageEntry(message, presentationData, read, _, selection): let item: ListViewItem switch mode { case .bubbles: - item = ChatMessageItem(theme: theme, strings: strings, account: account, peerId: peerId, controllerInteraction: controllerInteraction, message: message, read: read) - case .list: - item = ListMessageItem(theme: theme, account: account, peerId: peerId, controllerInteraction: controllerInteraction, message: message) + item = ChatMessageItem(presentationData: presentationData, account: account, chatLocation: chatLocation, controllerInteraction: controllerInteraction, content: .message(message: message, read: read, selection: selection)) + case let .list(search, _): + item = ListMessageItem(theme: presentationData.theme, strings: presentationData.strings, account: account, chatLocation: chatLocation, controllerInteraction: controllerInteraction, message: message, selection: selection, displayHeader: search) + } + return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: item, directionHint: entry.directionHint) + case let .MessageGroupEntry(_, messages, presentationData): + let item: ListViewItem + switch mode { + case .bubbles: + item = ChatMessageItem(presentationData: presentationData, account: account, chatLocation: chatLocation, controllerInteraction: controllerInteraction, content: .group(messages: messages)) + case let .list(search, _): + assertionFailure() + item = ListMessageItem(theme: presentationData.theme, strings: presentationData.strings, account: account, chatLocation: chatLocation, controllerInteraction: controllerInteraction, message: messages[0].0, selection: .none, displayHeader: search) } return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: item, directionHint: entry.directionHint) case let .HoleEntry(_, theme, strings): @@ -185,8 +230,8 @@ private func mappedUpdateEntries(account: Account, peerId: PeerId, controllerInt } } -private func mappedChatHistoryViewListTransition(account: Account, peerId: PeerId, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, transition: ChatHistoryViewTransition) -> ChatHistoryListViewTransition { - return ChatHistoryListViewTransition(historyView: transition.historyView, deleteItems: transition.deleteItems, insertItems: mappedInsertEntries(account: account, peerId: peerId, controllerInteraction: controllerInteraction, mode: mode, entries: transition.insertEntries), updateItems: mappedUpdateEntries(account: account, peerId: peerId, controllerInteraction: controllerInteraction, mode: mode, entries: transition.updateEntries), options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, initialData: transition.initialData, keyboardButtonsMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData, cachedDataMessages: transition.cachedDataMessages, readStateData: transition.readStateData, scrolledToIndex: transition.scrolledToIndex, animateIn: transition.animateIn) +private func mappedChatHistoryViewListTransition(account: Account, chatLocation: ChatLocation, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, transition: ChatHistoryViewTransition) -> ChatHistoryListViewTransition { + return ChatHistoryListViewTransition(historyView: transition.historyView, deleteItems: transition.deleteItems, insertItems: mappedInsertEntries(account: account, chatLocation: chatLocation, controllerInteraction: controllerInteraction, mode: mode, entries: transition.insertEntries), updateItems: mappedUpdateEntries(account: account, chatLocation: chatLocation, controllerInteraction: controllerInteraction, mode: mode, entries: transition.updateEntries), options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, initialData: transition.initialData, keyboardButtonsMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData, cachedDataMessages: transition.cachedDataMessages, readStateData: transition.readStateData, scrolledToIndex: transition.scrolledToIndex, animateIn: transition.animateIn) } private final class ChatHistoryTransactionOpaqueState { @@ -199,7 +244,7 @@ private final class ChatHistoryTransactionOpaqueState { public final class ChatHistoryListNode: ListView, ChatHistoryNode { private let account: Account - private let peerId: PeerId + private let chatLocation: ChatLocation private let messageId: MessageId? private let tagMask: MessageTags? private let controllerInteraction: ChatControllerInteraction @@ -254,13 +299,15 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { private var maxVisibleMessageIndexReported: MessageIndex? var maxVisibleMessageIndexUpdated: ((MessageIndex) -> Void)? - var scrolledToIndex: ((MessageIndex) -> Void)? + var scrolledToIndex: ((MessageHistoryAnchorIndex) -> Void)? private var currentPresentationData: PresentationData - private var themeAndStrings: Promise<(PresentationTheme, PresentationStrings)> + private var chatPresentationDataPromise: Promise private var presentationDataDisposable: Disposable? - private var isScrollAtBottomPosition = false + private(set) var isScrollAtBottomPosition = false + public var isScrollAtBottomPositionUpdated: (() -> Void)? + private var interactiveReadActionDisposable: Disposable? public var contentPositionChanged: (ListViewVisibleContentOffset) -> Void = { _ in } @@ -270,9 +317,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { private var loadedMessagesFromCachedDataDisposable: Disposable? - public init(account: Account, peerId: PeerId, tagMask: MessageTags?, messageId: MessageId?, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode = .bubbles) { + public init(account: Account, chatLocation: ChatLocation, tagMask: MessageTags?, messageId: MessageId?, controllerInteraction: ChatControllerInteraction, selectedMessages: Signal?, NoError>, mode: ChatHistoryListMode = .bubbles) { self.account = account - self.peerId = peerId + self.chatLocation = chatLocation self.messageId = messageId self.tagMask = tagMask self.controllerInteraction = controllerInteraction @@ -280,7 +327,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { self.currentPresentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } - self.themeAndStrings = Promise((self.currentPresentationData.theme, self.currentPresentationData.strings)) + self.chatPresentationDataPromise = Promise(ChatPresentationData(theme: self.currentPresentationData.theme, fontSize: self.currentPresentationData.fontSize, strings: self.currentPresentationData.strings, wallpaper: self.currentPresentationData.chatWallpaper, timeFormat: self.currentPresentationData.timeFormat)) super.init() @@ -306,21 +353,23 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { let messageViewQueue = self.messageViewQueue - let fixedCombinedReadState = Atomic(value: nil) + let fixedCombinedReadStates = Atomic(value: nil) var additionalData: [AdditionalMessageHistoryViewData] = [] - additionalData.append(.cachedPeerData(peerId)) - additionalData.append(.cachedPeerDataMessages(peerId)) + if case let .peer(peerId) = chatLocation { + additionalData.append(.cachedPeerData(peerId)) + additionalData.append(.cachedPeerDataMessages(peerId)) + additionalData.append(.peerNotificationSettings(peerId)) + } additionalData.append(.totalUnreadCount) - additionalData.append(.peerNotificationSettings(peerId)) let historyViewUpdate = self.chatHistoryLocation |> distinctUntilChanged |> mapToSignal { location in - return chatHistoryViewForLocation(location, account: account, peerId: peerId, fixedCombinedReadState: fixedCombinedReadState.with { $0 }, tagMask: tagMask, additionalData: additionalData) |> beforeNext { viewUpdate in + return chatHistoryViewForLocation(location, account: account, chatLocation: chatLocation, fixedCombinedReadStates: fixedCombinedReadStates.with { $0 }, tagMask: tagMask, additionalData: additionalData) |> beforeNext { viewUpdate in switch viewUpdate { case let .HistoryView(view, _, _, _): - let _ = fixedCombinedReadState.swap(view.combinedReadState) + let _ = fixedCombinedReadStates.swap(view.combinedReadStates) default: break } @@ -329,7 +378,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { let previousView = Atomic(value: nil) - let historyViewTransition = combineLatest(historyViewUpdate, self.themeAndStrings.get()) |> mapToQueue { [weak self] update, themeAndStrings -> Signal in + let historyViewTransition = combineLatest(historyViewUpdate, self.chatPresentationDataPromise.get(), selectedMessages) |> mapToQueue { [weak self] update, chatPresentationData, selectedMessages -> Signal in let initialData: ChatHistoryCombinedInitialData? switch update { case let .Loading(combinedInitialData): @@ -359,29 +408,45 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { return .complete() case let .HistoryView(view, type, scrollPosition, data): initialData = data - let reason: ChatHistoryViewTransitionReason - var prepareOnMainQueue = false - switch type { - case let .Initial(fadeIn): - reason = ChatHistoryViewTransitionReason.Initial(fadeIn: fadeIn) - prepareOnMainQueue = !fadeIn - case let .Generic(genericType): - switch genericType { - case .InitialUnread: - reason = ChatHistoryViewTransitionReason.Initial(fadeIn: false) - case .Generic: - reason = ChatHistoryViewTransitionReason.InteractiveChanges - case .UpdateVisible: - reason = ChatHistoryViewTransitionReason.Reload - case let .FillHole(insertions, deletions): - reason = ChatHistoryViewTransitionReason.HoleChanges(filledHoleDirections: insertions, removeHoleDirections: deletions) - } + + var updatedScrollPosition = scrollPosition + + var reverse = false + var includeSearchEntry = false + if case let .list(search, reverseValue) = mode { + includeSearchEntry = search + reverse = reverseValue } - let processedView = ChatHistoryView(originalView: view, filteredEntries: chatHistoryEntriesForView(view, includeUnreadEntry: mode == .bubbles, includeEmptyEntry: mode == .bubbles && tagMask == nil, includeChatInfoEntry: mode == .bubbles, includeSearchEntry: mode == .list && tagMask == nil, theme: themeAndStrings.0, strings: themeAndStrings.1)) + let processedView = ChatHistoryView(originalView: view, filteredEntries: chatHistoryEntriesForView(view, includeUnreadEntry: mode == .bubbles, includeEmptyEntry: mode == .bubbles && tagMask == nil, includeChatInfoEntry: mode == .bubbles, includeSearchEntry: includeSearchEntry && tagMask != nil, reverse: reverse, groupMessages: mode == .bubbles, selectedMessages: selectedMessages, presentationData: chatPresentationData)) let previous = previousView.swap(processedView) - return preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, account: account, peerId: peerId, controllerInteraction: controllerInteraction, scrollPosition: scrollPosition, initialData: initialData?.initialData, keyboardButtonsMessage: view.topTaggedMessages.first, cachedData: initialData?.cachedData, cachedDataMessages: initialData?.cachedDataMessages, readStateData: initialData?.readStateData) |> map({ mappedChatHistoryViewListTransition(account: account, peerId: peerId, controllerInteraction: controllerInteraction, mode: mode, transition: $0) }) |> runOn(prepareOnMainQueue ? Queue.mainQueue() : messageViewQueue) + let reason: ChatHistoryViewTransitionReason + var prepareOnMainQueue = false + + if let previous = previous, previous.originalView.entries == processedView.originalView.entries { + reason = ChatHistoryViewTransitionReason.InteractiveChanges + updatedScrollPosition = nil + } else { + switch type { + case let .Initial(fadeIn): + reason = ChatHistoryViewTransitionReason.Initial(fadeIn: fadeIn) + prepareOnMainQueue = !fadeIn + case let .Generic(genericType): + switch genericType { + case .InitialUnread: + reason = ChatHistoryViewTransitionReason.Initial(fadeIn: false) + case .Generic: + reason = ChatHistoryViewTransitionReason.InteractiveChanges + case .UpdateVisible: + reason = ChatHistoryViewTransitionReason.Reload + case let .FillHole(insertions, deletions): + reason = ChatHistoryViewTransitionReason.HoleChanges(filledHoleDirections: insertions, removeHoleDirections: deletions) + } + } + } + + return preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, reverse: reverse, account: account, chatLocation: chatLocation, controllerInteraction: controllerInteraction, scrollPosition: updatedScrollPosition, initialData: initialData?.initialData, keyboardButtonsMessage: view.topTaggedMessages.first, cachedData: initialData?.cachedData, cachedDataMessages: initialData?.cachedDataMessages, readStateData: initialData?.readStateData) |> map({ mappedChatHistoryViewListTransition(account: account, chatLocation: chatLocation, controllerInteraction: controllerInteraction, mode: mode, transition: $0) }) |> runOn(prepareOnMainQueue ? Queue.mainQueue() : messageViewQueue) } } @@ -410,7 +475,14 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { return dict } if apply { - let _ = applyMaxReadIndexInteractively(postbox: account.postbox, network: account.network, index: messageIndex).start() + switch chatLocation { + case .peer: + let _ = applyMaxReadIndexInteractively(postbox: account.postbox, network: account.network, index: messageIndex).start() + case let .group(groupId): + let _ = account.postbox.modify({ modifier -> Void in + modifier.applyGroupFeedInteractiveReadMaxIndex(groupId: groupId, index: messageIndex) + }).start() + } } } } @@ -486,9 +558,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { if let loaded = displayedRange.loadedRange, let firstEntry = historyView.filteredEntries.first, let lastEntry = historyView.filteredEntries.last { if loaded.firstIndex < 5 && historyView.originalView.laterId != nil { - strongSelf._chatHistoryLocation.set(ChatHistoryLocation.Navigation(index: lastEntry.index, anchorIndex: historyView.originalView.anchorIndex)) + strongSelf._chatHistoryLocation.set(ChatHistoryLocation.Navigation(index: .message(lastEntry.index), anchorIndex: .message(lastEntry.index), count: 140)) } else if loaded.lastIndex >= historyView.filteredEntries.count - 5 && historyView.originalView.earlierId != nil { - strongSelf._chatHistoryLocation.set(ChatHistoryLocation.Navigation(index: firstEntry.index, anchorIndex: historyView.originalView.anchorIndex)) + strongSelf._chatHistoryLocation.set(ChatHistoryLocation.Navigation(index: .message(firstEntry.index), anchorIndex: .message(firstEntry.index), count: 140)) } } } @@ -509,7 +581,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { dateNode.updateThemeAndStrings(theme: presentationData.theme, strings: presentationData.strings) } } - strongSelf.themeAndStrings.set(.single((presentationData.theme, presentationData.strings))) + strongSelf.chatPresentationDataPromise.set(.single(ChatPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings, wallpaper: presentationData.chatWallpaper, timeFormat: presentationData.timeFormat))) } } }) @@ -532,6 +604,8 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { if atBottom != strongSelf.isScrollAtBottomPosition { strongSelf.isScrollAtBottomPosition = atBottom strongSelf.updateReadHistoryActions() + + strongSelf.isScrollAtBottomPositionUpdated?() } } } @@ -562,7 +636,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } public func scrollToStartOfHistory() { - self._chatHistoryLocation.set(ChatHistoryLocation.Scroll(index: MessageIndex.lowerBound(peerId: self.peerId), anchorIndex: MessageIndex.lowerBound(peerId: self.peerId), sourceIndex: MessageIndex.upperBound(peerId: self.peerId), scrollPosition: .bottom(0.0), animated: true)) + self._chatHistoryLocation.set(ChatHistoryLocation.Scroll(index: .lowerBound, anchorIndex: .lowerBound, sourceIndex: .upperBound, scrollPosition: .bottom(0.0), animated: true)) } public func scrollToEndOfHistory() { @@ -570,12 +644,12 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { case .known(0.0): break default: - self._chatHistoryLocation.set(ChatHistoryLocation.Scroll(index: MessageIndex.upperBound(peerId: self.peerId), anchorIndex: MessageIndex.upperBound(peerId: self.peerId), sourceIndex: MessageIndex.lowerBound(peerId: self.peerId), scrollPosition: .top(0.0), animated: true)) + self._chatHistoryLocation.set(ChatHistoryLocation.Scroll(index: .upperBound, anchorIndex: .upperBound, sourceIndex: .lowerBound, scrollPosition: .top(0.0), animated: true)) } } - public func scrollToMessage(from fromIndex: MessageIndex, to toIndex: MessageIndex) { - self._chatHistoryLocation.set(ChatHistoryLocation.Scroll(index: toIndex, anchorIndex: toIndex, sourceIndex: fromIndex, scrollPosition: .center(.bottom), animated: true)) + public func scrollToMessage(from fromIndex: MessageIndex, to toIndex: MessageIndex, animated: Bool) { + self._chatHistoryLocation.set(ChatHistoryLocation.Scroll(index: .message(toIndex), anchorIndex: .message(toIndex), sourceIndex: .message(fromIndex), scrollPosition: .center(.bottom), animated: animated)) } public func anchorMessageInCurrentHistoryView() -> Message? { @@ -601,13 +675,60 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { public func messageInCurrentHistoryView(_ id: MessageId) -> Message? { if let historyView = self.historyView { - for case let .MessageEntry(message, _, _, _, _) in historyView.filteredEntries where message.id == id { - return message + for entry in historyView.filteredEntries { + if case let .MessageEntry(message, _, _, _, _) = entry { + if message.id == id { + return message + } + } else if case let .MessageGroupEntry(_, messages, _) = entry { + for (message, _, _) in messages { + if message.id == id { + return message + } + } + } } } return nil } + public func messageGroupInCurrentHistoryView(_ id: MessageId) -> [Message]? { + if let historyView = self.historyView { + for entry in historyView.filteredEntries { + if case let .MessageEntry(message, _, _, _, _) = entry { + if message.id == id { + return [message] + } + } else if case let .MessageGroupEntry(_, messages, _) = entry { + for (message, _, _) in messages { + if message.id == id { + return messages.map { $0.0 } + } + } + } + } + } + return nil + } + + public func forEachMessageInCurrentHistoryView(_ f: (Message) -> Bool) { + if let historyView = self.historyView { + for entry in historyView.filteredEntries { + if case let .MessageEntry(message, _, _, _, _) = entry { + if !f(message) { + return + } + } else if case let .MessageGroupEntry(_, messages, _) = entry { + for (message, _, _) in messages { + if !f(message) { + return + } + } + } + } + } + } + private func updateMaxVisibleReadIncomingMessageIndex(_ index: MessageIndex) { self.maxVisibleIncomingMessageIndex.set(index) } @@ -741,7 +862,15 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } public func updateLayout(transition: ContainedViewLayoutTransition, updateSizeAndInsets: ListViewUpdateSizeAndInsets) { - self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + self.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets, additionalScrollDistance: 0.0, scrollToTop: false) + } + + public func updateLayout(transition: ContainedViewLayoutTransition, updateSizeAndInsets: ListViewUpdateSizeAndInsets, additionalScrollDistance: CGFloat, scrollToTop: Bool) { + var scrollToItem: ListViewScrollToItem? + if scrollToTop, case .known = self.visibleContentOffset() { + scrollToItem = ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Spring(duration: updateSizeAndInsets.duration), directionHint: .Up) + } + self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: scrollToItem, additionalScrollDistance: scrollToTop ? 0.0 : additionalScrollDistance, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) if !self.dequeuedInitialTransitionOnLayout { self.dequeuedInitialTransitionOnLayout = true @@ -762,7 +891,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { self.interactiveReadActionDisposable = nil } } else if self.interactiveReadActionDisposable == nil { - self.interactiveReadActionDisposable = installInteractiveReadMessagesAction(postbox: self.account.postbox, peerId: self.peerId) + if case let .peer(peerId) = self.chatLocation { + self.interactiveReadActionDisposable = installInteractiveReadMessagesAction(postbox: self.account.postbox, peerId: peerId) + } } } } @@ -779,6 +910,11 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { currentMessage = message } break loop + } else if case let .MessageGroupEntry(_, messages, _) = entry { + if index != 0 || historyView.originalView.laterId != nil { + currentMessage = messages.first?.0 + } + break loop } } index += 1 @@ -799,4 +935,51 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } return nil } + + func scrollToNextMessage() { + if let historyView = self.historyView { + var scrolled = false + if let scrollState = self.immediateScrollState() { + var index = historyView.filteredEntries.count - 1 + loop: for entry in historyView.filteredEntries.reversed() { + if entry.index == scrollState.messageIndex { + break loop + } + index -= 1 + } + + if index != 0 { + var nextItem = false + self.forEachItemNode { itemNode in + if let itemNode = itemNode as? ChatMessageItemView, itemNode.item?.content.index == scrollState.messageIndex { + if itemNode.frame.maxY >= self.bounds.size.height - self.insets.bottom - 4.0 { + nextItem = true + } + } + } + + if !nextItem { + scrolled = true + self.scrollToMessage(from: scrollState.messageIndex, to: scrollState.messageIndex, animated: true) + } else { + loop: for i in (index + 1) ..< historyView.filteredEntries.count { + let entry = historyView.filteredEntries[i] + switch entry { + case .MessageEntry, .MessageGroupEntry: + scrolled = true + self.scrollToMessage(from: scrollState.messageIndex, to: entry.index, animated: true) + break loop + default: + break + } + } + } + } + } + + if !scrolled { + self.scrollToEndOfHistory() + } + } + } } diff --git a/TelegramUI/ChatHistoryLocation.swift b/TelegramUI/ChatHistoryLocation.swift index 3b6516e384..4677c37f3c 100644 --- a/TelegramUI/ChatHistoryLocation.swift +++ b/TelegramUI/ChatHistoryLocation.swift @@ -9,15 +9,15 @@ enum ChatHistoryInitialSearchLocation { enum ChatHistoryLocation: Equatable { case Initial(count: Int) case InitialSearch(location: ChatHistoryInitialSearchLocation, count: Int) - case Navigation(index: MessageIndex, anchorIndex: MessageIndex) - case Scroll(index: MessageIndex, anchorIndex: MessageIndex, sourceIndex: MessageIndex, scrollPosition: ListViewScrollPosition, animated: Bool) + case Navigation(index: MessageHistoryAnchorIndex, anchorIndex: MessageHistoryAnchorIndex, count: Int) + case Scroll(index: MessageHistoryAnchorIndex, anchorIndex: MessageHistoryAnchorIndex, sourceIndex: MessageHistoryAnchorIndex, scrollPosition: ListViewScrollPosition, animated: Bool) } func ==(lhs: ChatHistoryLocation, rhs: ChatHistoryLocation) -> Bool { switch lhs { - case let .Navigation(lhsIndex, lhsAnchorIndex): + case let .Navigation(lhsIndex, lhsAnchorIndex, lhsCount): switch rhs { - case let .Navigation(rhsIndex, rhsAnchorIndex) where lhsIndex == rhsIndex && lhsAnchorIndex == rhsAnchorIndex: + case let .Navigation(rhsIndex, rhsAnchorIndex, rhsCount) where lhsIndex == rhsIndex && lhsAnchorIndex == rhsAnchorIndex && lhsCount == rhsCount: return true default: return false diff --git a/TelegramUI/ChatHistorySearchContainerNode.swift b/TelegramUI/ChatHistorySearchContainerNode.swift index a89051b2f4..b0b1d3648f 100644 --- a/TelegramUI/ChatHistorySearchContainerNode.swift +++ b/TelegramUI/ChatHistorySearchContainerNode.swift @@ -75,7 +75,7 @@ private enum ChatHistorySearchEntry: Comparable, Identifiable { func item(account: Account, peerId: PeerId, interaction: ChatControllerInteraction) -> ListViewItem { switch self { case let .message(message, theme, strings): - return ListMessageItem(theme: theme, account: account, peerId: peerId, controllerInteraction: interaction, message: message) + return ListMessageItem(theme: theme, strings: strings, account: account, chatLocation: .peer(peerId), controllerInteraction: interaction, message: message, selection: .none, displayHeader: true) } } } @@ -141,7 +141,7 @@ final class ChatHistorySearchContainerNode: SearchDisplayControllerContentNode { let searchItems = searchQuery.get() |> mapToSignal { query -> Signal<[ChatHistorySearchEntry]?, NoError> in if let query = query, !query.isEmpty { - let foundRemoteMessages: Signal<[Message], NoError> = searchMessages(account: account, peerId: peerId, query: query, tagMask: tagMask) + let foundRemoteMessages: Signal<[Message], NoError> = searchMessages(account: account, location: .peer(peerId: peerId, fromId: nil, tags: tagMask), query: query) |> delay(0.2, queue: Queue.concurrentDefaultQueue()) return combineLatest(foundRemoteMessages, themeAndStringsPromise.get()) @@ -206,7 +206,7 @@ final class ChatHistorySearchContainerNode: SearchDisplayControllerContentNode { transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: layout.size.width, height: layout.size.height - topInset))) self.listNode.frame = CGRect(origin: CGPoint(), size: layout.size) - self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: topInset + 2.0, left: 0.0, bottom: 0.0, right: 0.0), duration: 0.0, curve: .Default), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: topInset, left: 0.0, bottom: 0.0, right: 0.0), duration: 0.0, curve: .Default), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) if firstValidLayout { @@ -242,7 +242,7 @@ final class ChatHistorySearchContainerNode: SearchDisplayControllerContentNode { if displayingResults != !strongSelf.listNode.isHidden { strongSelf.listNode.isHidden = !displayingResults strongSelf.dimNode.isHidden = displayingResults - strongSelf.backgroundColor = displayingResults ? UIColor.white : nil + strongSelf.backgroundColor = displayingResults ? strongSelf.presentationData.theme.list.plainBackgroundColor : nil } } }) diff --git a/TelegramUI/ChatHistoryViewForLocation.swift b/TelegramUI/ChatHistoryViewForLocation.swift index 0852abff87..627ba4d2ca 100644 --- a/TelegramUI/ChatHistoryViewForLocation.swift +++ b/TelegramUI/ChatHistoryViewForLocation.swift @@ -4,54 +4,28 @@ import TelegramCore import SwiftSignalKit import Display -func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Account, peerId: PeerId, fixedCombinedReadState: CombinedPeerReadState?, tagMask: MessageTags?, additionalData: [AdditionalMessageHistoryViewData], orderStatistics: MessageHistoryViewOrderStatistics = []) -> Signal { +func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Account, chatLocation: ChatLocation, fixedCombinedReadStates: MessageHistoryViewReadState?, tagMask: MessageTags?, additionalData: [AdditionalMessageHistoryViewData], orderStatistics: MessageHistoryViewOrderStatistics = []) -> Signal { switch location { case let .Initial(count): var preloaded = false var fadeIn = false let signal: Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> if let tagMask = tagMask { - signal = account.viewTracker.aroundMessageHistoryViewForPeerId(peerId, index: MessageIndex.upperBound(peerId: peerId), count: count, anchorIndex: MessageIndex.upperBound(peerId: peerId), fixedCombinedReadState: nil, tagMask: tagMask, orderStatistics: orderStatistics) + signal = account.viewTracker.aroundMessageHistoryViewForLocation(chatLocation, index: .upperBound, anchorIndex: .upperBound, count: count, fixedCombinedReadStates: nil, tagMask: tagMask, orderStatistics: orderStatistics) } else { - signal = account.viewTracker.aroundMessageOfInterestHistoryViewForPeerId(peerId, count: count, tagMask: tagMask, orderStatistics: orderStatistics, additionalData: additionalData) + signal = account.viewTracker.aroundMessageOfInterestHistoryViewForLocation(chatLocation, count: count, tagMask: tagMask, orderStatistics: orderStatistics, additionalData: additionalData) } return signal |> map { view, updateType, initialData -> ChatHistoryViewUpdate in - var cachedData: CachedPeerData? - var cachedDataMessages: [MessageId: Message]? - var readStateData: ChatHistoryCombinedInitialReadStateData? - var notificationSettings: PeerNotificationSettings? - for data in view.additionalData { - switch data { - case let .peerNotificationSettings(value): - notificationSettings = value - default: - break - } - } - for data in view.additionalData { - switch data { - case let .cachedPeerData(peerIdValue, value): - if peerIdValue == peerId { - cachedData = value - } - case let .cachedPeerDataMessages(peerIdValue, value): - if peerIdValue == peerId { - cachedDataMessages = value - } - case let .totalUnreadCount(totalUnreadCount): - if let readState = view.combinedReadState { - readStateData = ChatHistoryCombinedInitialReadStateData(unreadCount: readState.count, totalUnreadCount: totalUnreadCount, notificationSettings: notificationSettings) - } - default: - break - } - } + let (cachedData, cachedDataMessages, readStateData) = extractAdditionalData(view: view, chatLocation: chatLocation) let combinedInitialData = ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData) if preloaded { return .HistoryView(view: view, type: .Generic(type: updateType), scrollPosition: nil, initialData: combinedInitialData) } else { + if view.isLoading { + return .Loading(initialData: combinedInitialData) + } var scrollPosition: ChatHistoryViewScrollPosition? if let maxReadIndex = view.maxReadIndex, tagMask == nil { @@ -69,7 +43,7 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun let maxIndex = min(view.entries.count, targetIndex + count / 2) if maxIndex >= targetIndex { for i in targetIndex ..< maxIndex { - if case .HoleEntry = view.entries[i] { + if case let .HoleEntry(hole) = view.entries[i] { var incomingCount: Int32 = 0 inner: for entry in view.entries.reversed() { switch entry { @@ -81,8 +55,7 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun } } } - if let combinedReadState = view.combinedReadState, combinedReadState.count == incomingCount { - + if let combinedReadStates = view.combinedReadStates, case let .peer(readStates) = combinedReadStates, let readState = readStates[hole.0.maxIndex.id.peerId], readState.count == incomingCount { } else { fadeIn = true return .Loading(initialData: combinedInitialData) @@ -118,42 +91,13 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun let signal: Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> switch searchLocation { case let .index(index): - signal = account.viewTracker.aroundMessageHistoryViewForPeerId(peerId, index: index, count: count, anchorIndex: index, fixedCombinedReadState: nil, tagMask: tagMask, orderStatistics: orderStatistics, additionalData: additionalData) + signal = account.viewTracker.aroundMessageHistoryViewForLocation(chatLocation, index: .message(index), anchorIndex: .message(index), count: count, fixedCombinedReadStates: nil, tagMask: tagMask, orderStatistics: orderStatistics, additionalData: additionalData) case let .id(id): - signal = account.viewTracker.aroundIdMessageHistoryViewForPeerId(peerId, count: count, messageId: id, tagMask: tagMask, orderStatistics: orderStatistics, additionalData: additionalData) + signal = account.viewTracker.aroundIdMessageHistoryViewForLocation(chatLocation, count: count, messageId: id, tagMask: tagMask, orderStatistics: orderStatistics, additionalData: additionalData) } return signal |> map { view, updateType, initialData -> ChatHistoryViewUpdate in - var cachedData: CachedPeerData? - var cachedDataMessages: [MessageId: Message]? - var readStateData: ChatHistoryCombinedInitialReadStateData? - var notificationSettings: PeerNotificationSettings? - for data in view.additionalData { - switch data { - case let .peerNotificationSettings(value): - notificationSettings = value - default: - break - } - } - for data in view.additionalData { - switch data { - case let .cachedPeerData(peerIdValue, value): - if peerIdValue == peerId { - cachedData = value - } - case let .cachedPeerDataMessages(peerIdValue, value): - if peerIdValue == peerId { - cachedDataMessages = value - } - case let .totalUnreadCount(totalUnreadCount): - if let readState = view.combinedReadState { - readStateData = ChatHistoryCombinedInitialReadStateData(unreadCount: readState.count, totalUnreadCount: totalUnreadCount, notificationSettings: notificationSettings) - } - default: - break - } - } + let (cachedData, cachedDataMessages, readStateData) = extractAdditionalData(view: view, chatLocation: chatLocation) let combinedInitialData = ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData) @@ -164,7 +108,7 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun var targetIndex = 0 for i in 0 ..< view.entries.count { - if view.entries[i].index >= anchorIndex { + if anchorIndex.isLessOrEqual(to: view.entries[i].index) { targetIndex = i break } @@ -185,39 +129,10 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun return .HistoryView(view: view, type: .Initial(fadeIn: fadeIn), scrollPosition: .index(index: anchorIndex, position: .center(.bottom), directionHint: .Down, animated: false), initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData)) } } - case let .Navigation(index, anchorIndex): + case let .Navigation(index, anchorIndex, count): var first = true - return account.viewTracker.aroundMessageHistoryViewForPeerId(peerId, index: index, count: 140, anchorIndex: anchorIndex, fixedCombinedReadState: fixedCombinedReadState, tagMask: tagMask, orderStatistics: orderStatistics, additionalData: additionalData) |> map { view, updateType, initialData -> ChatHistoryViewUpdate in - var cachedData: CachedPeerData? - var cachedDataMessages: [MessageId: Message]? - var readStateData: ChatHistoryCombinedInitialReadStateData? - var notificationSettings: PeerNotificationSettings? - for data in view.additionalData { - switch data { - case let .peerNotificationSettings(value): - notificationSettings = value - default: - break - } - } - for data in view.additionalData { - switch data { - case let .cachedPeerData(peerIdValue, value): - if peerIdValue == peerId { - cachedData = value - } - case let .cachedPeerDataMessages(peerIdValue, value): - if peerIdValue == peerId { - cachedDataMessages = value - } - case let .totalUnreadCount(totalUnreadCount): - if let readState = view.combinedReadState { - readStateData = ChatHistoryCombinedInitialReadStateData(unreadCount: readState.count, totalUnreadCount: totalUnreadCount, notificationSettings: notificationSettings) - } - default: - break - } - } + return account.viewTracker.aroundMessageHistoryViewForLocation(chatLocation, index: index, anchorIndex: anchorIndex, count: count, fixedCombinedReadStates: fixedCombinedReadStates, tagMask: tagMask, orderStatistics: orderStatistics, additionalData: additionalData) |> map { view, updateType, initialData -> ChatHistoryViewUpdate in + let (cachedData, cachedDataMessages, readStateData) = extractAdditionalData(view: view, chatLocation: chatLocation) let genericType: ViewUpdateType if first { @@ -226,43 +141,19 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun } else { genericType = updateType } + /*print("————————") + print("entries for navigation around \(index)") + print("--------") + print("\(view.entries.map { $0.index.id })") + print("————————")*/ return .HistoryView(view: view, type: .Generic(type: genericType), scrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData)) } case let .Scroll(index, anchorIndex, sourceIndex, scrollPosition, animated): let directionHint: ListViewScrollToItemDirectionHint = sourceIndex > index ? .Down : .Up let chatScrollPosition = ChatHistoryViewScrollPosition.index(index: index, position: scrollPosition, directionHint: directionHint, animated: animated) var first = true - return account.viewTracker.aroundMessageHistoryViewForPeerId(peerId, index: index, count: 140, anchorIndex: anchorIndex, fixedCombinedReadState: fixedCombinedReadState, tagMask: tagMask, orderStatistics: orderStatistics, additionalData: additionalData) |> map { view, updateType, initialData -> ChatHistoryViewUpdate in - var cachedData: CachedPeerData? - var cachedDataMessages: [MessageId: Message]? - var readStateData: ChatHistoryCombinedInitialReadStateData? - var notificationSettings: PeerNotificationSettings? - for data in view.additionalData { - switch data { - case let .peerNotificationSettings(value): - notificationSettings = value - default: - break - } - } - for data in view.additionalData { - switch data { - case let .cachedPeerData(peerIdValue, value): - if peerIdValue == peerId { - cachedData = value - } - case let .cachedPeerDataMessages(peerIdValue, value): - if peerIdValue == peerId { - cachedDataMessages = value - } - case let .totalUnreadCount(totalUnreadCount): - if let readState = view.combinedReadState { - readStateData = ChatHistoryCombinedInitialReadStateData(unreadCount: readState.count, totalUnreadCount: totalUnreadCount, notificationSettings: notificationSettings) - } - default: - break - } - } + return account.viewTracker.aroundMessageHistoryViewForLocation(chatLocation, index: index, anchorIndex: anchorIndex, count: 140, fixedCombinedReadStates: fixedCombinedReadStates, tagMask: tagMask, orderStatistics: orderStatistics, additionalData: additionalData) |> map { view, updateType, initialData -> ChatHistoryViewUpdate in + let (cachedData, cachedDataMessages, readStateData) = extractAdditionalData(view: view, chatLocation: chatLocation) let genericType: ViewUpdateType let scrollPosition: ChatHistoryViewScrollPosition? = first ? chatScrollPosition : nil @@ -276,3 +167,54 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun } } } + +private func extractAdditionalData(view: MessageHistoryView, chatLocation: ChatLocation) -> ( + cachedData: CachedPeerData?, + cachedDataMessages: [MessageId: Message]?, + readStateData: [PeerId: ChatHistoryCombinedInitialReadStateData]? + ) { + var cachedData: CachedPeerData? + var cachedDataMessages: [MessageId: Message]? + var readStateData: [PeerId: ChatHistoryCombinedInitialReadStateData] = [:] + var notificationSettings: PeerNotificationSettings? + + loop: for data in view.additionalData { + switch data { + case let .peerNotificationSettings(value): + notificationSettings = value + break loop + default: + break + } + } + + for data in view.additionalData { + switch data { + case let .peerNotificationSettings(value): + notificationSettings = value + case let .cachedPeerData(peerIdValue, value): + if case .peer(peerIdValue) = chatLocation { + cachedData = value + } + case let .cachedPeerDataMessages(peerIdValue, value): + if case .peer(peerIdValue) = chatLocation { + cachedDataMessages = value + } + case let .totalUnreadCount(totalUnreadCount): + switch chatLocation { + case let .peer(peerId): + if let combinedReadStates = view.combinedReadStates { + if case let .peer(readStates) = combinedReadStates, let readState = readStates[peerId] { + readStateData[peerId] = ChatHistoryCombinedInitialReadStateData(unreadCount: readState.count, totalUnreadCount: totalUnreadCount, notificationSettings: notificationSettings) + } + } + case .group: + break + } + default: + break + } + } + + return (cachedData, cachedDataMessages, readStateData) +} diff --git a/TelegramUI/ChatHoleItem.swift b/TelegramUI/ChatHoleItem.swift index 0b698419f5..c1bdd7eecf 100644 --- a/TelegramUI/ChatHoleItem.swift +++ b/TelegramUI/ChatHoleItem.swift @@ -11,26 +11,26 @@ class ChatHoleItem: ListViewItem { let index: MessageIndex let theme: PresentationTheme let strings: PresentationStrings - let header: ChatMessageDateHeader + //let header: ChatMessageDateHeader init(index: MessageIndex, theme: PresentationTheme, strings: PresentationStrings) { self.index = index self.theme = theme self.strings = strings - self.header = ChatMessageDateHeader(timestamp: index.timestamp, theme: theme, strings: strings) + //self.header = ChatMessageDateHeader(timestamp: index.timestamp, theme: theme, strings: strings) } - func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { async { let node = ChatHoleItemNode() - node.layoutForWidth(width, item: self, previousItem: previousItem, nextItem: nextItem) + node.layoutForParams(params, item: self, previousItem: previousItem, nextItem: nextItem) completion(node, { return (nil, {}) }) } } - func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { completion(ListViewItemNodeLayout(contentSize: node.contentSize, insets: node.insets), { }) } @@ -61,31 +61,31 @@ class ChatHoleItemNode: ListViewItemNode { self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) } - override func layoutForWidth(_ width: CGFloat, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { + override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { if let item = item as? ChatHoleItem { - let dateAtBottom = !chatItemsHaveCommonDateHeader(item, nextItem) - let (layout, apply) = self.asyncLayout()(item, width, dateAtBottom) + let dateAtBottom = false//!chatItemsHaveCommonDateHeader(item, nextItem) + let (layout, apply) = self.asyncLayout()(item, params, dateAtBottom) apply() self.contentSize = layout.contentSize self.insets = layout.insets } } - func asyncLayout() -> (_ item: ChatHoleItem, _ width: CGFloat, _ dateAtBottom: Bool) -> (ListViewItemNodeLayout, () -> Void) { + func asyncLayout() -> (_ item: ChatHoleItem, _ params: ListViewItemLayoutParams, _ dateAtBottom: Bool) -> (ListViewItemNodeLayout, () -> Void) { let labelLayout = TextNode.asyncLayout(self.labelNode) let layoutConstants = self.layoutConstants let currentItem = self.item - return { item, width, dateAtBottom in + return { item, params, dateAtBottom in var updatedBackground: UIImage? if item.theme !== currentItem?.theme { updatedBackground = PresentationResourcesChat.chatServiceBubbleFillImage(item.theme) } - let (size, apply) = labelLayout(NSAttributedString(string: item.strings.Channel_NotificationLoading, font: titleFont, textColor: item.theme.chat.serviceMessage.serviceMessagePrimaryTextColor), nil, 1, .end, CGSize(width: width, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let (size, apply) = labelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.strings.Channel_NotificationLoading, font: titleFont, textColor: item.theme.chat.serviceMessage.serviceMessagePrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let backgroundSize = CGSize(width: size.size.width + 8.0 + 8.0, height: 20.0) - return (ListViewItemNodeLayout(contentSize: CGSize(width: width, height: 20.0), insets: UIEdgeInsets(top: 4.0 + (dateAtBottom ? layoutConstants.timestampHeaderHeight : 0.0), left: 0.0, bottom: 4.0, right: 0.0)), { [weak self] in + return (ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 20.0), insets: UIEdgeInsets(top: 4.0 + (dateAtBottom ? layoutConstants.timestampHeaderHeight : 0.0), left: 0.0, bottom: 4.0, right: 0.0)), { [weak self] in if let strongSelf = self { strongSelf.item = item @@ -95,20 +95,20 @@ class ChatHoleItemNode: ListViewItemNode { let _ = apply() - strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((width - backgroundSize.width) / 2.0), y: 0.0), size: backgroundSize) - strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: strongSelf.backgroundNode.frame.origin.x + 8.0, y: floorToScreenPixels((backgroundSize.height - size.size.height) / 2.0) - 1.0), size: size.size) + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((params.width - backgroundSize.width) / 2.0), y: 0.0), size: backgroundSize) + strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: strongSelf.backgroundNode.frame.origin.x + 8.0, y: floorToScreenPixels((backgroundSize.height - size.size.height) / 2.0)), size: size.size) } }) } } - override public func header() -> ListViewItemHeader? { + /*override public func header() -> ListViewItemHeader? { if let item = self.item { return item.header } else { return nil } - } + }*/ override public func animateAdded(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) diff --git a/TelegramUI/ChatImageGalleryItem.swift b/TelegramUI/ChatImageGalleryItem.swift index 24c85b9890..dc4366d48b 100644 --- a/TelegramUI/ChatImageGalleryItem.swift +++ b/TelegramUI/ChatImageGalleryItem.swift @@ -66,11 +66,15 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { private let imageNode: TransformImageNode fileprivate let _ready = Promise() fileprivate let _title = Promise() + private let statusNodeContainer: HighlightableButtonNode + private let statusNode: RadialStatusNode private let footerContentNode: ChatItemGalleryFooterContentNode private var accountAndMedia: (Account, Media)? private var fetchDisposable = MetaDisposable() + private let statusDisposable = MetaDisposable() + private var status: MediaResourceStatus? init(account: Account, theme: PresentationTheme, strings: PresentationStrings) { self.account = account @@ -78,6 +82,10 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { self.imageNode = TransformImageNode() self.footerContentNode = ChatItemGalleryFooterContentNode(account: account, theme: theme, strings: strings) + self.statusNodeContainer = HighlightableButtonNode() + self.statusNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.5)) + self.statusNode.isHidden = true + super.init() self.imageNode.imageUpdated = { [weak self] in @@ -86,10 +94,18 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { self.imageNode.view.contentMode = .scaleAspectFill self.imageNode.clipsToBounds = true + + self.statusNodeContainer.addSubnode(self.statusNode) + self.addSubnode(self.statusNodeContainer) + + self.statusNodeContainer.addTarget(self, action: #selector(self.statusPressed), forControlEvents: .touchUpInside) + + self.statusNodeContainer.isUserInteractionEnabled = false } deinit { self.fetchDisposable.dispose() + self.statusDisposable.dispose() } override func ready() -> Signal { @@ -98,6 +114,10 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) + + let statusSize = CGSize(width: 50.0, height: 50.0) + transition.updateFrame(node: self.statusNodeContainer, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - statusSize.width) / 2.0), y: floor((layout.size.height - statusSize.height) / 2.0)), size: statusSize)) + transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(), size: statusSize)) } fileprivate func setMessage(_ message: Message) { @@ -108,11 +128,11 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { if self.accountAndMedia == nil || !self.accountAndMedia!.1.isEqual(image) { if let largestSize = largestRepresentationForPhoto(image) { let displaySize = largestSize.dimensions.fitted(CGSize(width: 1280.0, height: 1280.0)).dividedByScreenScale().integralFloor - self.imageNode.alphaTransitionOnFirstUpdate = false self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets()))() - self.imageNode.setSignal(account: account, signal: chatMessagePhoto(account: account, photo: image), dispatchOnDisplayLink: false) + self.imageNode.setSignal(chatMessagePhoto(postbox: account.postbox, photo: image), dispatchOnDisplayLink: false) self.zoomableContent = (largestSize.dimensions, self.imageNode) self.fetchDisposable.set(account.postbox.mediaBox.fetchedResource(largestSize.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .image)).start()) + self.setupStatus(resource: largestSize.resource) } else { self._ready.set(.single(Void())) } @@ -123,11 +143,11 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { func setFile(account: Account, file: TelegramMediaFile) { if self.accountAndMedia == nil || !self.accountAndMedia!.1.isEqual(file) { if let largestSize = file.dimensions { - self.imageNode.alphaTransitionOnFirstUpdate = false let displaySize = largestSize.dividedByScreenScale() self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets()))() - self.imageNode.setSignal(account: account, signal: chatMessageImageFile(account: account, file: file, progressive: false), dispatchOnDisplayLink: false) + self.imageNode.setSignal(chatMessageImageFile(account: account, file: file, thumbnail: false), dispatchOnDisplayLink: false) self.zoomableContent = (largestSize, self.imageNode) + self.setupStatus(resource: file.resource) } else { self._ready.set(.single(Void())) } @@ -135,6 +155,54 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { self.accountAndMedia = (account, file) } + private func setupStatus(resource: MediaResource) { + self.statusDisposable.set((account.postbox.mediaBox.resourceStatus(resource) + |> deliverOnMainQueue).start(next: { [weak self] status in + if let strongSelf = self { + let previousStatus = strongSelf.status + strongSelf.status = status + switch status { + case .Remote: + strongSelf.statusNode.isHidden = false + strongSelf.statusNode.alpha = 1.0 + strongSelf.statusNodeContainer.isUserInteractionEnabled = true + strongSelf.statusNode.transitionToState(.download(.white), completion: {}) + case let .Fetching(isActive, progress): + strongSelf.statusNode.isHidden = false + strongSelf.statusNode.alpha = 1.0 + strongSelf.statusNodeContainer.isUserInteractionEnabled = true + var actualProgress = progress + if isActive { + actualProgress = max(actualProgress, 0.027) + } + strongSelf.statusNode.transitionToState(.progress(color: .white, value: CGFloat(actualProgress), cancelEnabled: true), completion: {}) + case .Local: + if let previousStatus = previousStatus, case .Fetching = previousStatus { + strongSelf.statusNode.transitionToState(.progress(color: .white, value: 1.0, cancelEnabled: true), completion: { + if let strongSelf = self { + strongSelf.statusNode.alpha = 0.0 + strongSelf.statusNodeContainer.isUserInteractionEnabled = false + strongSelf.statusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { _ in + if let strongSelf = self { + strongSelf.statusNode.transitionToState(.none, animated: false, completion: {}) + } + }) + } + }) + } else if !strongSelf.statusNode.isHidden && !strongSelf.statusNode.alpha.isZero { + strongSelf.statusNode.alpha = 0.0 + strongSelf.statusNodeContainer.isUserInteractionEnabled = false + strongSelf.statusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { _ in + if let strongSelf = self { + strongSelf.statusNode.transitionToState(.none, animated: false, completion: {}) + } + }) + } + } + } + })) + } + override func animateIn(from node: ASDisplayNode, addToTransitionSurface: (UIView) -> Void) { var transformedFrame = node.view.convert(node.view.bounds, to: self.imageNode.view) let transformedSuperFrame = node.view.convert(node.view.bounds, to: self.imageNode.view.superview) @@ -183,9 +251,15 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { transformedFrame.origin = CGPoint() self.imageNode.layer.animateBounds(from: transformedFrame, to: self.imageNode.layer.bounds, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) + + self.statusNodeContainer.layer.animatePosition(from: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), to: self.statusNodeContainer.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) + self.statusNodeContainer.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) + self.statusNodeContainer.layer.animateScale(from: 0.5, to: 1.0, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) } override func animateOut(to node: ASDisplayNode, addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) { + self.fetchDisposable.set(nil) + var transformedFrame = node.view.convert(node.view.bounds, to: self.imageNode.view) let transformedSuperFrame = node.view.convert(node.view.bounds, to: self.imageNode.view.superview) let transformedSelfFrame = node.view.convert(node.view.bounds, to: self.view) @@ -218,8 +292,8 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { } } - copyView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.12, removeOnCompletion: false) - surfaceCopyView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, removeOnCompletion: false) + copyView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false) + surfaceCopyView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, removeOnCompletion: false) copyView.layer.animatePosition(from: CGPoint(x: transformedCopyViewInitialFrame.midX, y: transformedCopyViewInitialFrame.midY), to: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) let scale = CGSize(width: transformedCopyViewInitialFrame.size.width / transformedSelfFrame.size.width, height: transformedCopyViewInitialFrame.size.height / transformedSelfFrame.size.height) @@ -246,6 +320,9 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { boundsCompleted = true intermediateCompletion() }) + + self.statusNodeContainer.layer.animatePosition(from: self.statusNodeContainer.position, to: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + self.statusNodeContainer.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, timingFunction: kCAMediaTimingFunctionEaseIn, removeOnCompletion: false) } override func visibilityUpdated(isVisible: Bool) { @@ -253,7 +330,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { if let (account, media) = self.accountAndMedia, let file = media as? TelegramMediaFile { if isVisible { - self.fetchDisposable.set(account.postbox.mediaBox.fetchedResource(file.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .image)).start()) + //self.fetchDisposable.set(account.postbox.mediaBox.fetchedResource(file.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .image)).start()) } else { self.fetchDisposable.set(nil) } @@ -267,4 +344,25 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { override func footerContent() -> Signal { return .single(self.footerContentNode) } + + @objc func statusPressed() { + if let (_, media) = self.accountAndMedia, let status = self.status { + var resource: MediaResource? + if let file = media as? TelegramMediaFile { + resource = file.resource + } else if let image = media as? TelegramMediaImage { + resource = largestImageRepresentation(image.representations)?.resource + } + if let resource = resource { + switch status { + case .Fetching: + self.account.postbox.mediaBox.cancelInteractiveResourceFetch(resource) + case .Remote: + self.fetchDisposable.set(self.account.postbox.mediaBox.fetchedResource(resource, tag: TelegramMediaResourceFetchTag(statsCategory: .generic)).start()) + default: + break + } + } + } + } } diff --git a/TelegramUI/ChatInfoTitlePanelNode.swift b/TelegramUI/ChatInfoTitlePanelNode.swift index 3d5d54255b..fd3fc86352 100644 --- a/TelegramUI/ChatInfoTitlePanelNode.swift +++ b/TelegramUI/ChatInfoTitlePanelNode.swift @@ -118,7 +118,7 @@ final class ChatInfoTitlePanelNode: ChatTitleAccessoryPanelNode { self.addSubnode(self.separatorNode) } - override func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { let themeUpdated = self.theme !== interfaceState.theme self.theme = interfaceState.theme @@ -166,8 +166,8 @@ final class ChatInfoTitlePanelNode: ChatTitleAccessoryPanelNode { } if !self.buttons.isEmpty { - let buttonWidth = floor(width / CGFloat(self.buttons.count)) - var nextButtonOrigin: CGFloat = 0.0 + let buttonWidth = floor((width - leftInset - rightInset) / CGFloat(self.buttons.count)) + var nextButtonOrigin: CGFloat = leftInset for (_, buttonNode) in self.buttons { buttonNode.frame = CGRect(origin: CGPoint(x: nextButtonOrigin, y: 0.0), size: CGSize(width: buttonWidth, height: panelHeight)) nextButtonOrigin += buttonWidth @@ -190,7 +190,7 @@ final class ChatInfoTitlePanelNode: ChatTitleAccessoryPanelNode { case .unmute: self.interfaceInteraction?.togglePeerNotifications() case .search: - self.interfaceInteraction?.beginMessageSearch() + self.interfaceInteraction?.beginMessageSearch(.everything) case .call: self.interfaceInteraction?.beginCall() case .report: diff --git a/TelegramUI/ChatInputContextPanelNode.swift b/TelegramUI/ChatInputContextPanelNode.swift index a9e421ee82..1d678498c0 100644 --- a/TelegramUI/ChatInputContextPanelNode.swift +++ b/TelegramUI/ChatInputContextPanelNode.swift @@ -13,13 +13,13 @@ class ChatInputContextPanelNode: ASDisplayNode { var interfaceInteraction: ChatPanelInterfaceInteraction? var placement: ChatInputContextPanelPlacement = .overPanels - init(account: Account) { + init(account: Account, theme: PresentationTheme, strings: PresentationStrings) { self.account = account super.init() } - func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) { + func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) { } func animateOut(completion: @escaping () -> Void) { diff --git a/TelegramUI/ChatInputNode.swift b/TelegramUI/ChatInputNode.swift index 42b3a396f6..ccdd70af9d 100644 --- a/TelegramUI/ChatInputNode.swift +++ b/TelegramUI/ChatInputNode.swift @@ -5,7 +5,7 @@ import AsyncDisplayKit class ChatInputNode: ASDisplayNode { var interfaceInteraction: ChatPanelInterfaceInteraction? - func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { + func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { return 0.0 } } diff --git a/TelegramUI/ChatInputPanelNode.swift b/TelegramUI/ChatInputPanelNode.swift index 2ebd6c0361..23ffb8fb2a 100644 --- a/TelegramUI/ChatInputPanelNode.swift +++ b/TelegramUI/ChatInputPanelNode.swift @@ -8,7 +8,7 @@ class ChatInputPanelNode: ASDisplayNode { var account: Account? var interfaceInteraction: ChatPanelInterfaceInteraction? - func updateLayout(width: CGFloat, maxHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { + func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { return 0.0 } } diff --git a/TelegramUI/ChatInterfaceInputContextPanels.swift b/TelegramUI/ChatInterfaceInputContextPanels.swift index 4645f78e2e..b528d2938b 100644 --- a/TelegramUI/ChatInterfaceInputContextPanels.swift +++ b/TelegramUI/ChatInterfaceInputContextPanels.swift @@ -1,8 +1,29 @@ import Foundation import TelegramCore +private func inputQueryResultPriority(_ result: ChatPresentationInputQueryResult) -> Int { + switch result { + case .stickers: + return 0 + case .hashtags: + return 1 + case .mentions: + return 2 + case .commands: + return 3 + case .contextRequestResult: + return 4 + } +} + func inputContextPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, account: Account, currentPanel: ChatInputContextPanelNode?, interfaceInteraction: ChatPanelInterfaceInteraction?) -> ChatInputContextPanelNode? { - guard let inputQueryResult = chatPresentationInterfaceState.inputQueryResult, let _ = chatPresentationInterfaceState.peer else { + guard let _ = chatPresentationInterfaceState.peer else { + return nil + } + + guard let inputQueryResult = chatPresentationInterfaceState.inputQueryResults.values.sorted(by: { + inputQueryResultPriority($0) < inputQueryResultPriority($1) + }).first else { return nil } @@ -13,7 +34,7 @@ func inputContextPanelForChatPresentationIntefaceState(_ chatPresentationInterfa currentPanel.updateResults(results.map({ $0.file })) return currentPanel } else { - let panel = HorizontalStickersChatContextPanelNode(account: account) + let panel = HorizontalStickersChatContextPanelNode(account: account, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) panel.interfaceInteraction = interfaceInteraction panel.updateResults(results.map({ $0.file })) return panel @@ -24,18 +45,18 @@ func inputContextPanelForChatPresentationIntefaceState(_ chatPresentationInterfa currentPanel.updateResults(results) return currentPanel } else { - let panel = HashtagChatInputContextPanelNode(account: account) + let panel = HashtagChatInputContextPanelNode(account: account, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) panel.interfaceInteraction = interfaceInteraction panel.updateResults(results) return panel } case let .mentions(peers): if !peers.isEmpty { - if let currentPanel = currentPanel as? MentionChatInputContextPanelNode { + if let currentPanel = currentPanel as? MentionChatInputContextPanelNode, currentPanel.mode == .input { currentPanel.updateResults(peers) return currentPanel } else { - let panel = MentionChatInputContextPanelNode(account: account) + let panel = MentionChatInputContextPanelNode(account: account, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, mode: .input) panel.interfaceInteraction = interfaceInteraction panel.updateResults(peers) return panel @@ -49,7 +70,7 @@ func inputContextPanelForChatPresentationIntefaceState(_ chatPresentationInterfa currentPanel.updateResults(commands) return currentPanel } else { - let panel = CommandChatInputContextPanelNode(account: account) + let panel = CommandChatInputContextPanelNode(account: account, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) panel.interfaceInteraction = interfaceInteraction panel.updateResults(commands) return panel @@ -57,7 +78,7 @@ func inputContextPanelForChatPresentationIntefaceState(_ chatPresentationInterfa } else { return nil } - case let .contextRequestResult(peer, results): + case let .contextRequestResult(_, results): if let results = results, (!results.results.isEmpty || results.switchPeer != nil) { switch results.presentation { case .list: @@ -65,7 +86,7 @@ func inputContextPanelForChatPresentationIntefaceState(_ chatPresentationInterfa currentPanel.updateResults(results) return currentPanel } else { - let panel = VerticalListContextResultsChatInputContextPanelNode(account: account) + let panel = VerticalListContextResultsChatInputContextPanelNode(account: account, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) panel.interfaceInteraction = interfaceInteraction panel.updateResults(results) return panel @@ -75,7 +96,7 @@ func inputContextPanelForChatPresentationIntefaceState(_ chatPresentationInterfa currentPanel.updateResults(results) return currentPanel } else { - let panel = HorizontalListContextResultsChatInputContextPanelNode(account: account) + let panel = HorizontalListContextResultsChatInputContextPanelNode(account: account, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) panel.interfaceInteraction = interfaceInteraction panel.updateResults(results) return panel @@ -88,3 +109,31 @@ func inputContextPanelForChatPresentationIntefaceState(_ chatPresentationInterfa return nil } + +func chatOverlayContextPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, account: Account, currentPanel: ChatInputContextPanelNode?, interfaceInteraction: ChatPanelInterfaceInteraction?) -> ChatInputContextPanelNode? { + guard let searchQuerySuggestionResult = chatPresentationInterfaceState.searchQuerySuggestionResult, let _ = chatPresentationInterfaceState.peer else { + return nil + } + + switch searchQuerySuggestionResult { + case let .mentions(peers): + if !peers.isEmpty { + if let currentPanel = currentPanel as? MentionChatInputContextPanelNode, currentPanel.mode == .search { + currentPanel.updateResults(peers) + return currentPanel + } else { + let panel = MentionChatInputContextPanelNode(account: account, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, mode: .search) + panel.interfaceInteraction = interfaceInteraction + panel.updateResults(peers) + return panel + } + } else { + return nil + } + default: + break + } + + return nil +} + diff --git a/TelegramUI/ChatInterfaceInputContexts.swift b/TelegramUI/ChatInterfaceInputContexts.swift index 93806e5e46..57c767a3b3 100644 --- a/TelegramUI/ChatInterfaceInputContexts.swift +++ b/TelegramUI/ChatInterfaceInputContexts.swift @@ -33,8 +33,9 @@ private let atScalar = makeScalar("@") private let slashScalar = makeScalar("/") private let alphanumerics = CharacterSet.alphanumerics -func textInputStateContextQueryRangeAndType(_ inputState: ChatTextInputState) -> (Range, PossibleContextQueryTypes, Range?)? { +func textInputStateContextQueryRangeAndType(_ inputState: ChatTextInputState) -> [(Range, PossibleContextQueryTypes, Range?)] { let inputText = inputState.inputText + var results: [(Range, PossibleContextQueryTypes, Range?)] = [] if !inputText.isEmpty { if inputText.hasPrefix("@") && inputText != "@" { let startIndex = inputText.index(after: inputText.startIndex) @@ -67,23 +68,23 @@ func textInputStateContextQueryRangeAndType(_ inputState: ChatTextInputState) -> } if let contextAddressRange = contextAddressRange { - return (contextAddressRange, [.contextRequest], index ..< inputText.endIndex) + results.append((contextAddressRange, [.contextRequest], index ..< inputText.endIndex)) } } let maxUtfIndex = inputText.utf16.index(inputText.utf16.startIndex, offsetBy: inputState.selectionRange.lowerBound) guard let maxIndex = maxUtfIndex.samePosition(in: inputText) else { - return nil + return results } if maxIndex == inputText.startIndex { - return nil + return results } var index = inputText.index(before: maxIndex) var possibleQueryRange: Range? if inputText.isSingleEmoji { - return (inputText.startIndex ..< inputText.endIndex, [.emoji], nil) + return [(inputText.startIndex ..< inputText.endIndex, [.emoji], nil)] } var possibleTypes = PossibleContextQueryTypes([.command, .mention, .hashtag]) @@ -123,50 +124,61 @@ func textInputStateContextQueryRangeAndType(_ inputState: ChatTextInputState) -> } if let possibleQueryRange = possibleQueryRange, definedType && !possibleTypes.isEmpty { - return (possibleQueryRange, possibleTypes, nil) + results.append((possibleQueryRange, possibleTypes, nil)) } } - return nil + return results } -func inputContextQueryForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState) -> ChatPresentationInputQuery? { +func inputContextQueriesForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState) -> [ChatPresentationInputQuery] { let inputState = chatPresentationInterfaceState.interfaceState.effectiveInputState - if let (possibleQueryRange, possibleTypes, additionalStringRange) = textInputStateContextQueryRangeAndType(inputState) { + var result: [ChatPresentationInputQuery] = [] + for (possibleQueryRange, possibleTypes, additionalStringRange) in textInputStateContextQueryRangeAndType(inputState) { let query = String(inputState.inputText[possibleQueryRange]) if possibleTypes == [.emoji] { - return .emoji(query) + result.append(.emoji(query)) } else if possibleTypes == [.hashtag] { - return .hashtag(query) + result.append(.hashtag(query)) } else if possibleTypes == [.mention] { - return .mention(query) + var types: ChatInputQueryMentionTypes = [.members] + if possibleQueryRange.lowerBound == inputState.inputText.index(after: inputState.inputText.startIndex) { + types.insert(.contextBots) + } + result.append(.mention(query: query, types: types)) } else if possibleTypes == [.command] { - return .command(query) + result.append(.command(query)) } else if possibleTypes == [.contextRequest], let additionalStringRange = additionalStringRange { let additionalString = String(inputState.inputText[additionalStringRange]) - return .contextRequest(addressName: query, query: additionalString) + result.append(.contextRequest(addressName: query, query: additionalString)) } - return nil - } else { - return nil } + return result } func inputTextPanelStateForChatPresentationInterfaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, account: Account) -> ChatTextInputPanelState { var contextPlaceholder: NSAttributedString? - if let inputQueryResult = chatPresentationInterfaceState.inputQueryResult { - if case let .contextRequestResult(peer, _) = inputQueryResult, let botUser = peer as? TelegramUser, let botInfo = botUser.botInfo, let inlinePlaceholder = botInfo.inlinePlaceholder { - if let inputQuery = inputContextQueryForChatPresentationIntefaceState(chatPresentationInterfaceState) { + loop: for (_, result) in chatPresentationInterfaceState.inputQueryResults { + if case let .contextRequestResult(peer, _) = result, let botUser = peer as? TelegramUser, let botInfo = botUser.botInfo, let inlinePlaceholder = botInfo.inlinePlaceholder { + let inputQueries = inputContextQueriesForChatPresentationIntefaceState(chatPresentationInterfaceState) + for inputQuery in inputQueries { if case let .contextRequest(addressName, query) = inputQuery, query.isEmpty { let string = NSMutableAttributedString() string.append(NSAttributedString(string: "@" + addressName, font: Font.regular(17.0), textColor: UIColor.clear)) - string.append(NSAttributedString(string: " " + inlinePlaceholder, font: Font.regular(17.0), textColor: UIColor(rgb: 0xC8C8CE))) + string.append(NSAttributedString(string: " " + inlinePlaceholder, font: Font.regular(17.0), textColor: chatPresentationInterfaceState.theme.chat.inputPanel.inputPlaceholderColor)) contextPlaceholder = string } } + + break loop } } switch chatPresentationInterfaceState.inputMode { - case .media, .inputButtons: + case .media: + if contextPlaceholder == nil && chatPresentationInterfaceState.interfaceState.editMessage == nil && chatPresentationInterfaceState.interfaceState.composeInputState.inputText.isEmpty && chatPresentationInterfaceState.inputMode == .media(.gif) { + contextPlaceholder = NSAttributedString(string: "@gif", font: Font.regular(17.0), textColor: chatPresentationInterfaceState.theme.chat.inputPanel.inputPlaceholderColor) + } + return ChatTextInputPanelState(accessoryItems: [.keyboard], contextPlaceholder: contextPlaceholder, mediaRecordingState: chatPresentationInterfaceState.inputTextPanelState.mediaRecordingState) + case .inputButtons: return ChatTextInputPanelState(accessoryItems: [.keyboard], contextPlaceholder: contextPlaceholder, mediaRecordingState: chatPresentationInterfaceState.inputTextPanelState.mediaRecordingState) case .none, .text: if let _ = chatPresentationInterfaceState.interfaceState.editMessage { diff --git a/TelegramUI/ChatInterfaceInputNodes.swift b/TelegramUI/ChatInterfaceInputNodes.swift index 394fae58a4..3f7f721992 100644 --- a/TelegramUI/ChatInterfaceInputNodes.swift +++ b/TelegramUI/ChatInterfaceInputNodes.swift @@ -13,7 +13,21 @@ func inputNodeForChatPresentationIntefaceState(_ chatPresentationInterfaceState: } else if let inputMediaNode = inputMediaNode { return inputMediaNode } else { - let inputNode = ChatMediaInputNode(account: account, controllerInteraction: controllerInteraction, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) + let inputNode = ChatMediaInputNode(account: account, controllerInteraction: controllerInteraction, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, gifPaneIsActiveUpdated: { [weak interfaceInteraction] value in + if let interfaceInteraction = interfaceInteraction { + interfaceInteraction.updateInputModeAndDismissedButtonKeyboardMessageId { state in + if case .media = state.inputMode { + if value { + return (.media(.gif), nil) + } else { + return (.media(.other), nil) + } + } else { + return (state.inputMode, nil) + } + } + } + }) inputNode.interfaceInteraction = interfaceInteraction return inputNode } diff --git a/TelegramUI/ChatInterfaceState.swift b/TelegramUI/ChatInterfaceState.swift index ea2d42203b..d44e6da127 100644 --- a/TelegramUI/ChatInterfaceState.swift +++ b/TelegramUI/ChatInterfaceState.swift @@ -67,10 +67,12 @@ public struct ChatTextInputState: PostboxCoding, Equatable { struct ChatEditMessageState: PostboxCoding, Equatable { let messageId: MessageId let inputState: ChatTextInputState + let disableUrlPreview: String? - init(messageId: MessageId, inputState: ChatTextInputState) { + init(messageId: MessageId, inputState: ChatTextInputState, disableUrlPreview: String?) { self.messageId = messageId self.inputState = inputState + self.disableUrlPreview = disableUrlPreview } init(decoder: PostboxDecoder) { @@ -80,6 +82,7 @@ struct ChatEditMessageState: PostboxCoding, Equatable { } else { self.inputState = ChatTextInputState() } + self.disableUrlPreview = decoder.decodeOptionalStringForKey("dup") } func encode(_ encoder: PostboxEncoder) { @@ -87,14 +90,23 @@ struct ChatEditMessageState: PostboxCoding, Equatable { encoder.encodeInt32(self.messageId.namespace, forKey: "mn") encoder.encodeInt32(self.messageId.id, forKey: "mi") encoder.encodeObject(self.inputState, forKey: "is") + if let disableUrlPreview = self.disableUrlPreview { + encoder.encodeString(disableUrlPreview, forKey: "dup") + } else { + encoder.encodeNil(forKey: "dup") + } } static func ==(lhs: ChatEditMessageState, rhs: ChatEditMessageState) -> Bool { - return lhs.messageId == rhs.messageId && lhs.inputState == rhs.inputState + return lhs.messageId == rhs.messageId && lhs.inputState == rhs.inputState && lhs.disableUrlPreview == rhs.disableUrlPreview } func withUpdatedInputState(_ inputState: ChatTextInputState) -> ChatEditMessageState { - return ChatEditMessageState(messageId: self.messageId, inputState: inputState) + return ChatEditMessageState(messageId: self.messageId, inputState: inputState, disableUrlPreview: self.disableUrlPreview) + } + + func withUpdatedDisableUrlPreview(_ disableUrlPreview: String?) -> ChatEditMessageState { + return ChatEditMessageState(messageId: self.messageId, inputState: self.inputState, disableUrlPreview: disableUrlPreview) } } @@ -478,24 +490,28 @@ final class ChatInterfaceState: SynchronizeableChatInterfaceState, Equatable { return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode) } - func withUpdatedSelectedMessage(_ messageId: MessageId) -> ChatInterfaceState { + func withUpdatedSelectedMessages(_ messageIds: [MessageId]) -> ChatInterfaceState { var selectedIds = Set() if let selectionState = self.selectionState { selectedIds.formUnion(selectionState.selectedIds) } - selectedIds.insert(messageId) + for messageId in messageIds { + selectedIds.insert(messageId) + } return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds), messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode) } - func withToggledSelectedMessage(_ messageId: MessageId) -> ChatInterfaceState { + func withToggledSelectedMessages(_ messageIds: [MessageId], value: Bool) -> ChatInterfaceState { var selectedIds = Set() if let selectionState = self.selectionState { selectedIds.formUnion(selectionState.selectedIds) } - if selectedIds.contains(messageId) { - let _ = selectedIds.remove(messageId) - } else { - selectedIds.insert(messageId) + for messageId in messageIds { + if value { + selectedIds.insert(messageId) + } else { + selectedIds.remove(messageId) + } } return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds), messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode) } diff --git a/TelegramUI/ChatInterfaceStateAccessoryPanels.swift b/TelegramUI/ChatInterfaceStateAccessoryPanels.swift index 6edd7fced1..c139ed83b9 100644 --- a/TelegramUI/ChatInterfaceStateAccessoryPanels.swift +++ b/TelegramUI/ChatInterfaceStateAccessoryPanels.swift @@ -6,8 +6,24 @@ func accessoryPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceS if let _ = chatPresentationInterfaceState.interfaceState.selectionState { return nil } + if chatPresentationInterfaceState.search != nil { + return nil + } if let editMessage = chatPresentationInterfaceState.interfaceState.editMessage { + if let editingUrlPreview = chatPresentationInterfaceState.editingUrlPreview, editMessage.disableUrlPreview != editingUrlPreview.0 { + if let previewPanelNode = currentPanel as? WebpagePreviewAccessoryPanelNode { + previewPanelNode.interfaceInteraction = interfaceInteraction + previewPanelNode.replaceWebpage(url: editingUrlPreview.0, webpage: editingUrlPreview.1) + previewPanelNode.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) + return previewPanelNode + } else { + let panelNode = WebpagePreviewAccessoryPanelNode(account: account, url: editingUrlPreview.0, webpage: editingUrlPreview.1, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) + panelNode.interfaceInteraction = interfaceInteraction + return panelNode + } + } + if let editPanelNode = currentPanel as? EditAccessoryPanelNode, editPanelNode.messageId == editMessage.messageId { editPanelNode.interfaceInteraction = interfaceInteraction editPanelNode.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) @@ -38,13 +54,13 @@ func accessoryPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceS return panelNode } } else if let urlPreview = chatPresentationInterfaceState.urlPreview, chatPresentationInterfaceState.interfaceState.composeDisableUrlPreview != urlPreview.0 { - if let previewPanelNode = currentPanel as? WebpagePreviewAccessoryPanelNode, previewPanelNode.webpage.id == urlPreview.1.id { + if let previewPanelNode = currentPanel as? WebpagePreviewAccessoryPanelNode { previewPanelNode.interfaceInteraction = interfaceInteraction - previewPanelNode.replaceWebpage(urlPreview.1) + previewPanelNode.replaceWebpage(url: urlPreview.0, webpage: urlPreview.1) previewPanelNode.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) return previewPanelNode } else { - let panelNode = WebpagePreviewAccessoryPanelNode(account: account, webpage: urlPreview.1, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) + let panelNode = WebpagePreviewAccessoryPanelNode(account: account, url: urlPreview.0, webpage: urlPreview.1, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) panelNode.interfaceInteraction = interfaceInteraction return panelNode } diff --git a/TelegramUI/ChatInterfaceStateContextMenus.swift b/TelegramUI/ChatInterfaceStateContextMenus.swift index ee5fbfacdc..ec617e6f32 100644 --- a/TelegramUI/ChatInterfaceStateContextMenus.swift +++ b/TelegramUI/ChatInterfaceStateContextMenus.swift @@ -4,69 +4,152 @@ import TelegramCore import Display import UIKit import SwiftSignalKit +import MobileCoreServices private struct MessageContextMenuData { let starStatus: Bool? let canReply: Bool let canPin: Bool let canEdit: Bool + let resourceStatus: MediaResourceStatus? } private let starIconEmpty = UIImage(bundleImageName: "Chat/Context Menu/StarIconEmpty")?.precomposed() private let starIconFilled = UIImage(bundleImageName: "Chat/Context Menu/StarIconFilled")?.precomposed() -func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: ChatPresentationInterfaceState, account: Account, message: Message, interfaceInteraction: ChatPanelInterfaceInteraction?) -> Signal { - guard let peer = chatPresentationInterfaceState.peer, let interfaceInteraction = interfaceInteraction else { +func canReplyInChat(_ chatPresentationInterfaceState: ChatPresentationInterfaceState) -> Bool { + guard let peer = chatPresentationInterfaceState.peer else { + return false + } + + var canReply = false + switch chatPresentationInterfaceState.chatLocation { + case .peer: + if let channel = peer as? TelegramChannel { + if case .member = channel.participationStatus { + switch channel.info { + case .broadcast: + canReply = channel.hasAdminRights([.canPostMessages]) + case .group: + canReply = true + } + } + } else if let group = peer as? TelegramGroup { + if case .Member = group.membership { + canReply = true + } + } else { + canReply = true + } + case .group: + break + } + return canReply +} + +func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: ChatPresentationInterfaceState, account: Account, messages: [Message], interfaceInteraction: ChatPanelInterfaceInteraction?, debugStreamSingleVideo: @escaping (MessageId) -> Void) -> Signal { + guard let interfaceInteraction = interfaceInteraction else { return .single(nil) } let dataSignal: Signal var loadStickerSaveStatus: MediaId? - for media in message.media { - if let file = media as? TelegramMediaFile { - for attribute in file.attributes { - if case let .Sticker(_, packInfo, _) = attribute, packInfo != nil { - loadStickerSaveStatus = file.fileId + var loadCopyMediaResource: MediaResource? + var isAction = false + if messages.count == 1 { + for media in messages[0].media { + if let file = media as? TelegramMediaFile { + for attribute in file.attributes { + if case let .Sticker(_, packInfo, _) = attribute, packInfo != nil { + loadStickerSaveStatus = file.fileId + } } + } else if let _ = media as? TelegramMediaAction { + isAction = true + } else if let image = media as? TelegramMediaImage { + loadCopyMediaResource = largestImageRepresentation(image.representations)?.resource } } } var canReply = false var canPin = false - if let channel = peer as? TelegramChannel { - switch channel.info { - case .broadcast: - canReply = channel.hasAdminRights([.canPostMessages]) - case .group: + switch chatPresentationInterfaceState.chatLocation { + case .peer: + if let channel = messages[0].peers[messages[0].id.peerId] as? TelegramChannel { + switch channel.info { + case .broadcast: + canReply = channel.hasAdminRights([.canPostMessages]) + if !isAction { + canPin = channel.hasAdminRights([.canEditMessages]) + } + case .group: + canReply = true + if !isAction { + canPin = channel.hasAdminRights([.canPinMessages]) + } + } + } else { canReply = true - canPin = channel.hasAdminRights([.canPinMessages]) - } - } else { - canReply = true + } + case .group: + break } var canEdit = false - if let author = message.author, author.id == account.peerId { - var hasUneditableAttributes = false - for attribute in message.attributes { - if let _ = attribute as? InlineBotMessageAttribute { - hasUneditableAttributes = true - break + if !isAction { + let message = messages[0] + + var hasEditRights = false + if message.id.peerId.namespace == Namespaces.Peer.SecretChat { + hasEditRights = false + } else if let author = message.author, author.id == account.peerId { + hasEditRights = true + } else if message.author?.id == message.id.peerId, let peer = message.peers[message.id.peerId] { + if let peer = peer as? TelegramChannel, case .broadcast = peer.info { + if peer.hasAdminRights(.canEditMessages) { + hasEditRights = true + } } } - if !hasUneditableAttributes { - let timestamp = Int32(CFAbsoluteTimeGetCurrent()) - if message.timestamp >= timestamp - 60 * 60 * 24 * 2 { - canEdit = true + if hasEditRights { + var hasUneditableAttributes = false + for attribute in message.attributes { + if let _ = attribute as? InlineBotMessageAttribute { + hasUneditableAttributes = true + break + } + } + if message.forwardInfo != nil { + hasUneditableAttributes = true + } + + for media in message.media { + if let file = media as? TelegramMediaFile { + if file.isSticker || file.isInstantVideo { + hasUneditableAttributes = true + break + } + } else if let _ = media as? TelegramMediaContact { + hasUneditableAttributes = true + break + } + } + + if !hasUneditableAttributes { + let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) + if message.timestamp >= timestamp - 60 * 60 * 24 * 2 { + canEdit = true + } } } } + var loadStickerSaveStatusSignal: Signal = .single(nil) if loadStickerSaveStatus != nil { - dataSignal = account.postbox.modify { modifier -> MessageContextMenuData in + loadStickerSaveStatusSignal = account.postbox.modify { modifier -> Bool? in var starStatus: Bool? if let loadStickerSaveStatus = loadStickerSaveStatus { if getIsStickerSaved(modifier: modifier, fileId: loadStickerSaveStatus) { @@ -76,10 +159,20 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: } } - return MessageContextMenuData(starStatus: starStatus, canReply: canReply, canPin: canPin, canEdit: canEdit) + return starStatus } - } else { - dataSignal = .single(MessageContextMenuData(starStatus: nil, canReply: canReply, canPin: canPin, canEdit: canEdit)) + } + + var loadResourceStatusSignal: Signal = .single(nil) + if let loadCopyMediaResource = loadCopyMediaResource { + loadResourceStatusSignal = account.postbox.mediaBox.resourceStatus(loadCopyMediaResource) + |> take(1) + |> map(Optional.init) + } + + dataSignal = combineLatest(loadStickerSaveStatusSignal, loadResourceStatusSignal) + |> map { stickerSaveStatus, resourceStatus -> MessageContextMenuData in + return MessageContextMenuData(starStatus: stickerSaveStatus, canReply: canReply, canPin: canPin, canEdit: canEdit, resourceStatus: resourceStatus) } return dataSignal |> deliverOnMainQueue |> map { data -> ContextMenuController? in @@ -87,32 +180,67 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: if let starStatus = data.starStatus, let image = starStatus ? starIconFilled : starIconEmpty { actions.append(ContextMenuAction(content: .icon(image), action: { - interfaceInteraction.toggleMessageStickerStarred(message.id) + interfaceInteraction.toggleMessageStickerStarred(messages[0].id) })) } if data.canReply { actions.append(ContextMenuAction(content: .text(chatPresentationInterfaceState.strings.Conversation_ContextMenuReply), action: { - interfaceInteraction.setupReplyMessage(message.id) + interfaceInteraction.setupReplyMessage(messages[0].id) })) } if data.canEdit { actions.append(ContextMenuAction(content: .text(chatPresentationInterfaceState.strings.Conversation_Edit), action: { - interfaceInteraction.setupEditMessage(message.id) + interfaceInteraction.setupEditMessage(messages[0].id) })) } - actions.append(ContextMenuAction(content: .text(chatPresentationInterfaceState.strings.Conversation_ContextMenuCopy), action: { - if !message.text.isEmpty { - UIPasteboard.general.string = message.text - } - })) + let resourceAvailable: Bool + if let resourceStatus = data.resourceStatus, case .Local = resourceStatus { + resourceAvailable = true + } else { + resourceAvailable = false + } + + if !messages[0].text.isEmpty || resourceAvailable { + let message = messages[0] + actions.append(ContextMenuAction(content: .text(chatPresentationInterfaceState.strings.Conversation_ContextMenuCopy), action: { + if resourceAvailable { + for media in message.media { + if let image = media as? TelegramMediaImage, let largest = largestImageRepresentation(image.representations) { + let _ = (account.postbox.mediaBox.resourceData(largest.resource, option: .incremental(waitUntilFetchStatus: false)) + |> take(1) + |> deliverOnMainQueue).start(next: { data in + if data.complete, let imageData = try? Data(contentsOf: URL(fileURLWithPath: data.path)) { + if let image = UIImage(data: imageData) { + if !message.text.isEmpty { + UIPasteboard.general.items = [ + [kUTTypeUTF8PlainText as String: message.text], + [kUTTypePNG as String: image] + ] + } else { + UIPasteboard.general.image = image + } + } else { + UIPasteboard.general.string = message.text + } + } else { + UIPasteboard.general.string = message.text + } + }) + } + } + } else { + UIPasteboard.general.string = message.text + } + })) + } if data.canPin { - if chatPresentationInterfaceState.pinnedMessage?.id != message.id { + if chatPresentationInterfaceState.pinnedMessage?.id != messages[0].id { actions.append(ContextMenuAction(content: .text(chatPresentationInterfaceState.strings.Conversation_Pin), action: { - interfaceInteraction.pinMessage(message.id) + interfaceInteraction.pinMessage(messages[0].id) })) } else { actions.append(ContextMenuAction(content: .text(chatPresentationInterfaceState.strings.Conversation_Unpin), action: { @@ -121,19 +249,28 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: } } - for media in message.media { - if let file = media as? TelegramMediaFile { - if file.isVideo && file.isAnimated { - actions.append(ContextMenuAction(content: .text(chatPresentationInterfaceState.strings.Conversation_LinkDialogSave), action: { - let _ = addSavedGif(postbox: account.postbox, file: file).start() - })) - break + if messages.count == 1 { + let message = messages[0] + + for media in message.media { + if let file = media as? TelegramMediaFile { + if file.isVideo { + if file.isAnimated { + actions.append(ContextMenuAction(content: .text(chatPresentationInterfaceState.strings.Conversation_LinkDialogSave), action: { + let _ = addSavedGif(postbox: account.postbox, file: file).start() + })) + } else if !GlobalExperimentalSettings.isAppStoreBuild { + actions.append(ContextMenuAction(content: .text("Stream"), action: { + debugStreamSingleVideo(message.id) + })) + } + break + } } } } - actions.append(ContextMenuAction(content: .text(chatPresentationInterfaceState.strings.Conversation_ContextMenuMore), action: { - interfaceInteraction.beginMessageSelection(message.id) + interfaceInteraction.beginMessageSelection(messages.map { $0.id }) })) if !actions.isEmpty { @@ -160,11 +297,13 @@ struct ChatDeleteMessagesOptions: OptionSet { static let globally = ChatDeleteMessagesOptions(rawValue: 1 << 1) } -func chatDeleteMessagesOptions(account: Account, messageIds: Set) -> Signal { - return account.postbox.modify { modifier -> ChatDeleteMessagesOptions in +func chatDeleteMessagesOptions(postbox: Postbox, accountPeerId: PeerId, messageIds: Set) -> Signal { + return postbox.modify { modifier -> ChatDeleteMessagesOptions in var optionsMap: [MessageId: ChatDeleteMessagesOptions] = [:] for id in messageIds { - if let peer = modifier.getPeer(id.peerId), let message = modifier.getMessage(id) { + if id.peerId == accountPeerId { + optionsMap[id] = .locally + } else if let peer = modifier.getPeer(id.peerId), let message = modifier.getMessage(id) { if let channel = peer as? TelegramChannel { var options: ChatDeleteMessagesOptions = [] if !message.flags.contains(.Incoming) { diff --git a/TelegramUI/ChatInterfaceStateContextQueries.swift b/TelegramUI/ChatInterfaceStateContextQueries.swift index 79ace2ffd8..5810fb54be 100644 --- a/TelegramUI/ChatInterfaceStateContextQueries.swift +++ b/TelegramUI/ChatInterfaceStateContextQueries.swift @@ -3,184 +3,281 @@ import SwiftSignalKit import TelegramCore import Postbox -func contextQueryResultStateForChatInterfacePresentationState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, account: Account, currentQuery: ChatPresentationInputQuery?) -> (ChatPresentationInputQuery?, Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError>)? { - if let inputQuery = inputContextQueryForChatPresentationIntefaceState(chatPresentationInterfaceState) { +enum ChatContextQueryUpdate { + case remove + case update(ChatPresentationInputQuery, Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError>) +} + +func contextQueryResultStateForChatInterfacePresentationState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, account: Account, currentQueryStates: inout [ChatPresentationInputQueryKind: (ChatPresentationInputQuery, Disposable)]) -> [ChatPresentationInputQueryKind: ChatContextQueryUpdate] { + guard let peer = chatPresentationInterfaceState.peer else { + return [:] + } + let inputQueries = inputContextQueriesForChatPresentationIntefaceState(chatPresentationInterfaceState) + + var updates: [ChatPresentationInputQueryKind: ChatContextQueryUpdate] = [:] + + for query in inputQueries { + let previousQuery = currentQueryStates[query.kind]?.0 + if previousQuery != query { + let signal = updatedContextQueryResultStateForQuery(account: account, peer: peer, inputQuery: query, previousQuery: previousQuery) + updates[query.kind] = .update(query, signal) + } + } + + for currentQueryKind in currentQueryStates.keys { + var found = false + inner: for query in inputQueries { + if query.kind == currentQueryKind { + found = true + break inner + } + } + if !found { + updates[currentQueryKind] = .remove + } + } + + return updates +} + +private func updatedContextQueryResultStateForQuery(account: Account, peer: Peer, inputQuery: ChatPresentationInputQuery, previousQuery: ChatPresentationInputQuery?) -> Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> { + switch inputQuery { + case let .emoji(query): + var signal: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> = .complete() + if let previousQuery = previousQuery { + switch previousQuery { + case .emoji: + break + default: + signal = .single({ _ in return .stickers([]) }) + } + } else { + signal = .single({ _ in return .stickers([]) }) + } + let stickers: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> = searchStickers(postbox: account.postbox, query: query.firstEmoji) + |> map { stickers -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in + return { _ in + return .stickers(stickers) + } + } + return signal |> then(stickers) + case let .hashtag(query): + var signal: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> = .complete() + if let previousQuery = previousQuery { + switch previousQuery { + case .hashtag: + break + default: + signal = .single({ _ in return .hashtags([]) }) + } + } else { + signal = .single({ _ in return .hashtags([]) }) + } + + let hashtags: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> = recentlyUsedHashtags(postbox: account.postbox) |> map { hashtags -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in + let normalizedQuery = query.lowercased() + var result: [String] = [] + for hashtag in hashtags { + if hashtag.lowercased().hasPrefix(normalizedQuery) { + result.append(hashtag) + } + } + return { _ in return .hashtags(result) } + } + + return signal |> then(hashtags) + case let .mention(query, types): + let normalizedQuery = query.lowercased() + + var signal: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> = .complete() + if let previousQuery = previousQuery { + switch previousQuery { + case .mention: + break + default: + signal = .single({ _ in return .mentions([]) }) + } + } else { + signal = .single({ _ in return .mentions([]) }) + } + + let inlineBots: Signal<[(Peer, Double)], NoError> = types.contains(.contextBots) ? recentlyUsedInlineBots(postbox: account.postbox) : .single([]) + let participants = combineLatest(inlineBots, searchGroupMembers(postbox: account.postbox, network: account.network, peerId: peer.id, query: query)) + |> map { inlineBots, peers -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in + let filteredInlineBots = inlineBots.sorted(by: { $0.1 > $1.1 }).filter { peer, rating in + if rating < 0.14 { + return false + } + if peer.indexName.matchesByTokens(normalizedQuery) { + return true + } + if let addressName = peer.addressName, addressName.lowercased().hasPrefix(normalizedQuery) { + return true + } + return false + }.map { $0.0 } + + let inlineBotPeerIds = Set(filteredInlineBots.map { $0.id }) + + let filteredPeers = peers.filter { peer in + if inlineBotPeerIds.contains(peer.id) { + return false + } + if !types.contains(.accountPeer) && peer.id == account.peerId { + return false + } + return true + } + var sortedPeers = filteredInlineBots + sortedPeers.append(contentsOf: filteredPeers.sorted(by: { lhs, rhs in + let result = lhs.indexName.indexName(.lastNameFirst).compare(rhs.indexName.indexName(.lastNameFirst)) + return result == .orderedAscending + })) + return { _ in return .mentions(sortedPeers) } + } + + return signal |> then(participants) + case let .command(query): + let normalizedQuery = query.lowercased() + + var signal: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> = .complete() + if let previousQuery = previousQuery { + switch previousQuery { + case .command: + break + default: + signal = .single({ _ in return .commands([]) }) + } + } else { + signal = .single({ _ in return .commands([]) }) + } + + let participants = peerCommands(account: account, id: peer.id) + |> map { commands -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in + let filteredCommands = commands.commands.filter { command in + if command.command.text.hasPrefix(normalizedQuery) { + return true + } + return false + } + let sortedCommands = filteredCommands + return { _ in return .commands(sortedCommands) } + } + + return signal |> then(participants) + case let .contextRequest(addressName, query): + var delayRequest = true + var signal: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> = .complete() + if let previousQuery = previousQuery { + switch previousQuery { + case let .contextRequest(currentAddressName, currentContextQuery) where currentAddressName == addressName: + if query.isEmpty && !currentContextQuery.isEmpty { + delayRequest = false + } + default: + delayRequest = false + signal = .single({ _ in return .contextRequestResult(nil, nil) }) + } + } else { + signal = .single({ _ in return .contextRequestResult(nil, nil) }) + } + + let chatPeer = peer + let contextBot = resolvePeerByName(account: account, name: addressName) + |> mapToSignal { peerId -> Signal in + if let peerId = peerId { + return account.postbox.loadedPeerWithId(peerId) + |> map { peer -> Peer? in + return peer + } + |> take(1) + } else { + return .single(nil) + } + } + |> mapToSignal { peer -> Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> in + if let user = peer as? TelegramUser, let botInfo = user.botInfo, let _ = botInfo.inlinePlaceholder { + let contextResults = requestChatContextResults(account: account, botId: user.id, peerId: chatPeer.id, query: query, offset: "") + |> map { results -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in + return { _ in + return .contextRequestResult(user, results) + } + } + + let botResult: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> = .single({ previousResult in + var passthroughPreviousResult: ChatContextResultCollection? + if let previousResult = previousResult { + if case let .contextRequestResult(previousUser, previousResults) = previousResult { + if previousUser?.id == user.id { + passthroughPreviousResult = previousResults + } + } + } + return .contextRequestResult(user, passthroughPreviousResult) + }) + + let maybeDelayedContextResults: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> + if delayRequest { + maybeDelayedContextResults = contextResults |> delay(0.4, queue: Queue.concurrentDefaultQueue()) + } else { + maybeDelayedContextResults = contextResults + } + + return botResult |> then(maybeDelayedContextResults) + } else { + return .single({ _ in return nil }) + } + } + + return signal |> then(contextBot) + } +} + +func searchQuerySuggestionResultStateForChatInterfacePresentationState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, account: Account, currentQuery: ChatPresentationInputQuery?) -> (ChatPresentationInputQuery?, Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError>)? { + var inputQuery: ChatPresentationInputQuery? + if let search = chatPresentationInterfaceState.search { + switch search.domain { + case .members: + inputQuery = .mention(query: search.query, types: [.members, .accountPeer]) + default: + break + } + } + + if let inputQuery = inputQuery { if inputQuery == currentQuery { return nil } else { switch inputQuery { - case let .emoji(query): - var signal: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> = .complete() - if let currentQuery = currentQuery { - switch currentQuery { - case .emoji: - break - default: - signal = .single({ _ in return nil }) - } - } - let stickers: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> = searchStickers(postbox: account.postbox, query: query) - |> map { stickers -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in - return { _ in - return .stickers(stickers) - } - } - return (inputQuery, signal |> then(stickers)) - case let .hashtag(query): - var signal: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> = .complete() - if let currentQuery = currentQuery { - switch currentQuery { - case .hashtag: - break - default: - signal = .single({ _ in return nil }) - } - } - - let hashtags: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> = recentlyUsedHashtags(postbox: account.postbox) |> map { hashtags -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in - let normalizedQuery = query.lowercased() - var result: [String] = [] - for hashtag in hashtags { - if hashtag.lowercased().hasPrefix(normalizedQuery) { - result.append(hashtag) - } - } - return { _ in return .hashtags(result) } - } - - return (inputQuery, signal |> then(hashtags)) - case let .mention(query): - let normalizedQuery = query.lowercased() - + case let .mention(query, _): if let peer = chatPresentationInterfaceState.peer { var signal: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> = .complete() if let currentQuery = currentQuery { switch currentQuery { - case .mention: - break - default: - signal = .single({ _ in return nil }) + case .mention: + break + default: + signal = .single({ _ in return nil }) } } - let participants = peerParticipants(account: account, id: peer.id) + let participants = searchGroupMembers(postbox: account.postbox, network: account.network, peerId: peer.id, query: query) |> map { peers -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in - let filteredPeers = peers.filter { peer in - if peer.indexName.matchesByTokens(normalizedQuery) { - return true - } - if let addressName = peer.addressName, addressName.lowercased().hasPrefix(normalizedQuery) { - return true - } - return false - } - let sortedPeers = filteredPeers.sorted(by: { lhs, rhs in + let filteredPeers = peers + var sortedPeers: [Peer] = [] + sortedPeers.append(contentsOf: filteredPeers.sorted(by: { lhs, rhs in let result = lhs.indexName.indexName(.lastNameFirst).compare(rhs.indexName.indexName(.lastNameFirst)) return result == .orderedAscending - }) + })) return { _ in return .mentions(sortedPeers) } - } - - return (inputQuery, signal |> then(participants)) - } else { - return (nil, .single({ _ in return nil })) - } - case let .command(query): - let normalizedQuery = query.lowercased() - - if let peer = chatPresentationInterfaceState.peer { - var signal: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> = .complete() - if let currentQuery = currentQuery { - switch currentQuery { - case .command: - break - default: - signal = .single({ _ in return nil }) - } - } - - let participants = peerCommands(account: account, id: peer.id) - |> map { commands -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in - let filteredCommands = commands.commands.filter { command in - if command.command.text.hasPrefix(normalizedQuery) { - return true - } - return false - } - let sortedCommands = filteredCommands - return { _ in return .commands(sortedCommands) } } return (inputQuery, signal |> then(participants)) } else { return (nil, .single({ _ in return nil })) } - case let .contextRequest(addressName, query): - guard let chatPeer = chatPresentationInterfaceState.peer else { - return (nil, .single({ _ in return nil })) - } - - var delayRequest = true - var signal: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> = .complete() - if let currentQuery = currentQuery { - switch currentQuery { - case let .contextRequest(currentAddressName, currentContextQuery) where currentAddressName == addressName: - if query.isEmpty && !currentContextQuery.isEmpty { - delayRequest = false - } - default: - delayRequest = false - signal = .single({ _ in return nil }) - } - } - - let contextBot = resolvePeerByName(account: account, name: addressName) - |> mapToSignal { peerId -> Signal in - if let peerId = peerId { - return account.postbox.loadedPeerWithId(peerId) - |> map { peer -> Peer? in - return peer - } - |> take(1) - } else { - return .single(nil) - } - } - |> mapToSignal { peer -> Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> in - if let user = peer as? TelegramUser, let botInfo = user.botInfo, let _ = botInfo.inlinePlaceholder { - let contextResults = requestChatContextResults(account: account, botId: user.id, peerId: chatPeer.id, query: query, offset: "") - |> map { results -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in - return { _ in - return .contextRequestResult(user, results) - } - } - - let botResult: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> = .single({ previousResult in - var passthroughPreviousResult: ChatContextResultCollection? - if let previousResult = previousResult { - if case let .contextRequestResult(previousUser, previousResults) = previousResult { - if previousUser.id == user.id { - passthroughPreviousResult = previousResults - } - } - } - return .contextRequestResult(user, passthroughPreviousResult) - }) - - let maybeDelayedContextResults: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> - if delayRequest { - maybeDelayedContextResults = contextResults |> delay(0.4, queue: Queue.concurrentDefaultQueue()) - } else { - maybeDelayedContextResults = contextResults - } - - return botResult |> then(maybeDelayedContextResults) - } else { - return .single({ _ in return nil }) - } - } - - return (inputQuery, signal |> then(contextBot)) + default: + return (nil, .single({ _ in return nil })) } } } else { @@ -190,9 +287,15 @@ func contextQueryResultStateForChatInterfacePresentationState(_ chatPresentation private let dataDetector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType([.link]).rawValue) -func urlPreviewStateForChatInterfacePresentationState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, account: Account, currentQuery: String?) -> (String?, Signal<(TelegramMediaWebpage?) -> TelegramMediaWebpage?, NoError>)? { +func urlPreviewStateForInputText(_ inputText: String?, account: Account, currentQuery: String?) -> (String?, Signal<(TelegramMediaWebpage?) -> TelegramMediaWebpage?, NoError>)? { + guard let text = inputText else { + if currentQuery != nil { + return (nil, .single({ _ in return nil })) + } else { + return nil + } + } if let dataDetector = dataDetector { - let text = chatPresentationInterfaceState.interfaceState.composeInputState.inputText let utf16 = text.utf16 var detectedUrl: String? diff --git a/TelegramUI/ChatInterfaceStateInputPanels.swift b/TelegramUI/ChatInterfaceStateInputPanels.swift index 289d9c5667..0222cd13a6 100644 --- a/TelegramUI/ChatInterfaceStateInputPanels.swift +++ b/TelegramUI/ChatInterfaceStateInputPanels.swift @@ -23,72 +23,102 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState if let selectionState = chatPresentationInterfaceState.interfaceState.selectionState { if let currentPanel = currentPanel as? ChatMessageSelectionInputPanelNode { - currentPanel.selectedMessageCount = selectionState.selectedIds.count + currentPanel.selectedMessages = selectionState.selectedIds currentPanel.interfaceInteraction = interfaceInteraction currentPanel.updateTheme(theme: chatPresentationInterfaceState.theme) return currentPanel } else { let panel = ChatMessageSelectionInputPanelNode(theme: chatPresentationInterfaceState.theme) panel.account = account - panel.selectedMessageCount = selectionState.selectedIds.count + panel.selectedMessages = selectionState.selectedIds panel.interfaceInteraction = interfaceInteraction return panel } - } else { - if chatPresentationInterfaceState.peerIsBlocked { - if let currentPanel = currentPanel as? ChatUnblockInputPanelNode { + } + if chatPresentationInterfaceState.peerIsBlocked { + if let currentPanel = currentPanel as? ChatUnblockInputPanelNode { + currentPanel.interfaceInteraction = interfaceInteraction + currentPanel.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) + return currentPanel + } else { + let panel = ChatUnblockInputPanelNode(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) + panel.account = account + panel.interfaceInteraction = interfaceInteraction + return panel + } + } + + var displayInputTextPanel = false + + if case .group = chatPresentationInterfaceState.chatLocation { + if chatPresentationInterfaceState.interfaceState.editMessage != nil { + displayInputTextPanel = true + } else { + if let currentPanel = currentPanel as? ChatFeedNavigationInputPanelNode { currentPanel.interfaceInteraction = interfaceInteraction currentPanel.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) return currentPanel } else { - let panel = ChatUnblockInputPanelNode(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) + let panel = ChatFeedNavigationInputPanelNode(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) panel.account = account panel.interfaceInteraction = interfaceInteraction return panel } } - - if let peer = chatPresentationInterfaceState.peer { - if let secretChat = peer as? TelegramSecretChat { - switch secretChat.embeddedState { - case .handshake: - if let currentPanel = currentPanel as? SecretChatHandshakeStatusInputPanelNode { + } + + if let peer = chatPresentationInterfaceState.peer { + if let secretChat = peer as? TelegramSecretChat { + switch secretChat.embeddedState { + case .handshake: + if let currentPanel = currentPanel as? SecretChatHandshakeStatusInputPanelNode { + return currentPanel + } else { + let panel = SecretChatHandshakeStatusInputPanelNode() + panel.account = account + panel.interfaceInteraction = interfaceInteraction + return panel + } + case .terminated: + if let currentPanel = currentPanel as? DeleteChatInputPanelNode { + return currentPanel + } else { + let panel = DeleteChatInputPanelNode() + panel.account = account + panel.interfaceInteraction = interfaceInteraction + return panel + } + case .active: + break + } + } else if let channel = peer as? TelegramChannel { + switch channel.participationStatus { + case .kicked: + if let currentPanel = currentPanel as? DeleteChatInputPanelNode { + return currentPanel + } else { + let panel = DeleteChatInputPanelNode() + panel.account = account + panel.interfaceInteraction = interfaceInteraction + return panel + } + case .member, .left: + break + } + switch channel.info { + case .broadcast: + if !channel.hasAdminRights([.canPostMessages]) { + if let currentPanel = currentPanel as? ChatChannelSubscriberInputPanelNode { return currentPanel } else { - let panel = SecretChatHandshakeStatusInputPanelNode() + let panel = ChatChannelSubscriberInputPanelNode() panel.account = account - panel.interfaceInteraction = interfaceInteraction return panel } - case .terminated: - if let currentPanel = currentPanel as? DeleteChatInputPanelNode { - return currentPanel - } else { - let panel = DeleteChatInputPanelNode() - panel.account = account - panel.interfaceInteraction = interfaceInteraction - return panel - } - case .active: - break - } - } else if let channel = peer as? TelegramChannel { - switch channel.participationStatus { - case .kicked: - if let currentPanel = currentPanel as? DeleteChatInputPanelNode { - return currentPanel - } else { - let panel = DeleteChatInputPanelNode() - panel.account = account - panel.interfaceInteraction = interfaceInteraction - return panel - } - case .member, .left: - break - } - switch channel.info { - case .broadcast: - if !channel.hasAdminRights([.canPostMessages]) { + } + case .group: + switch channel.participationStatus { + case .kicked, .left: if let currentPanel = currentPanel as? ChatChannelSubscriberInputPanelNode { return currentPanel } else { @@ -96,77 +126,81 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState panel.account = account return panel } - } - case .group: - switch channel.participationStatus { - case .kicked, .left: - if let currentPanel = currentPanel as? ChatChannelSubscriberInputPanelNode { - return currentPanel - } else { - let panel = ChatChannelSubscriberInputPanelNode() - panel.account = account - return panel - } - case .member: - break - } - } - } else if let group = peer as? TelegramGroup { - switch group.membership { - case .Removed, .Left: - if let currentPanel = currentPanel as? DeleteChatInputPanelNode { - return currentPanel - } else { - let panel = DeleteChatInputPanelNode() - panel.account = account - panel.interfaceInteraction = interfaceInteraction - return panel - } - case .Member: - break - } + case .member: + break + } } - - var displayBotStartPanel = false - if let _ = chatPresentationInterfaceState.botStartPayload { + } else if let group = peer as? TelegramGroup { + switch group.membership { + case .Removed, .Left: + if let currentPanel = currentPanel as? DeleteChatInputPanelNode { + return currentPanel + } else { + let panel = DeleteChatInputPanelNode() + panel.account = account + panel.interfaceInteraction = interfaceInteraction + return panel + } + case .Member: + break + } + } + + var displayBotStartPanel = false + if let _ = chatPresentationInterfaceState.botStartPayload { + displayBotStartPanel = true + } else if let chatHistoryState = chatPresentationInterfaceState.chatHistoryState, case .loaded(true) = chatHistoryState { + if let user = chatPresentationInterfaceState.peer as? TelegramUser, user.botInfo != nil { displayBotStartPanel = true - } else if let chatHistoryState = chatPresentationInterfaceState.chatHistoryState, case .loaded(true) = chatHistoryState { - if let user = chatPresentationInterfaceState.peer as? TelegramUser, user.botInfo != nil { - displayBotStartPanel = true - } } - - if displayBotStartPanel { - if let currentPanel = currentPanel as? ChatBotStartInputPanelNode { - currentPanel.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) + } + + if displayBotStartPanel { + if let currentPanel = currentPanel as? ChatBotStartInputPanelNode { + currentPanel.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) + return currentPanel + } else { + let panel = ChatBotStartInputPanelNode(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) + panel.account = account + panel.interfaceInteraction = interfaceInteraction + return panel + } + } else { + if let _ = chatPresentationInterfaceState.recordedMediaPreview { + if let currentPanel = currentPanel as? ChatRecordingPreviewInputPanelNode { + //currentPanel.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) return currentPanel } else { - let panel = ChatBotStartInputPanelNode(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) + let panel = ChatRecordingPreviewInputPanelNode(theme: chatPresentationInterfaceState.theme) panel.account = account panel.interfaceInteraction = interfaceInteraction return panel } - } else { - if let currentPanel = currentPanel as? ChatTextInputPanelNode { - currentPanel.interfaceInteraction = interfaceInteraction - return currentPanel - } else { - if let textInputPanelNode = textInputPanelNode { - textInputPanelNode.interfaceInteraction = interfaceInteraction - textInputPanelNode.account = account - return textInputPanelNode - } else { - let panel = ChatTextInputPanelNode(theme: chatPresentationInterfaceState.theme, presentController: { [weak interfaceInteraction] controller in - interfaceInteraction?.presentController(controller) - }) - panel.interfaceInteraction = interfaceInteraction - panel.account = account - return panel - } - } } - } else { - return nil + + displayInputTextPanel = true } } + + if displayInputTextPanel { + if let currentPanel = currentPanel as? ChatTextInputPanelNode { + currentPanel.interfaceInteraction = interfaceInteraction + return currentPanel + } else { + if let textInputPanelNode = textInputPanelNode { + textInputPanelNode.interfaceInteraction = interfaceInteraction + textInputPanelNode.account = account + return textInputPanelNode + } else { + let panel = ChatTextInputPanelNode(theme: chatPresentationInterfaceState.theme, presentController: { [weak interfaceInteraction] controller in + interfaceInteraction?.presentController(controller, nil) + }) + panel.interfaceInteraction = interfaceInteraction + panel.account = account + return panel + } + } + } else { + return nil + } } diff --git a/TelegramUI/ChatInterfaceStateNavigationButtons.swift b/TelegramUI/ChatInterfaceStateNavigationButtons.swift index 7b4d14ff14..b1239ac523 100644 --- a/TelegramUI/ChatInterfaceStateNavigationButtons.swift +++ b/TelegramUI/ChatInterfaceStateNavigationButtons.swift @@ -1,10 +1,13 @@ import Foundation import UIKit +import Postbox +import TelegramCore enum ChatNavigationButtonAction { case openChatInfo case clearHistory case cancelMessageSelection + case search } struct ChatNavigationButton: Equatable { @@ -16,25 +19,41 @@ struct ChatNavigationButton: Equatable { } } -func leftNavigationButtonForChatInterfaceState(_ chatInterfaceState: ChatInterfaceState, strings: PresentationStrings, currentButton: ChatNavigationButton?, target: Any?, selector: Selector?) -> ChatNavigationButton? { - if let _ = chatInterfaceState.selectionState { +func leftNavigationButtonForChatInterfaceState(_ presentationInterfaceState: ChatPresentationInterfaceState, strings: PresentationStrings, currentButton: ChatNavigationButton?, target: Any?, selector: Selector?) -> ChatNavigationButton? { + if let _ = presentationInterfaceState.interfaceState.selectionState { if let currentButton = currentButton, currentButton.action == .clearHistory { return currentButton - } else { - return ChatNavigationButton(action: .clearHistory, buttonItem: UIBarButtonItem(title: strings.Conversation_ClearAll, style: .plain, target: target, action: selector)) + } else if let peer = presentationInterfaceState.peer { + let canClear: Bool + if peer is TelegramUser || peer is TelegramGroup || peer is TelegramSecretChat { + canClear = true + } else if let peer = peer as? TelegramChannel, case .group = peer.info, peer.addressName == nil { + canClear = true + } else { + canClear = false + } + if canClear { + return ChatNavigationButton(action: .clearHistory, buttonItem: UIBarButtonItem(title: strings.Conversation_ClearAll, style: .plain, target: target, action: selector)) + } } } return nil } -func rightNavigationButtonForChatInterfaceState(_ chatInterfaceState: ChatInterfaceState, strings: PresentationStrings, currentButton: ChatNavigationButton?, target: Any?, selector: Selector?, chatInfoNavigationButton: ChatNavigationButton?) -> ChatNavigationButton? { - if let _ = chatInterfaceState.selectionState { +func rightNavigationButtonForChatInterfaceState(_ presentationInterfaceState: ChatPresentationInterfaceState, strings: PresentationStrings, currentButton: ChatNavigationButton?, target: Any?, selector: Selector?, chatInfoNavigationButton: ChatNavigationButton?) -> ChatNavigationButton? { + if let _ = presentationInterfaceState.interfaceState.selectionState { if let currentButton = currentButton, currentButton.action == .cancelMessageSelection { return currentButton } else { return ChatNavigationButton(action: .cancelMessageSelection, buttonItem: UIBarButtonItem(title: strings.Common_Cancel, style: .plain, target: target, action: selector)) } } + + if let peer = presentationInterfaceState.peer { + if presentationInterfaceState.accountPeerId == peer.id { + return ChatNavigationButton(action: .search, buttonItem: UIBarButtonItem(image: PresentationResourcesRootController.navigationSearchIcon(presentationInterfaceState.theme), style: .plain, target: target, action: selector)) + } + } return chatInfoNavigationButton } diff --git a/TelegramUI/ChatInterfaceTitlePanelNodes.swift b/TelegramUI/ChatInterfaceTitlePanelNodes.swift index fae2817ef6..b439b7dd75 100644 --- a/TelegramUI/ChatInterfaceTitlePanelNodes.swift +++ b/TelegramUI/ChatInterfaceTitlePanelNodes.swift @@ -2,6 +2,12 @@ import Foundation import TelegramCore func titlePanelForChatPresentationInterfaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, account: Account, currentPanel: ChatTitleAccessoryPanelNode?, interfaceInteraction: ChatPanelInterfaceInteraction?) -> ChatTitleAccessoryPanelNode? { + if case .overlay = chatPresentationInterfaceState.mode { + return nil + } + if chatPresentationInterfaceState.search != nil { + return nil + } var selectedContext: ChatTitlePanelContext? if !chatPresentationInterfaceState.titlePanelContexts.isEmpty { loop: for context in chatPresentationInterfaceState.titlePanelContexts.reversed() { diff --git a/TelegramUI/ChatItemGalleryFooterContentNode.swift b/TelegramUI/ChatItemGalleryFooterContentNode.swift index 9074da659b..7e9a1f96f7 100644 --- a/TelegramUI/ChatItemGalleryFooterContentNode.swift +++ b/TelegramUI/ChatItemGalleryFooterContentNode.swift @@ -28,6 +28,30 @@ private let pauseImage = generateImage(CGSize(width: 16.0, height: 16.0), rotate context.translateBy(x: -(diameter - size.width) / 2.0, y: -(diameter - size.height) / 2.0) }) +private let playImage = generateImage(CGSize(width: 15.0, height: 18.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + let color = UIColor.white + let diameter: CGFloat = 16.0 + + context.setFillColor(color.cgColor) + + context.translateBy(x: (diameter - size.width) / 2.0 + 1.5, y: (diameter - size.height) / 2.0 + 1.0) + if (diameter < 40.0) { + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.scaleBy(x: 0.8, y: 0.8) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + } + let _ = try? drawSvgPath(context, path: "M1.71891969,0.209353049 C0.769586558,-0.350676705 0,0.0908839327 0,1.18800046 L0,16.8564753 C0,17.9569971 0.750549162,18.357187 1.67393713,17.7519379 L14.1073836,9.60224049 C15.0318735,8.99626906 15.0094718,8.04970371 14.062401,7.49100858 L1.71891969,0.209353049 ") + context.fillPath() + if (diameter < 40.0) { + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.scaleBy(x: 1.0 / 0.8, y: 1.0 / 0.8) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + } + context.translateBy(x: -(diameter - size.width) / 2.0 - 1.5, y: -(diameter - size.height) / 2.0) +}) + private let textFont = Font.regular(16.0) private let titleFont = Font.medium(15.0) private let dateFont = Font.regular(14.0) @@ -35,6 +59,7 @@ private let dateFont = Font.regular(14.0) enum ChatItemGalleryFooterContent { case info case playbackPause + case playbackPlay } final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode { @@ -71,6 +96,12 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode { self.authorNameNode.isHidden = true self.dateNode.isHidden = true self.playbackControlButton.isHidden = false + self.playbackControlButton.setImage(pauseImage, for: []) + case .playbackPlay: + self.authorNameNode.isHidden = true + self.dateNode.isHidden = true + self.playbackControlButton.isHidden = false + self.playbackControlButton.setImage(playImage, for: []) } } } @@ -94,7 +125,6 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode { self.dateNode.maximumNumberOfLines = 1 self.playbackControlButton = HighlightableButtonNode() - self.playbackControlButton.setImage(pauseImage, for: []) self.playbackControlButton.isHidden = true super.init() @@ -119,7 +149,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode { func setup(origin: GalleryItemOriginData?, caption: String) { let titleText = origin?.title - let dateText = origin?.timestamp.flatMap { humanReadableStringForTimestamp(strings: self.strings, timestamp: $0) } + let dateText = origin?.timestamp.flatMap { humanReadableStringForTimestamp(strings: self.strings, timeFormat: .regular, timestamp: $0) } if self.currentMessageText != caption || self.currentAuthorNameText != titleText || self.currentDateText != dateText { self.currentMessageText = caption @@ -179,17 +209,28 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode { authorNameText = peer.displayTitle } - let dateText = humanReadableStringForTimestamp(strings: self.strings, timestamp: message.timestamp) + let dateText = humanReadableStringForTimestamp(strings: self.strings, timeFormat: .regular, timestamp: message.timestamp) - if self.currentMessageText != message.text || canDelete != !self.deleteButton.isHidden || self.currentAuthorNameText != authorNameText || self.currentDateText != dateText { - self.currentMessageText = message.text + var messageText = "" + var hasCaption = false + for media in message.media { + if media is TelegramMediaImage || media is TelegramMediaFile { + hasCaption = true + } + } + if hasCaption { + messageText = message.text + } + + if self.currentMessageText != messageText || canDelete != !self.deleteButton.isHidden || self.currentAuthorNameText != authorNameText || self.currentDateText != dateText { + self.currentMessageText = messageText - if message.text.isEmpty { + if messageText.isEmpty { self.textNode.isHidden = true self.textNode.attributedText = nil } else { self.textNode.isHidden = false - self.textNode.attributedText = NSAttributedString(string: message.text, font: textFont, textColor: .white) + self.textNode.attributedText = NSAttributedString(string: messageText, font: textFont, textColor: .white) } if let authorNameText = authorNameText { @@ -205,31 +246,31 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode { } } - override func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { - var panelHeight: CGFloat = 44.0 + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { + var panelHeight: CGFloat = 44.0 + bottomInset if !self.textNode.isHidden { - let sideInset: CGFloat = 8.0 + let sideInset: CGFloat = 8.0 + leftInset let topInset: CGFloat = 8.0 - let bottomInset: CGFloat = 8.0 + let textBottomInset: CGFloat = 8.0 let textSize = self.textNode.measure(CGSize(width: width - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude)) - panelHeight += textSize.height + topInset + bottomInset + panelHeight += textSize.height + topInset + textBottomInset transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: sideInset, y: topInset), size: textSize)) } - self.actionButton.frame = CGRect(origin: CGPoint(x: 0.0, y: panelHeight - 44.0), size: CGSize(width: 44.0, height: 44.0)) - self.deleteButton.frame = CGRect(origin: CGPoint(x: width - 44.0, y: panelHeight - 44.0), size: CGSize(width: 44.0, height: 44.0)) + transition.updateFrame(view: self.actionButton, frame: CGRect(origin: CGPoint(x: leftInset, y: panelHeight - bottomInset - 44.0), size: CGSize(width: 44.0, height: 44.0))) + transition.updateFrame(view: self.deleteButton, frame: CGRect(origin: CGPoint(x: width - 44.0 - rightInset, y: panelHeight - bottomInset - 44.0), size: CGSize(width: 44.0, height: 44.0))) - self.playbackControlButton.frame = CGRect(origin: CGPoint(x: floor((width - 44.0) / 2.0), y: panelHeight - 44.0), size: CGSize(width: 44.0, height: 44.0)) + transition.updateFrame(node: self.playbackControlButton, frame: CGRect(origin: CGPoint(x: floor((width - 44.0) / 2.0), y: panelHeight - bottomInset - 44.0), size: CGSize(width: 44.0, height: 44.0))) - let authorNameSize = self.authorNameNode.measure(CGSize(width: width - 44.0 * 2.0 - 8.0 * 2.0, height: CGFloat.greatestFiniteMagnitude)) + let authorNameSize = self.authorNameNode.measure(CGSize(width: width - 44.0 * 2.0 - 8.0 * 2.0 - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude)) let dateSize = self.dateNode.measure(CGSize(width: width - 44.0 * 2.0 - 8.0 * 2.0, height: CGFloat.greatestFiniteMagnitude)) if authorNameSize.height.isZero { - transition.updateFrame(node: self.dateNode, frame: CGRect(origin: CGPoint(x: floor((width - dateSize.width) / 2.0), y: panelHeight - 44.0 + floor((44.0 - dateSize.height) / 2.0)), size: dateSize)) + transition.updateFrame(node: self.dateNode, frame: CGRect(origin: CGPoint(x: floor((width - dateSize.width) / 2.0), y: panelHeight - bottomInset - 44.0 + floor((44.0 - dateSize.height) / 2.0)), size: dateSize)) } else { let labelsSpacing: CGFloat = 0.0 - transition.updateFrame(node: self.authorNameNode, frame: CGRect(origin: CGPoint(x: floor((width - authorNameSize.width) / 2.0), y: panelHeight - 44.0 + floor((44.0 - dateSize.height - authorNameSize.height - labelsSpacing) / 2.0)), size: authorNameSize)) - transition.updateFrame(node: self.dateNode, frame: CGRect(origin: CGPoint(x: floor((width - dateSize.width) / 2.0), y: panelHeight - 44.0 + floor((44.0 - dateSize.height - authorNameSize.height - labelsSpacing) / 2.0) + authorNameSize.height + labelsSpacing), size: dateSize)) + transition.updateFrame(node: self.authorNameNode, frame: CGRect(origin: CGPoint(x: floor((width - authorNameSize.width) / 2.0), y: panelHeight - bottomInset - 44.0 + floor((44.0 - dateSize.height - authorNameSize.height - labelsSpacing) / 2.0)), size: authorNameSize)) + transition.updateFrame(node: self.dateNode, frame: CGRect(origin: CGPoint(x: floor((width - dateSize.width) / 2.0), y: panelHeight - bottomInset - 44.0 + floor((44.0 - dateSize.height - authorNameSize.height - labelsSpacing) / 2.0) + authorNameSize.height + labelsSpacing), size: dateSize)) } return panelHeight @@ -237,44 +278,117 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode { @objc func deleteButtonPressed() { if let currentMessage = self.currentMessage { - self.messageContextDisposable.set((chatDeleteMessagesOptions(account: self.account, messageIds: [currentMessage.id]) |> deliverOnMainQueue).start(next: { [weak self] options in - if let strongSelf = self, let controllerInteration = strongSelf.controllerInteraction, !options.isEmpty { - let actionSheet = ActionSheetController(presentationTheme: strongSelf.theme) - var items: [ActionSheetItem] = [] - var personalPeerName: String? - var isChannel = false - if let user = currentMessage.peers[currentMessage.id.peerId] as? TelegramUser { - personalPeerName = user.compactDisplayTitle - } else if let channel = currentMessage.peers[currentMessage.id.peerId] as? TelegramChannel, case .broadcast = channel.info { - isChannel = true - } - - if options.contains(.globally) { - let globalTitle: String - if isChannel { - globalTitle = strongSelf.strings.Common_Delete - } else if let personalPeerName = personalPeerName { - globalTitle = strongSelf.strings.Conversation_DeleteMessagesFor(personalPeerName).0 - } else { - globalTitle = strongSelf.strings.Conversation_DeleteMessagesForEveryone + let _ = (self.account.postbox.modify { modifier -> [Message] in + return modifier.getMessageGroup(currentMessage.id) ?? [] + } |> deliverOnMainQueue).start(next: { [weak self] messages in + if let strongSelf = self, !messages.isEmpty { + if messages.count == 1 { + strongSelf.commitDeleteMessages(messages, ask: true) + } else { + let presentationData = strongSelf.account.telegramApplicationContext.currentPresentationData.with { $0 } + var generalMessageContentKind: MessageContentKind? + for message in messages { + let currentKind = messageContentKind(message, strings: presentationData.strings, accountPeerId: strongSelf.account.peerId) + if generalMessageContentKind == nil || generalMessageContentKind == currentKind { + generalMessageContentKind = currentKind + } else { + generalMessageContentKind = nil + break + } } - items.append(ActionSheetButtonItem(title: globalTitle, color: .destructive, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - if let strongSelf = self { - let _ = deleteMessagesInteractively(postbox: strongSelf.account.postbox, messageIds: [currentMessage.id], type: .forEveryone).start() - strongSelf.controllerInteraction?.dismissController() + + var singleText = presentationData.strings.Media_ShareItem(1) + var multipleText = presentationData.strings.Media_ShareItem(Int32(messages.count)) + + if let generalMessageContentKind = generalMessageContentKind { + switch generalMessageContentKind { + case .image: + singleText = presentationData.strings.Media_ShareThisPhoto + multipleText = presentationData.strings.Media_SharePhoto(Int32(messages.count)) + case .video: + singleText = presentationData.strings.Media_ShareThisVideo + multipleText = presentationData.strings.Media_ShareVideo(Int32(messages.count)) + default: + break } - })) - } - if options.contains(.locally) { - items.append(ActionSheetButtonItem(title: strongSelf.strings.Conversation_DeleteMessagesForMe, color: .destructive, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() + } + + let deleteAction: ([Message]) -> Void = { messages in if let strongSelf = self { - let _ = deleteMessagesInteractively(postbox: strongSelf.account.postbox, messageIds: [currentMessage.id], type: .forLocalPeer).start() - strongSelf.controllerInteraction?.dismissController() + strongSelf.commitDeleteMessages(messages, ask: false) } - })) + } + + let actionSheet = ActionSheetController(presentationTheme: presentationData.theme) + let items: [ActionSheetItem] = [ + ActionSheetButtonItem(title: singleText, color: .destructive, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + deleteAction([currentMessage]) + }), + ActionSheetButtonItem(title: multipleText, color: .destructive, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + deleteAction(messages) + }) + ] + + actionSheet.setItemGroups([ + ActionSheetItemGroup(items: items), + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ]) + ]) + strongSelf.controllerInteraction?.presentController(actionSheet, nil) } + } + }) + } + } + + private func commitDeleteMessages(_ messages: [Message], ask: Bool) { + self.messageContextDisposable.set((chatDeleteMessagesOptions(postbox: self.account.postbox, accountPeerId: self.account.peerId, messageIds: Set(messages.map { $0.id })) |> deliverOnMainQueue).start(next: { [weak self] options in + if let strongSelf = self, let controllerInteration = strongSelf.controllerInteraction, !options.isEmpty { + let actionSheet = ActionSheetController(presentationTheme: strongSelf.theme) + var items: [ActionSheetItem] = [] + var personalPeerName: String? + var isChannel = false + if let user = messages[0].peers[messages[0].id.peerId] as? TelegramUser { + personalPeerName = user.compactDisplayTitle + } else if let channel = messages[0].peers[messages[0].id.peerId] as? TelegramChannel, case .broadcast = channel.info { + isChannel = true + } + + if options.contains(.globally) { + let globalTitle: String + if isChannel { + globalTitle = strongSelf.strings.Common_Delete + } else if let personalPeerName = personalPeerName { + globalTitle = strongSelf.strings.Conversation_DeleteMessagesFor(personalPeerName).0 + } else { + globalTitle = strongSelf.strings.Conversation_DeleteMessagesForEveryone + } + items.append(ActionSheetButtonItem(title: globalTitle, color: .destructive, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + if let strongSelf = self { + let _ = deleteMessagesInteractively(postbox: strongSelf.account.postbox, messageIds: messages.map { $0.id }, type: .forEveryone).start() + strongSelf.controllerInteraction?.dismissController() + } + })) + } + if options.contains(.locally) { + items.append(ActionSheetButtonItem(title: strongSelf.strings.Conversation_DeleteMessagesForMe, color: .destructive, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + if let strongSelf = self { + let _ = deleteMessagesInteractively(postbox: strongSelf.account.postbox, messageIds: messages.map { $0.id }, type: .forLocalPeer).start() + strongSelf.controllerInteraction?.dismissController() + } + })) + } + if !ask && items.count == 1 { + let _ = deleteMessagesInteractively(postbox: strongSelf.account.postbox, messageIds: messages.map { $0.id }, type: .forEveryone).start() + strongSelf.controllerInteraction?.dismissController() + } else { actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: strongSelf.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() @@ -282,16 +396,87 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode { ])]) controllerInteration.presentController(actionSheet, nil) } - })) - } + } + })) } @objc func actionButtonPressed() { - if let controllerInteraction = self.controllerInteraction, let currentMessage = self.currentMessage { - var saveToCameraRoll: (() -> Void)? - var shareAction: (([PeerId]) -> Void)? - let shareController = ShareController(account: self.account, subject: .message(currentMessage), saveToCameraRoll: true) - controllerInteraction.presentController(shareController, nil) + if let currentMessage = self.currentMessage { + let _ = (self.account.postbox.modify { modifier -> [Message] in + return modifier.getMessageGroup(currentMessage.id) ?? [] + } |> deliverOnMainQueue).start(next: { [weak self] messages in + if let strongSelf = self, !messages.isEmpty { + let presentationData = strongSelf.account.telegramApplicationContext.currentPresentationData.with { $0 } + var generalMessageContentKind: MessageContentKind? + for message in messages { + let currentKind = messageContentKind(message, strings: presentationData.strings, accountPeerId: strongSelf.account.peerId) + if generalMessageContentKind == nil || generalMessageContentKind == currentKind { + generalMessageContentKind = currentKind + } else { + generalMessageContentKind = nil + break + } + } + var saveToCameraRoll = false + if let generalMessageContentKind = generalMessageContentKind { + switch generalMessageContentKind { + case .image, .video: + saveToCameraRoll = true + default: + break + } + } + + if messages.count == 1 { + let shareController = ShareController(account: strongSelf.account, subject: .messages([currentMessage]), saveToCameraRoll: saveToCameraRoll) + strongSelf.controllerInteraction?.presentController(shareController, nil) + } else { + var singleText = presentationData.strings.Media_ShareItem(1) + var multipleText = presentationData.strings.Media_ShareItem(Int32(messages.count)) + + if let generalMessageContentKind = generalMessageContentKind { + switch generalMessageContentKind { + case .image: + singleText = presentationData.strings.Media_ShareThisPhoto + multipleText = presentationData.strings.Media_SharePhoto(Int32(messages.count)) + case .video: + singleText = presentationData.strings.Media_ShareThisVideo + multipleText = presentationData.strings.Media_ShareVideo(Int32(messages.count)) + default: + break + } + } + + let shareAction: ([Message]) -> Void = { messages in + if let strongSelf = self { + let shareController = ShareController(account: strongSelf.account, subject: .messages(messages), saveToCameraRoll: saveToCameraRoll) + strongSelf.controllerInteraction?.presentController(shareController, nil) + } + } + + let actionSheet = ActionSheetController(presentationTheme: presentationData.theme) + let items: [ActionSheetItem] = [ + ActionSheetButtonItem(title: singleText, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + shareAction([currentMessage]) + }), + ActionSheetButtonItem(title: multipleText, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + shareAction(messages) + }) + ] + + actionSheet.setItemGroups([ActionSheetItemGroup(items: items), + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ]) + ]) + strongSelf.controllerInteraction?.presentController(actionSheet, nil) + } + } + }) } } diff --git a/TelegramUI/ChatItemGalleryItemNode.swift b/TelegramUI/ChatItemGalleryItemNode.swift deleted file mode 100644 index 99761d7be2..0000000000 --- a/TelegramUI/ChatItemGalleryItemNode.swift +++ /dev/null @@ -1,5 +0,0 @@ -import Foundation -import Postbox -import TelegramCore - - diff --git a/TelegramUI/ChatListController.swift b/TelegramUI/ChatListController.swift index 9eeb4bd9fa..f2a8e71035 100644 --- a/TelegramUI/ChatListController.swift +++ b/TelegramUI/ChatListController.swift @@ -6,6 +6,9 @@ import TelegramCore public class ChatListController: TelegramController, UIViewControllerPreviewingDelegate { private let account: Account + private let controlsHistoryPreload: Bool + + public let groupId: PeerGroupId? let openMessageFromSearchDisposable: MetaDisposable = MetaDisposable() @@ -26,28 +29,37 @@ public class ChatListController: TelegramController, UIViewControllerPreviewingD private var presentationData: PresentationData private var presentationDataDisposable: Disposable? - public override init(account: Account) { + public init(account: Account, groupId: PeerGroupId?, controlsHistoryPreload: Bool) { self.account = account + self.controlsHistoryPreload = controlsHistoryPreload + + self.groupId = groupId self.presentationData = (account.telegramApplicationContext.currentPresentationData.with { $0 }) self.titleView = NetworkStatusTitleView(theme: self.presentationData.theme) - super.init(account: account) + super.init(account: account, navigationBarTheme: NavigationBarTheme(rootControllerTheme: self.presentationData.theme), enableMediaAccessoryPanel: true) self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style - self.navigationBar?.item = nil + if groupId == nil { + self.navigationBar?.item = nil - self.titleView.title = NetworkStatusTitle(text: self.presentationData.strings.DialogList_Title, activity: false) - self.navigationItem.titleView = self.titleView - self.tabBarItem.title = self.presentationData.strings.DialogList_Title - self.tabBarItem.image = UIImage(bundleImageName: "Chat List/Tabs/IconChats") - self.tabBarItem.selectedImage = UIImage(bundleImageName: "Chat List/Tabs/IconChatsSelected") + self.titleView.title = NetworkStatusTitle(text: self.presentationData.strings.DialogList_Title, activity: false) + self.navigationItem.titleView = self.titleView + self.tabBarItem.title = self.presentationData.strings.DialogList_Title + self.tabBarItem.image = UIImage(bundleImageName: "Chat List/Tabs/IconChats") + self.tabBarItem.selectedImage = UIImage(bundleImageName: "Chat List/Tabs/IconChats") + + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed)) + self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationComposeIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.composePressed)) + } else { + self.navigationItem.title = "Channels" + self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed)) + } self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.DialogList_Title, style: .plain, target: nil, action: nil) - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed)) - self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationComposeIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.composePressed)) self.scrollToTop = { [weak self] in if let strongSelf = self { @@ -146,12 +158,18 @@ public class ChatListController: TelegramController, UIViewControllerPreviewingD editing = state.editing return state } + let editItem: UIBarButtonItem if editing { - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)) + editItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)) } else { - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed)) + editItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed)) + } + if self.groupId == nil { + self.navigationItem.leftBarButtonItem = editItem + self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationComposeIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.composePressed)) + } else { + self.navigationItem.rightBarButtonItem = editItem } - self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationComposeIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.composePressed)) self.titleView.theme = self.presentationData.theme @@ -159,12 +177,12 @@ public class ChatListController: TelegramController, UIViewControllerPreviewingD self.navigationBar?.updateTheme(NavigationBarTheme(rootControllerTheme: self.presentationData.theme)) if self.isNodeLoaded { - self.chatListDisplayNode.updateThemeAndStrings(theme: self.presentationData.theme, strings: self.presentationData.strings) + self.chatListDisplayNode.updateThemeAndStrings(theme: self.presentationData.theme, strings: self.presentationData.strings, timeFormat: self.presentationData.timeFormat) } } override public func loadDisplayNode() { - self.displayNode = ChatListControllerNode(account: self.account, theme: self.presentationData.theme, strings: self.presentationData.strings) + self.displayNode = ChatListControllerNode(account: self.account, groupId: self.groupId, controlsHistoryPreload: self.controlsHistoryPreload, theme: self.presentationData.theme, strings: self.presentationData.strings, timeFormat: self.presentationData.timeFormat) self.chatListDisplayNode.navigationBar = self.navigationBar @@ -184,36 +202,77 @@ public class ChatListController: TelegramController, UIViewControllerPreviewingD self.chatListDisplayNode.chatListNode.deletePeerChat = { [weak self] peerId in if let strongSelf = self { - let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) - actionSheet.setItemGroups([ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Delete, color: .destructive, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - - if let strongSelf = self { - let _ = removePeerChat(postbox: strongSelf.account.postbox, peerId: peerId, reportChatSpam: false).start() + let _ = (strongSelf.account.postbox.modify { modifier -> Peer? in + return modifier.getPeer(peerId) + } |> deliverOnMainQueue).start(next: { peer in + if let strongSelf = self, let peer = peer { + let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) + var items: [ActionSheetItem] = [] + var canClear = true + if let channel = peer as? TelegramChannel { + if case .broadcast = channel.info { + canClear = false + } + if let addressName = channel.addressName, !addressName.isEmpty { + canClear = false + } } - }) - ]), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + if canClear { + items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.DialogList_ClearHistoryConfirmation, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + + if let strongSelf = self { + let _ = clearHistoryInteractively(postbox: strongSelf.account.postbox, peerId: peerId).start() + } + })) + } + + items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Delete, color: .destructive, action: { [weak actionSheet] in actionSheet?.dismissAnimated() + + if let strongSelf = self { + let _ = removePeerChat(postbox: strongSelf.account.postbox, peerId: peerId, reportChatSpam: false).start() + } + })) + + actionSheet.setItemGroups([ActionSheetItemGroup(items: items), + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ]) + ]) + strongSelf.present(actionSheet, in: .window(.root)) + } }) - ])]) - strongSelf.present(actionSheet, in: .window(.root)) } } self.chatListDisplayNode.chatListNode.peerSelected = { [weak self] peerId in if let strongSelf = self { - (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, peerId: peerId)) + (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, chatLocation: .peer(peerId))) strongSelf.chatListDisplayNode.chatListNode.clearHighlightAnimated(true) } } + self.chatListDisplayNode.chatListNode.groupSelected = { [weak self] groupId in + if let strongSelf = self { + (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, chatLocation: .group(groupId))) + strongSelf.chatListDisplayNode.chatListNode.clearHighlightAnimated(true) + } + } + + self.chatListDisplayNode.chatListNode.updatePeerGrouping = { [weak self] peerId, group in + if let strongSelf = self { + let _ = updatePeerGroupIdInteractively(postbox: strongSelf.account.postbox, peerId: peerId, groupId: group ? Namespaces.PeerGroup.feed : nil).start() + } + } + self.chatListDisplayNode.requestOpenMessageFromSearch = { [weak self] peer, messageId in if let strongSelf = self { strongSelf.openMessageFromSearchDisposable.set((storedMessageFromSearchPeer(account: strongSelf.account, peer: peer) |> deliverOnMainQueue).start(completed: { [weak strongSelf] in if let strongSelf = strongSelf { - (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, peerId: messageId.peerId, messageId: messageId)) + (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, chatLocation: .peer(messageId.peerId), messageId: messageId)) } })) } @@ -231,12 +290,39 @@ public class ChatListController: TelegramController, UIViewControllerPreviewingD strongSelf.openMessageFromSearchDisposable.set((storedPeer |> deliverOnMainQueue).start(completed: { [weak strongSelf] in if let strongSelf = strongSelf { strongSelf.dismissSearchOnDisappear = true - (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, peerId: peer.id)) + (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, chatLocation: .peer(peer.id))) } })) } } + self.chatListDisplayNode.requestOpenRecentPeerOptions = { [weak self] peer in + if let strongSelf = self { + strongSelf.chatListDisplayNode.view.endEditing(true) + let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) + + actionSheet.setItemGroups([ + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Delete, color: .destructive, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + + if let strongSelf = self { + let _ = removeRecentPeer(account: strongSelf.account, peerId: peer.id).start() + let searchContainer = strongSelf.chatListDisplayNode.searchDisplayController?.contentNode as? ChatListSearchContainerNode + searchContainer?.removePeerFromTopPeers(peer.id) + } + }) + ]), + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ]) + ]) + strongSelf.present(actionSheet, in: .window(.root)) + } + } + self.displayNodeDidLoad() } @@ -267,14 +353,24 @@ public class ChatListController: TelegramController, UIViewControllerPreviewingD } @objc func editPressed() { - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)) + let editItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)) + if self.groupId == nil { + self.navigationItem.leftBarButtonItem = editItem + } else { + self.navigationItem.rightBarButtonItem = editItem + } self.chatListDisplayNode.chatListNode.updateState { state in return state.withUpdatedEditing(true) } } @objc func donePressed() { - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed)) + let editItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed)) + if self.groupId == nil { + self.navigationItem.leftBarButtonItem = editItem + } else { + self.navigationItem.rightBarButtonItem = editItem + } self.chatListDisplayNode.chatListNode.updateState { state in return state.withUpdatedEditing(false).withUpdatedPeerIdWithRevealedOptions(nil) } @@ -302,6 +398,14 @@ public class ChatListController: TelegramController, UIViewControllerPreviewingD } public func previewingContext(_ previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController? { + let boundsSize = self.view.bounds.size + var contentSize = CGSize(width: boundsSize.width, height: boundsSize.height - (boundsSize.height > boundsSize.width ? 50.0 : 10.0)) + if (boundsSize.width.isEqual(to: 375.0) && boundsSize.height.isEqual(to: 812.0)) { + contentSize.height -= 56.0 + } else if (boundsSize.height.isEqual(to: 375.0) && boundsSize.width.isEqual(to: 812.0)) { + contentSize.height += 107.0 + } + if let searchController = self.chatListDisplayNode.searchDisplayController { if let (view, action) = searchController.previewViewAndActionAtLocation(location) { if let peerId = action as? PeerId { @@ -311,7 +415,7 @@ public class ChatListController: TelegramController, UIViewControllerPreviewingD previewingContext.sourceRect = sourceRect } - let chatController = ChatController(account: self.account, peerId: peerId) + let chatController = ChatController(account: self.account, chatLocation: .peer(peerId)) chatController.peekActions = .remove({ [weak self] in if let strongSelf = self { let _ = removeRecentPeer(account: strongSelf.account, peerId: peerId).start() @@ -320,7 +424,7 @@ public class ChatListController: TelegramController, UIViewControllerPreviewingD } }) chatController.canReadHistory.set(false) - chatController.containerLayoutUpdated(ContainerViewLayout(size: CGSize(width: self.view.bounds.size.width, height: self.view.bounds.size.height - (self.view.bounds.size.height > self.view.bounds.size.width ? 50.0 : 10.0)), metrics: LayoutMetrics(), intrinsicInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil), transition: .immediate) + chatController.containerLayoutUpdated(ContainerViewLayout(size: CGSize(width: self.view.bounds.size.width, height: self.view.bounds.size.height - (self.view.bounds.size.height > self.view.bounds.size.width ? 50.0 : 10.0)), metrics: LayoutMetrics(), intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false), transition: .immediate) return chatController } else if let messageId = action as? MessageId { if #available(iOSApplicationExtension 9.0, *) { @@ -329,9 +433,9 @@ public class ChatListController: TelegramController, UIViewControllerPreviewingD previewingContext.sourceRect = sourceRect } - let chatController = ChatController(account: self.account, peerId: messageId.peerId, messageId: messageId) + let chatController = ChatController(account: self.account, chatLocation: .peer(messageId.peerId), messageId: messageId) chatController.canReadHistory.set(false) - chatController.containerLayoutUpdated(ContainerViewLayout(size: CGSize(width: self.view.bounds.size.width, height: self.view.bounds.size.height - (self.view.bounds.size.height > self.view.bounds.size.width ? 50.0 : 10.0)), metrics: LayoutMetrics(), intrinsicInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil), transition: .immediate) + chatController.containerLayoutUpdated(ContainerViewLayout(size: CGSize(width: self.view.bounds.size.width, height: self.view.bounds.size.height - (self.view.bounds.size.height > self.view.bounds.size.width ? 50.0 : 10.0)), metrics: LayoutMetrics(), intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false), transition: .immediate) return chatController } } @@ -352,10 +456,17 @@ public class ChatListController: TelegramController, UIViewControllerPreviewingD sourceRect.size.height -= UIScreenPixel previewingContext.sourceRect = sourceRect } - let chatController = ChatController(account: self.account, peerId: item.peer.peerId) - chatController.canReadHistory.set(false) - chatController.containerLayoutUpdated(ContainerViewLayout(size: CGSize(width: self.view.bounds.size.width, height: self.view.bounds.size.height - (self.view.bounds.size.height > self.view.bounds.size.width ? 50.0 : 10.0)), metrics: LayoutMetrics(), intrinsicInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil), transition: .immediate) - return chatController + switch item.content { + case let .peer(_, peer, _, _, _, _, _): + let chatController = ChatController(account: self.account, chatLocation: .peer(peer.peerId)) + chatController.canReadHistory.set(false) + chatController.containerLayoutUpdated(ContainerViewLayout(size: contentSize, metrics: LayoutMetrics(), intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false), transition: .immediate) + return chatController + case let .groupReference(groupId, _, _, _): + let chatListController = ChatListController(account: self.account, groupId: groupId, controlsHistoryPreload: false) + chatListController.containerLayoutUpdated(ContainerViewLayout(size: contentSize, metrics: LayoutMetrics(), intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false), transition: .immediate) + return chatListController + } } else { return nil } diff --git a/TelegramUI/ChatListControllerNode.swift b/TelegramUI/ChatListControllerNode.swift index 9205efc5d9..13082e5020 100644 --- a/TelegramUI/ChatListControllerNode.swift +++ b/TelegramUI/ChatListControllerNode.swift @@ -6,6 +6,7 @@ import TelegramCore class ChatListControllerNode: ASDisplayNode { private let account: Account + private let groupId: PeerGroupId? let chatListNode: ChatListNode var navigationBar: NavigationBar? @@ -16,15 +17,17 @@ class ChatListControllerNode: ASDisplayNode { var requestDeactivateSearch: (() -> Void)? var requestOpenPeerFromSearch: ((Peer) -> Void)? + var requestOpenRecentPeerOptions: ((Peer) -> Void)? var requestOpenMessageFromSearch: ((Peer, MessageId) -> Void)? - var themeAndStrings: (PresentationTheme, PresentationStrings) + var themeAndStrings: (PresentationTheme, PresentationStrings, timeFormat: PresentationTimeFormat) - init(account: Account, theme: PresentationTheme, strings: PresentationStrings) { + init(account: Account, groupId: PeerGroupId?, controlsHistoryPreload: Bool, theme: PresentationTheme, strings: PresentationStrings, timeFormat: PresentationTimeFormat) { self.account = account - self.chatListNode = ChatListNode(account: account, mode: .chatList, theme: theme, strings: strings) + self.groupId = groupId + self.chatListNode = ChatListNode(account: account, groupId: groupId, controlsHistoryPreload: controlsHistoryPreload, mode: .chatList, theme: theme, strings: strings, timeFormat: timeFormat) - self.themeAndStrings = (theme, strings) + self.themeAndStrings = (theme, strings, timeFormat) super.init() @@ -35,9 +38,9 @@ class ChatListControllerNode: ASDisplayNode { self.addSubnode(self.chatListNode) } - func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { - self.themeAndStrings = (theme, strings) - self.chatListNode.updateThemeAndStrings(theme: theme, strings: strings) + func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings, timeFormat: PresentationTimeFormat) { + self.themeAndStrings = (theme, strings, timeFormat) + self.chatListNode.updateThemeAndStrings(theme: theme, strings: strings, timeFormat: timeFormat) self.searchDisplayController?.updateThemeAndStrings(theme: theme, strings: strings) } @@ -47,6 +50,9 @@ class ChatListControllerNode: ASDisplayNode { var insets = layout.insets(options: [.input]) insets.top += max(navigationBarHeight, layout.insets(options: [.statusBar]).top) + insets.left += layout.safeInsets.left + insets.right += layout.safeInsets.right + self.chatListNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) self.chatListNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0) @@ -98,10 +104,10 @@ class ChatListControllerNode: ASDisplayNode { } if let placeholderNode = maybePlaceholderNode { - self.searchDisplayController = SearchDisplayController(theme: self.themeAndStrings.0, strings: self.themeAndStrings.1, contentNode: ChatListSearchContainerNode(account: self.account, openPeer: { [weak self] peer in - if let requestOpenPeerFromSearch = self?.requestOpenPeerFromSearch { - requestOpenPeerFromSearch(peer) - } + self.searchDisplayController = SearchDisplayController(theme: self.themeAndStrings.0, strings: self.themeAndStrings.1, contentNode: ChatListSearchContainerNode(account: self.account, onlyWriteable: false, groupId: self.groupId, openPeer: { [weak self] peer in + self?.requestOpenPeerFromSearch?(peer) + }, openRecentPeerOptions: { [weak self] peer in + self?.requestOpenRecentPeerOptions?(peer) }, openMessage: { [weak self] peer, messageId in if let requestOpenMessageFromSearch = self?.requestOpenMessageFromSearch { requestOpenMessageFromSearch(peer, messageId) diff --git a/TelegramUI/ChatListHoleItem.swift b/TelegramUI/ChatListHoleItem.swift index e1bd218aa3..d77d9143ca 100644 --- a/TelegramUI/ChatListHoleItem.swift +++ b/TelegramUI/ChatListHoleItem.swift @@ -8,24 +8,27 @@ import SwiftSignalKit private let titleFont = Font.regular(17.0) class ChatListHoleItem: ListViewItem { + let theme: PresentationTheme + let selectable: Bool = false - init() { + init(theme: PresentationTheme) { + self.theme = theme } - func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { async { let node = ChatListHoleItemNode() node.relativePosition = (first: previousItem == nil, last: nextItem == nil) node.insets = ChatListItemNode.insets(first: false, last: false, firstWithHeader: false) - node.layoutForWidth(width, item: self, previousItem: previousItem, nextItem: nextItem) + node.layoutForParams(params, item: self, previousItem: previousItem, nextItem: nextItem) completion(node, { return (nil, {}) }) } } - func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { assert(node is ChatListHoleItemNode) if let node = node as? ChatListHoleItemNode { Queue.mainQueue().async { @@ -34,7 +37,7 @@ class ChatListHoleItem: ListViewItem { let first = previousItem == nil let last = nextItem == nil - let (nodeLayout, apply) = layout(width, first, last) + let (nodeLayout, apply) = layout(self, params, first, last) Queue.mainQueue().async { completion(nodeLayout, { [weak node] in apply() @@ -68,26 +71,28 @@ class ChatListHoleItemNode: ListViewItemNode { self.addSubnode(self.labelNode) } - override func layoutForWidth(_ width: CGFloat, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { + override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { let layout = self.asyncLayout() - let (_, apply) = layout(width, self.relativePosition.first, self.relativePosition.last) + let (_, apply) = layout(item as! ChatListHoleItem, params, self.relativePosition.first, self.relativePosition.last) apply() } - func asyncLayout() -> (_ width: CGFloat, _ first: Bool, _ last: Bool) -> (ListViewItemNodeLayout, () -> Void) { + func asyncLayout() -> (_ item: ChatListHoleItem, _ params: ListViewItemLayoutParams, _ first: Bool, _ last: Bool) -> (ListViewItemNodeLayout, () -> Void) { let labelNodeLayout = TextNode.asyncLayout(self.labelNode) - return { width, first, last in - let (labelLayout, labelApply) = labelNodeLayout(NSAttributedString(string: "", font: titleFont, textColor: UIColor(rgb: 0xc8c7cc)), nil, 1, .end, CGSize(width: width, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + return { item, params, first, last in + let baseWidth = params.width - params.leftInset - params.rightInset + + let (labelLayout, labelApply) = labelNodeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "", font: titleFont, textColor: item.theme.chatList.messageTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: baseWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let insets = ChatListItemNode.insets(first: first, last: last, firstWithHeader: false) - let layout = ListViewItemNodeLayout(contentSize: CGSize(width: width, height: 68.0), insets: insets) + let layout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 68.0), insets: insets) let separatorInset: CGFloat if last { separatorInset = 0.0 } else { - separatorInset = 80.0 + separatorInset = 80.0 + params.leftInset } return (layout, { [weak self] in @@ -96,9 +101,11 @@ class ChatListHoleItemNode: ListViewItemNode { let _ = labelApply() - strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: floor((width - labelLayout.size.width) / 2.0), y: floor((layout.contentSize.height - labelLayout.size.height) / 2.0)), size: labelLayout.size) + strongSelf.separatorNode.backgroundColor = item.theme.chatList.itemSeparatorColor - strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: separatorInset, y: 68.0 - separatorHeight), size: CGSize(width: width - separatorInset, height: separatorHeight)) + strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: floor((params.width - labelLayout.size.width) / 2.0), y: floor((layout.contentSize.height - labelLayout.size.height) / 2.0)), size: labelLayout.size) + + strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: separatorInset, y: 68.0 - separatorHeight), size: CGSize(width: params.width - separatorInset, height: separatorHeight)) strongSelf.contentSize = layout.contentSize strongSelf.insets = layout.insets diff --git a/TelegramUI/ChatListItem.swift b/TelegramUI/ChatListItem.swift index 6572ecc60f..ddf669ca75 100644 --- a/TelegramUI/ChatListItem.swift +++ b/TelegramUI/ChatListItem.swift @@ -6,52 +6,45 @@ import Display import SwiftSignalKit import TelegramCore +enum ChatListItemContent { + case peer(message: Message?, peer: RenderedPeer, combinedReadState: CombinedPeerReadState?, notificationSettings: PeerNotificationSettings?, summaryInfo: ChatListMessageTagSummaryInfo, embeddedState: PeerChatListEmbeddedInterfaceState?, inputActivities: [(Peer, PeerInputActivity)]?) + case groupReference(groupId: PeerGroupId, message: Message?, topPeers: [Peer], counters: GroupReferenceUnreadCounters) +} + class ChatListItem: ListViewItem { - let theme: PresentationTheme - let strings: PresentationStrings + let presentationData: ChatListPresentationData let account: Account + let peerGroupId: PeerGroupId? let index: ChatListIndex - let message: Message? - let peer: RenderedPeer - let combinedReadState: CombinedPeerReadState? - let notificationSettings: PeerNotificationSettings? - let summaryInfo: ChatListMessageTagSummaryInfo - let embeddedState: PeerChatListEmbeddedInterfaceState? + let content: ChatListItemContent let editing: Bool let hasActiveRevealControls: Bool - let inputActivities: [(Peer, PeerInputActivity)]? let interaction: ChatListNodeInteraction let selectable: Bool = true let header: ListViewItemHeader? - init(theme: PresentationTheme, strings: PresentationStrings, account: Account, index: ChatListIndex, message: Message?, peer: RenderedPeer, combinedReadState: CombinedPeerReadState?, notificationSettings: PeerNotificationSettings?, summaryInfo: ChatListMessageTagSummaryInfo, embeddedState: PeerChatListEmbeddedInterfaceState?, editing: Bool, hasActiveRevealControls: Bool, inputActivities: [(Peer, PeerInputActivity)]?, header: ListViewItemHeader?, interaction: ChatListNodeInteraction) { - self.theme = theme - self.strings = strings + init(presentationData: ChatListPresentationData, account: Account, peerGroupId: PeerGroupId?, index: ChatListIndex, content: ChatListItemContent, editing: Bool, hasActiveRevealControls: Bool, header: ListViewItemHeader?, interaction: ChatListNodeInteraction) { + self.presentationData = presentationData + self.peerGroupId = peerGroupId self.account = account self.index = index - self.message = message - self.peer = peer - self.combinedReadState = combinedReadState - self.notificationSettings = notificationSettings - self.summaryInfo = summaryInfo - self.embeddedState = embeddedState + self.content = content self.editing = editing self.hasActiveRevealControls = hasActiveRevealControls - self.inputActivities = inputActivities self.header = header self.interaction = interaction } - func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { async { let node = ChatListItemNode() node.setupItem(item: self) let (first, last, firstWithHeader, nextIsPinned) = ChatListItem.mergeType(item: self, previousItem: previousItem, nextItem: nextItem) node.insets = ChatListItemNode.insets(first: first, last: last, firstWithHeader: firstWithHeader) - let (nodeLayout, apply) = node.asyncLayout()(self, width, first, last, firstWithHeader, nextIsPinned) + let (nodeLayout, apply) = node.asyncLayout()(self, params, first, last, firstWithHeader, nextIsPinned) node.insets = nodeLayout.insets node.contentSize = nodeLayout.contentSize @@ -64,7 +57,7 @@ class ChatListItem: ListViewItem { } } - func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { assert(node is ChatListItemNode) if let node = node as? ChatListItemNode { Queue.mainQueue().async { @@ -77,7 +70,7 @@ class ChatListItem: ListViewItem { animated = false } - let (nodeLayout, apply) = layout(self, width, first, last, firstWithHeader, nextIsPinned) + let (nodeLayout, apply) = layout(self, params, first, last, firstWithHeader, nextIsPinned) Queue.mainQueue().async { completion(nodeLayout, { apply(animated) @@ -89,10 +82,15 @@ class ChatListItem: ListViewItem { } func selected(listView: ListView) { - if let message = self.message { - self.interaction.messageSelected(message) - } else if let peer = self.peer.peers[self.peer.peerId] { - self.interaction.peerSelected(peer) + switch self.content { + case let .peer(message, peer, _, _, _, _, _): + if let message = message { + self.interaction.messageSelected(message) + } else if let peer = peer.peers[peer.peerId] { + self.interaction.peerSelected(peer) + } + case let .groupReference(groupId, _, _, _): + self.interaction.groupSelected(groupId) } } @@ -134,6 +132,8 @@ private let unpinIcon = UIImage(bundleImageName: "Chat List/RevealActionUnpinIco private let muteIcon = UIImage(bundleImageName: "Chat List/RevealActionMuteIcon")?.precomposed() private let unmuteIcon = UIImage(bundleImageName: "Chat List/RevealActionUnmuteIcon")?.precomposed() private let deleteIcon = UIImage(bundleImageName: "Chat List/RevealActionDeleteIcon")?.precomposed() +private let groupIcon = UIImage(bundleImageName: "Chat List/RevealActionGroupIcon")?.precomposed() +private let ungroupIcon = UIImage(bundleImageName: "Chat List/RevealActionUngroupIcon")?.precomposed() private enum RevealOptionKey: Int32 { case pin @@ -141,23 +141,38 @@ private enum RevealOptionKey: Int32 { case mute case unmute case delete + case group + case ungroup } private let itemHeight: CGFloat = 76.0 -private func revealOptions(strings: PresentationStrings, isPinned: Bool, isMuted: Bool) -> [ItemListRevealOption] { +private func revealOptions(strings: PresentationStrings, theme: PresentationTheme, isPinned: Bool?, isMuted: Bool?, hasPeerGroupId: Bool?, canDelete: Bool) -> [ItemListRevealOption] { var options: [ItemListRevealOption] = [] - if isPinned { - options.append(ItemListRevealOption(key: RevealOptionKey.unpin.rawValue, title: strings.DialogList_Unpin, icon: unpinIcon, color: UIColor(rgb: 0xbcbcc3))) - } else { - options.append(ItemListRevealOption(key: RevealOptionKey.pin.rawValue, title: strings.DialogList_Pin, icon: pinIcon, color: UIColor(rgb: 0xbcbcc3))) + if let isPinned = isPinned { + if isPinned { + options.append(ItemListRevealOption(key: RevealOptionKey.unpin.rawValue, title: strings.DialogList_Unpin, icon: unpinIcon, color: theme.list.itemDisclosureActions.neutral1.fillColor, textColor: theme.list.itemDisclosureActions.neutral1.foregroundColor)) + } else { + options.append(ItemListRevealOption(key: RevealOptionKey.pin.rawValue, title: strings.DialogList_Pin, icon: pinIcon, color: theme.list.itemDisclosureActions.neutral1.fillColor, textColor: theme.list.itemDisclosureActions.neutral1.foregroundColor)) + } } - if isMuted { - options.append(ItemListRevealOption(key: RevealOptionKey.unmute.rawValue, title: strings.Conversation_Unmute, icon: unmuteIcon, color: UIColor(rgb: 0xaaaab3))) - } else { - options.append(ItemListRevealOption(key: RevealOptionKey.mute.rawValue, title: strings.Conversation_Mute, icon: muteIcon, color: UIColor(rgb: 0xaaaab3))) + if let isMuted = isMuted { + if isMuted { + options.append(ItemListRevealOption(key: RevealOptionKey.unmute.rawValue, title: strings.Conversation_Unmute, icon: unmuteIcon, color: theme.list.itemDisclosureActions.neutral2.fillColor, textColor: theme.list.itemDisclosureActions.neutral2.foregroundColor)) + } else { + options.append(ItemListRevealOption(key: RevealOptionKey.mute.rawValue, title: strings.Conversation_Mute, icon: muteIcon, color: theme.list.itemDisclosureActions.neutral2.fillColor, textColor: theme.list.itemDisclosureActions.neutral2.foregroundColor)) + } + } + if let hasPeerGroupId = hasPeerGroupId { + if hasPeerGroupId { + options.append(ItemListRevealOption(key: RevealOptionKey.ungroup.rawValue, title: "Ungroup", icon: ungroupIcon, color: theme.list.itemAccentColor, textColor: theme.list.itemDisclosureActions.neutral2.foregroundColor)) + } else { + options.append(ItemListRevealOption(key: RevealOptionKey.group.rawValue, title: "Group", icon: groupIcon, color: theme.list.itemAccentColor, textColor: theme.list.itemDisclosureActions.neutral2.foregroundColor)) + } + } + if canDelete { + options.append(ItemListRevealOption(key: RevealOptionKey.delete.rawValue, title: strings.Common_Delete, icon: deleteIcon, color: theme.list.itemDisclosureActions.destructive.fillColor, textColor: theme.list.itemDisclosureActions.destructive.foregroundColor)) } - options.append(ItemListRevealOption(key: RevealOptionKey.delete.rawValue, title: strings.Common_Delete, icon: deleteIcon, color: UIColor(rgb: 0xff3824))) return options } @@ -172,6 +187,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { private let highlightedBackgroundNode: ASDisplayNode let avatarNode: AvatarNode + var multipleAvatarsNode: MultipleAvatarsNode? let titleNode: TextNode let authorNode: TextNode let textNode: TextNode @@ -182,12 +198,13 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let badgeBackgroundNode: ASImageNode let badgeTextNode: TextNode let mentionBadgeNode: ASImageNode + var secretIconNode: ASImageNode? var verificationIconNode: ASImageNode? let mutedIconNode: ASImageNode var editableControlNode: ItemListEditableControlNode? - var layoutParams: (ChatListItem, first: Bool, last: Bool, firstWithHeader: Bool, nextIsPinned: Bool)? + var layoutParams: (ChatListItem, first: Bool, last: Bool, firstWithHeader: Bool, nextIsPinned: Bool, ListViewItemLayoutParams)? override var canBeSelected: Bool { if self.editableControlNode != nil { @@ -211,20 +228,14 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { self.titleNode = TextNode() self.titleNode.isLayerBacked = true self.titleNode.displaysAsynchronously = true - //self.titleNode.contentMode = .topLeft - //self.titleNode.contentsScale = self.titleNode.contentsScaleForDisplay self.authorNode = TextNode() self.authorNode.isLayerBacked = true self.authorNode.displaysAsynchronously = true - //self.authorNode.contentMode = .topLeft - //self.authorNode.contentsScale = self.titleNode.contentsScaleForDisplay self.textNode = TextNode() self.textNode.isLayerBacked = true self.textNode.displaysAsynchronously = true - //self.textNode.contentMode = .topLeft - //self.textNode.contentsScale = self.titleNode.contentsScaleForDisplay self.inputActivitiesNode = ChatListInputActivitiesNode() self.inputActivitiesNode.alpha = 0.0 @@ -280,22 +291,31 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { func setupItem(item: ChatListItem) { self.item = item - if let message = item.message { - let peer = messageMainPeer(message) - if let peer = peer { - self.avatarNode.setPeer(account: item.account, peer: peer) - } - } else { - if let peer = item.peer.chatMainPeer { - self.avatarNode.setPeer(account: item.account, peer: peer) + var peer: Peer? + switch item.content { + case let .peer(message, peerValue, _, _, _, _, _): + if let message = message { + peer = messageMainPeer(message) + } else { + peer = peerValue.chatMainPeer + } + case .groupReference: + break + } + + if let peer = peer { + var overrideImage: AvatarNodeImageOverride? + if peer.id == item.account.peerId { + overrideImage = .savedMessagesIcon } + self.avatarNode.setPeer(account: item.account, peer: peer, overrideImage: overrideImage) } } - override func layoutForWidth(_ width: CGFloat, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { + override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { let layout = self.asyncLayout() let (first, last, firstWithHeader, nextIsPinned) = ChatListItem.mergeType(item: item as! ChatListItem, previousItem: previousItem, nextItem: nextItem) - let (nodeLayout, apply) = layout(item as! ChatListItem, width, first, last, firstWithHeader, nextIsPinned) + let (nodeLayout, apply) = layout(item as! ChatListItem, params, first, last, firstWithHeader, nextIsPinned) apply(false) self.contentSize = nodeLayout.contentSize self.insets = nodeLayout.insets @@ -305,8 +325,8 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { return UIEdgeInsets(top: firstWithHeader ? 29.0 : 0.0, left: 0.0, bottom: 0.0, right: 0.0) } - override func setHighlighted(_ highlighted: Bool, animated: Bool) { - super.setHighlighted(highlighted, animated: animated) + override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { + super.setHighlighted(highlighted, at: point, animated: animated) if highlighted { /*var nodes: [ASDisplayNode] = [self.titleNode, self.textNode, self.dateNode, self.statusNode] @@ -337,7 +357,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } } - func asyncLayout() -> (_ item: ChatListItem, _ width: CGFloat, _ first: Bool, _ last: Bool, _ firstWithHeader: Bool, _ nextIsPinned: Bool) -> (ListViewItemNodeLayout, (Bool) -> Void) { + func asyncLayout() -> (_ item: ChatListItem, _ params: ListViewItemLayoutParams, _ first: Bool, _ last: Bool, _ firstWithHeader: Bool, _ nextIsPinned: Bool) -> (ListViewItemNodeLayout, (Bool) -> Void) { let dateLayout = TextNode.asyncLayout(self.dateNode) let textLayout = TextNode.asyncLayout(self.textNode) let titleLayout = TextNode.asyncLayout(self.titleNode) @@ -348,19 +368,66 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let currentItem = self.layoutParams?.0 - return { item, width, first, last, firstWithHeader, nextIsPinned in + let multipleAvatarsLayout = MultipleAvatarsNode.asyncLayout(self.multipleAvatarsNode) + + return { item, params, first, last, firstWithHeader, nextIsPinned in let account = item.account - let message = item.message - let combinedReadState = item.combinedReadState - let notificationSettings = item.notificationSettings - let embeddedState = item.embeddedState + let message: Message? + let itemPeer: RenderedPeer + let combinedReadState: CombinedPeerReadState? + let unreadCount: (count: Int32, muted: Bool) + let notificationSettings: PeerNotificationSettings? + let embeddedState: PeerChatListEmbeddedInterfaceState? + let summaryInfo: ChatListMessageTagSummaryInfo + let inputActivities: [(Peer, PeerInputActivity)]? + let isPeerGroup: Bool - let theme = item.theme.chatList + var multipleAvatarsApply: ((Bool) -> MultipleAvatarsNode)? + + switch item.content { + case let .peer(messageValue, peerValue, combinedReadStateValue, notificationSettingsValue, summaryInfoValue, embeddedStateValue, inputActivitiesValue): + message = messageValue + itemPeer = peerValue + combinedReadState = combinedReadStateValue + if let combinedReadState = combinedReadState { + unreadCount = (combinedReadState.count, notificationSettingsValue?.isRemovedFromTotalUnreadCount ?? false) + } else { + unreadCount = (0, false) + } + notificationSettings = notificationSettingsValue + embeddedState = embeddedStateValue + summaryInfo = summaryInfoValue + inputActivities = inputActivitiesValue + isPeerGroup = false + case let .groupReference(_, messageValue, topPeersValue, counters): + if let messageValue = messageValue { + itemPeer = RenderedPeer(message: messageValue) + } else { + itemPeer = RenderedPeer(peerId: item.index.messageIndex.id.peerId, peers: SimpleDictionary()) + } + message = messageValue + combinedReadState = nil + notificationSettings = nil + embeddedState = nil + summaryInfo = ChatListMessageTagSummaryInfo() + inputActivities = nil + isPeerGroup = true + multipleAvatarsApply = multipleAvatarsLayout(item.account, topPeersValue, CGSize(width: 60.0, height: 60.0)) + if counters.unreadCount > 0 { + unreadCount = (counters.unreadCount + counters.unreadMutedCount, false) + } else if counters.unreadMutedCount > 0 { + unreadCount = (counters.unreadMutedCount, true) + } else{ + unreadCount = (0, false) + } + } + + let theme = item.presentationData.theme.chatList var updatedTheme: PresentationTheme? - if currentItem?.theme !== item.theme { - updatedTheme = item.theme + if currentItem?.presentationData.theme !== item.presentationData.theme { + updatedTheme = item.presentationData.theme } var authorAttributedString: NSAttributedString? @@ -374,40 +441,65 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { var currentMentionBadgeImage: UIImage? var currentMutedIconImage: UIImage? var currentVerificationIconImage: UIImage? + var currentSecretIconImage: UIImage? var editableControlSizeAndApply: (CGSize, () -> ItemListEditableControlNode)? let editingOffset: CGFloat if item.editing { - let sizeAndApply = editableControlLayout(itemHeight) + let sizeAndApply = editableControlLayout(itemHeight, item.presentationData.theme, isPeerGroup) editableControlSizeAndApply = sizeAndApply editingOffset = sizeAndApply.0.width } else { editingOffset = 0.0 } - let (peer, hideAuthor, messageText) = chatListItemStrings(strings: item.strings, message: item.message, chatPeer: item.peer, accountPeerId: item.account.peerId) + let leftInset: CGFloat = params.leftInset + 78.0 + + let (peer, initialHideAuthor, messageText) = chatListItemStrings(strings: item.presentationData.strings, message: message, chatPeer: itemPeer, accountPeerId: item.account.peerId) + var hideAuthor = initialHideAuthor + if isPeerGroup { + hideAuthor = false + } let attributedText: NSAttributedString + var hasDraft = false if let embeddedState = embeddedState as? ChatEmbeddedInterfaceState { - authorAttributedString = NSAttributedString(string: item.strings.DialogList_Draft, font: textFont, textColor: theme.messageDraftTextColor) + hasDraft = true + authorAttributedString = NSAttributedString(string: item.presentationData.strings.DialogList_Draft, font: textFont, textColor: theme.messageDraftTextColor) attributedText = NSAttributedString(string: embeddedState.text, font: textFont, textColor: theme.messageTextColor) - } else if let message = message, let author = message.author as? TelegramUser, let peer = peer, !(peer is TelegramUser) { - if let peer = peer as? TelegramChannel, case .broadcast = peer.info { - attributedText = NSAttributedString(string: messageText as String, font: textFont, textColor: theme.messageTextColor) - } else { - let peerText: String = author.id == account.peerId ? item.strings.DialogList_You : author.displayTitle - + } else if let message = message { + attributedText = NSAttributedString(string: messageText as String, font: textFont, textColor: theme.messageTextColor) + + var peerText: String? + if let author = message.author as? TelegramUser, let peer = peer, !(peer is TelegramUser) { + if let peer = peer as? TelegramChannel, case .broadcast = peer.info { + } else { + peerText = author.id == account.peerId ? item.presentationData.strings.DialogList_You : author.displayTitle + } + } else if case .groupReference = item.content { + if let messagePeer = itemPeer.chatMainPeer { + peerText = messagePeer.displayTitle + } + } + + if let peerText = peerText { authorAttributedString = NSAttributedString(string: peerText, font: textFont, textColor: theme.authorNameColor) - attributedText = NSAttributedString(string: messageText, font: textFont, textColor: theme.messageTextColor) } } else { attributedText = NSAttributedString(string: messageText as String, font: textFont, textColor: theme.messageTextColor) } - if let displayTitle = peer?.displayTitle { - titleAttributedString = NSAttributedString(string: displayTitle, font: titleFont, textColor: item.index.messageIndex.id.peerId.namespace == Namespaces.Peer.SecretChat ? theme.secretTitleColor : theme.titleColor) + switch item.content { + case .peer: + if peer?.id == item.account.peerId { + titleAttributedString = NSAttributedString(string: item.presentationData.strings.DialogList_SavedMessages, font: titleFont, textColor: theme.titleColor) + } else if let displayTitle = peer?.displayTitle { + titleAttributedString = NSAttributedString(string: displayTitle, font: titleFont, textColor: item.index.messageIndex.id.peerId.namespace == Namespaces.Peer.SecretChat ? theme.secretTitleColor : theme.titleColor) + } + case .groupReference: + titleAttributedString = NSAttributedString(string: "Feed", font: titleFont, textColor: theme.titleColor) } textAttributedString = attributedText @@ -417,52 +509,49 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { localtime_r(&t, &timeinfo) let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) - let dateText = stringForRelativeTimestamp(strings: item.strings, relativeTimestamp: item.index.messageIndex.timestamp, relativeTo: timestamp) + let dateText = stringForRelativeTimestamp(strings: item.presentationData.strings, relativeTimestamp: item.index.messageIndex.timestamp, relativeTo: timestamp, timeFormat: item.presentationData.timeFormat) dateAttributedString = NSAttributedString(string: dateText, font: dateFont, textColor: theme.dateTextColor) - if let message = message, message.author?.id == account.peerId { - if !message.flags.isSending { + if let message = message, message.author?.id == account.peerId && !hasDraft { + if message.flags.isSending { + statusImage = PresentationResourcesChatList.pendingImage(item.presentationData.theme) + } else { if let combinedReadState = combinedReadState, combinedReadState.isOutgoingMessageIndexRead(MessageIndex(message)) { - statusImage = PresentationResourcesChatList.doubleCheckImage(item.theme) + statusImage = PresentationResourcesChatList.doubleCheckImage(item.presentationData.theme) } else { - statusImage = PresentationResourcesChatList.singleCheckImage(item.theme) + statusImage = PresentationResourcesChatList.singleCheckImage(item.presentationData.theme) } } } - if let unreadCount = combinedReadState?.count, unreadCount > 0 { - if let message = message, message.tags.contains(.unseenPersonalMessage), unreadCount == 1 { + if unreadCount.count > 0 { + if let message = message, message.tags.contains(.unseenPersonalMessage), unreadCount.count == 1 { } else { let badgeTextColor: UIColor - if let notificationSettings = notificationSettings as? TelegramPeerNotificationSettings { - if case .unmuted = notificationSettings.muteState { - currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundActive(item.theme) - badgeTextColor = theme.unreadBadgeActiveTextColor - } else { - currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundInactive(item.theme) - badgeTextColor = theme.unreadBadgeInactiveTextColor - } + if unreadCount.muted { + currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundInactive(item.presentationData.theme) + badgeTextColor = theme.unreadBadgeInactiveTextColor } else { - currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundActive(item.theme) + currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundActive(item.presentationData.theme) badgeTextColor = theme.unreadBadgeActiveTextColor } - badgeAttributedString = NSAttributedString(string: "\(unreadCount)", font: badgeFont, textColor: badgeTextColor) + badgeAttributedString = NSAttributedString(string: "\(unreadCount.count)", font: badgeFont, textColor: badgeTextColor) } } - let tagSummaryCount = item.summaryInfo.tagSummaryCount ?? 0 - let actionsSummaryCount = item.summaryInfo.actionsSummaryCount ?? 0 + let tagSummaryCount = summaryInfo.tagSummaryCount ?? 0 + let actionsSummaryCount = summaryInfo.actionsSummaryCount ?? 0 let totalMentionCount = tagSummaryCount - actionsSummaryCount if totalMentionCount > 0 { - currentMentionBadgeImage = PresentationResourcesChatList.badgeBackgroundMention(item.theme) + currentMentionBadgeImage = PresentationResourcesChatList.badgeBackgroundMention(item.presentationData.theme) } else if item.index.pinningIndex != nil && currentBadgeBackgroundImage == nil { - currentMentionBadgeImage = PresentationResourcesChatList.badgeBackgroundPinned(item.theme) + currentMentionBadgeImage = PresentationResourcesChatList.badgeBackgroundPinned(item.presentationData.theme) } if let notificationSettings = notificationSettings as? TelegramPeerNotificationSettings { if case .muted = notificationSettings.muteState { - currentMutedIconImage = PresentationResourcesChatList.mutedIcon(item.theme) + currentMutedIconImage = PresentationResourcesChatList.mutedIcon(item.presentationData.theme) } } @@ -477,7 +566,8 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } var isVerified = false - if let peer = item.peer.chatMainPeer { + let isSecret = item.index.messageIndex.id.peerId.namespace == Namespaces.Peer.SecretChat + if let peer = itemPeer.chatMainPeer { if let peer = peer as? TelegramUser { isVerified = peer.flags.contains(.isVerified) } else if let peer = peer as? TelegramChannel { @@ -485,8 +575,15 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } } + if isSecret { + currentSecretIconImage = PresentationResourcesChatList.secretIcon(item.presentationData.theme) + } + if isVerified { - currentVerificationIconImage = PresentationResourcesChatList.verifiedIcon(item.theme) + currentVerificationIconImage = PresentationResourcesChatList.verifiedIcon(item.presentationData.theme) + } + if let currentSecretIconImage = currentSecretIconImage { + titleIconsWidth += currentSecretIconImage.size.width + 2.0 } if let currentVerificationIconImage = currentVerificationIconImage { if titleIconsWidth.isZero { @@ -497,11 +594,11 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { titleIconsWidth += currentVerificationIconImage.size.width } - let rawContentRect = CGRect(origin: CGPoint(x: 2.0, y: 8.0), size: CGSize(width: width - 78.0 - 10.0 - 1.0 - editingOffset, height: itemHeight - 12.0 - 9.0)) + let rawContentRect = CGRect(origin: CGPoint(x: 2.0, y: 8.0), size: CGSize(width: params.width - leftInset - params.rightInset - 10.0 - 1.0 - editingOffset, height: itemHeight - 12.0 - 9.0)) - let (dateLayout, dateApply) = dateLayout(dateAttributedString, nil, 1, .end, CGSize(width: rawContentRect.width, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let (dateLayout, dateApply) = dateLayout(TextNodeLayoutArguments(attributedString: dateAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: rawContentRect.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let (badgeLayout, badgeApply) = badgeTextLayout(badgeAttributedString, nil, 1, .end, CGSize(width: 50.0, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let (badgeLayout, badgeApply) = badgeTextLayout(TextNodeLayoutArguments(attributedString: badgeAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 50.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) var badgeSize: CGFloat = 0.0 if let currentBadgeBackgroundImage = currentBadgeBackgroundImage { @@ -515,33 +612,53 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } } - let (authorLayout, authorApply) = authorLayout(hideAuthor ? nil : authorAttributedString, nil, 1, .end, CGSize(width: rawContentRect.width - badgeSize, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets(top: 2.0, left: 1.0, bottom: 2.0, right: 1.0)) + let (authorLayout, authorApply) = authorLayout(TextNodeLayoutArguments(attributedString: hideAuthor ? nil : authorAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: rawContentRect.width - badgeSize, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 1.0, bottom: 2.0, right: 1.0))) - let (textLayout, textApply) = textLayout(textAttributedString, nil, authorAttributedString == nil ? 2 : 1, .end, CGSize(width: rawContentRect.width - badgeSize, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets(top: 2.0, left: 1.0, bottom: 2.0, right: 1.0)) + let (textLayout, textApply) = textLayout(TextNodeLayoutArguments(attributedString: textAttributedString, backgroundColor: nil, maximumNumberOfLines: authorAttributedString == nil ? 2 : 1, truncationType: .end, constrainedSize: CGSize(width: rawContentRect.width - badgeSize, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 1.0, bottom: 2.0, right: 1.0))) let titleRect = CGRect(origin: rawContentRect.origin, size: CGSize(width: rawContentRect.width - dateLayout.size.width - 10.0 - statusWidth - titleIconsWidth, height: rawContentRect.height)) - let (titleLayout, titleApply) = titleLayout(titleAttributedString, nil, 1, .end, CGSize(width: titleRect.width, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let (titleLayout, titleApply) = titleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: titleRect.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) var inputActivitiesSize: CGSize? var inputActivitiesApply: (() -> Void)? - if let inputActivities = item.inputActivities, !inputActivities.isEmpty { - let (size, apply) = inputActivitiesLayout(CGSize(width: rawContentRect.width - badgeSize, height: 40.0), item.strings, item.theme.chatList.messageTextColor, item.index.messageIndex.id.peerId, inputActivities) + if let inputActivities = inputActivities, !inputActivities.isEmpty { + let (size, apply) = inputActivitiesLayout(CGSize(width: rawContentRect.width - badgeSize, height: 40.0), item.presentationData.strings, item.presentationData.theme.chatList.messageTextColor, item.index.messageIndex.id.peerId, inputActivities) inputActivitiesSize = size inputActivitiesApply = apply } let insets = ChatListItemNode.insets(first: first, last: last, firstWithHeader: firstWithHeader) - let layout = ListViewItemNodeLayout(contentSize: CGSize(width: width, height: itemHeight), insets: insets) + let layout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: itemHeight), insets: insets) - let peerRevealOptions = revealOptions(strings: item.strings, isPinned: item.index.pinningIndex != nil, isMuted: currentMutedIconImage != nil) + let peerRevealOptions: [ItemListRevealOption] + switch item.content { + case .peer: + var hasPeerGroupId: Bool? + if GlobalExperimentalSettings.enableFeed { + if let chatMainPeer = itemPeer.chatMainPeer as? TelegramChannel, case .broadcast = chatMainPeer.info { + hasPeerGroupId = item.peerGroupId != nil + } + } + + var isPinned: Bool? + if item.peerGroupId == nil { + isPinned = item.index.pinningIndex != nil + } + + peerRevealOptions = revealOptions(strings: item.presentationData.strings, theme: item.presentationData.theme, isPinned: isPinned, isMuted: item.account.peerId != item.index.messageIndex.id.peerId ? (currentMutedIconImage != nil) : nil, hasPeerGroupId: hasPeerGroupId, canDelete: true) + case .groupReference: + let isPinned = item.index.pinningIndex != nil + + peerRevealOptions = revealOptions(strings: item.presentationData.strings, theme: item.presentationData.theme, isPinned: isPinned, isMuted: nil, hasPeerGroupId: nil, canDelete: false) + } return (layout, { [weak self] animated in if let strongSelf = self { - strongSelf.layoutParams = (item, first, last, firstWithHeader, nextIsPinned) + strongSelf.layoutParams = (item, first, last, firstWithHeader, nextIsPinned, params) if let _ = updatedTheme { - strongSelf.separatorNode.backgroundColor = item.theme.chatList.itemSeparatorColor - strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.chatList.itemHighlightedBackgroundColor + strongSelf.separatorNode.backgroundColor = item.presentationData.theme.chatList.itemSeparatorColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.chatList.itemHighlightedBackgroundColor } let revealOffset = strongSelf.revealOffset @@ -566,9 +683,9 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } strongSelf.editableControlNode = editableControlNode strongSelf.addSubnode(editableControlNode) - let editableControlFrame = CGRect(origin: CGPoint(x: revealOffset, y: 0.0), size: editableControlSizeAndApply.0) + let editableControlFrame = CGRect(origin: CGPoint(x: params.leftInset + revealOffset, y: 0.0), size: editableControlSizeAndApply.0) editableControlNode.frame = editableControlFrame - transition.animatePosition(node: editableControlNode, from: CGPoint(x: editableControlFrame.midX - editableControlFrame.size.width, y: editableControlFrame.midY)) + transition.animatePosition(node: editableControlNode, from: CGPoint(x: -editableControlFrame.size.width / 2.0, y: editableControlFrame.midY)) editableControlNode.alpha = 0.0 transition.updateAlpha(node: editableControlNode, alpha: 1.0) } @@ -583,7 +700,24 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { }) } - transition.updateFrame(node: strongSelf.avatarNode, frame: CGRect(origin: CGPoint(x: editingOffset + 10.0 + revealOffset, y: 7.0), size: CGSize(width: 60.0, height: 60.0))) + let avatarFrame = CGRect(origin: CGPoint(x: leftInset - 78.0 + editingOffset + 10.0 + revealOffset, y: 7.0), size: CGSize(width: 60.0, height: 60.0)) + transition.updateFrame(node: strongSelf.avatarNode, frame: avatarFrame) + + if let multipleAvatarsApply = multipleAvatarsApply { + strongSelf.avatarNode.isHidden = true + let multipleAvatarsNode = multipleAvatarsApply(animated && strongSelf.multipleAvatarsNode != nil) + if strongSelf.multipleAvatarsNode != multipleAvatarsNode { + strongSelf.multipleAvatarsNode?.removeFromSupernode() + strongSelf.multipleAvatarsNode = multipleAvatarsNode + strongSelf.addSubnode(multipleAvatarsNode) + multipleAvatarsNode.frame = avatarFrame + } else { + transition.updateFrame(node: multipleAvatarsNode, frame: avatarFrame) + } + } else if let multipleAvatarsNode = strongSelf.multipleAvatarsNode { + multipleAvatarsNode.removeFromSupernode() + strongSelf.avatarNode.isHidden = false + } let _ = dateApply() let _ = textApply() @@ -591,7 +725,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let _ = titleApply() let _ = badgeApply() - let contentRect = rawContentRect.offsetBy(dx: editingOffset + 78.0 + revealOffset, dy: 0.0) + let contentRect = rawContentRect.offsetBy(dx: editingOffset + leftInset + revealOffset, dy: 0.0) strongSelf.dateNode.frame = CGRect(origin: CGPoint(x: contentRect.origin.x + contentRect.size.width - dateLayout.size.width, y: contentRect.origin.y + 2.0), size: dateLayout.size) @@ -599,7 +733,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { strongSelf.statusNode.image = statusImage strongSelf.statusNode.isHidden = false let statusSize = statusImage.size - strongSelf.statusNode.frame = CGRect(origin: CGPoint(x: contentRect.origin.x + contentRect.size.width - dateLayout.size.width - 2.0 - statusSize.width, y: contentRect.origin.y + 5.0), size: statusSize) + strongSelf.statusNode.frame = CGRect(origin: CGPoint(x: contentRect.origin.x + contentRect.size.width - dateLayout.size.width - 2.0 - statusSize.width, y: contentRect.origin.y + 2.0 + floor((dateLayout.size.height - statusSize.height) / 2.0)), size: statusSize) } else { strongSelf.statusNode.image = nil strongSelf.statusNode.isHidden = true @@ -612,7 +746,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { badgeBackgroundWidth = max(badgeLayout.size.width + 10.0, currentBadgeBackgroundImage.size.width) let badgeBackgroundFrame = CGRect(x: contentRect.maxX - badgeBackgroundWidth, y: contentRect.maxY - currentBadgeBackgroundImage.size.height - 2.0, width: badgeBackgroundWidth, height: currentBadgeBackgroundImage.size.height) - let badgeTextFrame = CGRect(origin: CGPoint(x: badgeBackgroundFrame.midX - badgeLayout.size.width / 2.0, y: badgeBackgroundFrame.minY + 1.0), size: badgeLayout.size) + let badgeTextFrame = CGRect(origin: CGPoint(x: badgeBackgroundFrame.midX - badgeLayout.size.width / 2.0, y: badgeBackgroundFrame.minY + 2.0), size: badgeLayout.size) strongSelf.badgeTextNode.frame = badgeTextFrame strongSelf.badgeBackgroundNode.frame = badgeBackgroundFrame @@ -675,16 +809,40 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { strongSelf.mutedIconNode.isHidden = true } - let contentDeltaX = contentRect.origin.x - strongSelf.titleNode.frame.minX - strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: contentRect.origin.x, y: contentRect.origin.y + UIScreenPixel), size: titleLayout.size) + var titleOffset: CGFloat = 0.0 + if let currentSecretIconImage = currentSecretIconImage { + let iconNode: ASImageNode + if let current = strongSelf.secretIconNode { + iconNode = current + } else { + iconNode = ASImageNode() + iconNode.isLayerBacked = true + iconNode.displaysAsynchronously = false + iconNode.displayWithoutProcessing = true + strongSelf.addSubnode(iconNode) + strongSelf.secretIconNode = iconNode + } + iconNode.image = currentSecretIconImage + transition.updateFrame(node: iconNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x, y: contentRect.origin.y + 4.0), size: currentSecretIconImage.size)) + titleOffset += currentSecretIconImage.size.width + 3.0 + } else if let secretIconNode = strongSelf.secretIconNode { + strongSelf.secretIconNode = nil + secretIconNode.removeFromSupernode() + } + + let contentDeltaX = contentRect.origin.x - (strongSelf.titleNode.frame.minX - titleOffset) + strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: contentRect.origin.x + titleOffset, y: contentRect.origin.y + UIScreenPixel), size: titleLayout.size) let authorNodeFrame = CGRect(origin: CGPoint(x: contentRect.origin.x - 1.0, y: contentRect.minY + titleLayout.size.height), size: authorLayout.size) strongSelf.authorNode.frame = authorNodeFrame let textNodeFrame = CGRect(origin: CGPoint(x: contentRect.origin.x - 1.0, y: contentRect.minY + titleLayout.size.height - 1.0 + UIScreenPixel + (authorLayout.size.height.isZero ? 0.0 : (authorLayout.size.height - 3.0))), size: textLayout.size) strongSelf.textNode.frame = textNodeFrame - if let inputActivities = item.inputActivities, !inputActivities.isEmpty { + var animateInputActivitiesFrame = false + if let inputActivities = inputActivities, !inputActivities.isEmpty { if strongSelf.inputActivitiesNode.supernode == nil { strongSelf.addSubnode(strongSelf.inputActivitiesNode) + } else { + animateInputActivitiesFrame = true } if strongSelf.inputActivitiesNode.alpha.isZero { @@ -717,7 +875,12 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } } if let inputActivitiesSize = inputActivitiesSize { - strongSelf.inputActivitiesNode.frame = CGRect(origin: CGPoint(x: authorNodeFrame.minX + 1.0, y: authorNodeFrame.minY + UIScreenPixel), size: inputActivitiesSize) + let inputActivitiesFrame = CGRect(origin: CGPoint(x: authorNodeFrame.minX + 1.0, y: authorNodeFrame.minY + UIScreenPixel), size: inputActivitiesSize) + if animateInputActivitiesFrame { + transition.updateFrame(node: strongSelf.inputActivitiesNode, frame: inputActivitiesFrame) + } else { + strongSelf.inputActivitiesNode.frame = inputActivitiesFrame + } } inputActivitiesApply?() @@ -733,13 +896,13 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } let separatorInset: CGFloat - if !nextIsPinned && item.index.pinningIndex != nil { + if (!nextIsPinned && item.index.pinningIndex != nil) || last { separatorInset = 0.0 } else { - separatorInset = editingOffset + 78.0 + rawContentRect.origin.x + separatorInset = editingOffset + leftInset + rawContentRect.origin.x } - transition.updateFrame(node: strongSelf.separatorNode, frame: CGRect(origin: CGPoint(x: separatorInset, y: itemHeight - separatorHeight), size: CGSize(width: width - separatorInset, height: separatorHeight))) + transition.updateFrame(node: strongSelf.separatorNode, frame: CGRect(origin: CGPoint(x: separatorInset, y: itemHeight - separatorHeight), size: CGSize(width: params.width - separatorInset, height: separatorHeight))) strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize) if item.index.pinningIndex != nil { @@ -750,6 +913,8 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let topNegativeInset: CGFloat = 0.0 strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -separatorHeight - topNegativeInset), size: CGSize(width: layout.contentSize.width, height: layout.contentSize.height + separatorHeight + topNegativeInset)) + strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) + strongSelf.setRevealOptions(peerRevealOptions) strongSelf.setRevealOptionsOpened(item.hasActiveRevealControls, animated: animated) } @@ -758,15 +923,16 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { - self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5) + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } override func animateRemoved(_ currentTimestamp: Double, duration: Double) { - self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.5, removeOnCompletion: false) + self.clipsToBounds = true + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) } override public func header() -> ListViewItemHeader? { - if let (item, _, _, _, _) = self.layoutParams { + if let item = self.layoutParams?.0 { return item.header } else { return nil @@ -776,39 +942,52 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { override func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { super.updateRevealOffset(offset: offset, transition: transition) - if let _ = self.item { + if let _ = self.item, let params = self.layoutParams?.5 { let editingOffset: CGFloat if let editableControlNode = self.editableControlNode { editingOffset = editableControlNode.bounds.size.width var editableControlFrame = editableControlNode.frame - editableControlFrame.origin.x = offset + editableControlFrame.origin.x = params.leftInset + offset transition.updateFrame(node: editableControlNode, frame: editableControlFrame) } else { editingOffset = 0.0 } - let rawContentRect = CGRect(origin: CGPoint(x: 2.0, y: 8.0), size: CGSize(width: self.contentSize.width - 78.0 - 10.0 - 1.0 - editingOffset, height: itemHeight - 12.0 - 9.0)) + let leftInset: CGFloat = params.leftInset + 78.0 - let contentRect = rawContentRect.offsetBy(dx: editingOffset + 78.0 + offset, dy: 0.0) + let rawContentRect = CGRect(origin: CGPoint(x: 2.0, y: 8.0), size: CGSize(width: params.width - leftInset - params.rightInset - 10.0 - 1.0 - editingOffset, height: itemHeight - 12.0 - 9.0)) + + let contentRect = rawContentRect.offsetBy(dx: editingOffset + leftInset + offset, dy: 0.0) var avatarFrame = self.avatarNode.frame - avatarFrame.origin.x = editingOffset + 10.0 + offset + avatarFrame.origin.x = leftInset - 78.0 + editingOffset + 10.0 + offset transition.updateFrame(node: self.avatarNode, frame: avatarFrame) + if let multipleAvatarsNode = self.multipleAvatarsNode { + transition.updateFrame(node: multipleAvatarsNode, frame: avatarFrame) + } + + var titleOffset: CGFloat = 0.0 + if let secretIconNode = self.secretIconNode, let image = secretIconNode.image { + transition.updateFrame(node: secretIconNode, frame: CGRect(origin: CGPoint(x: contentRect.minX, y: secretIconNode.frame.minY), size: image.size)) + titleOffset += image.size.width + 3.0 + } let titleFrame = self.titleNode.frame - transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x, y: titleFrame.origin.y), size: titleFrame.size)) + transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x + titleOffset, y: titleFrame.origin.y), size: titleFrame.size)) let authorFrame = self.authorNode.frame transition.updateFrame(node: self.authorNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x - 1.0, y: authorFrame.origin.y), size: authorFrame.size)) + transition.updateFrame(node: self.inputActivitiesNode, frame: CGRect(origin: CGPoint(x: authorFrame.minX + 1.0, y: self.inputActivitiesNode.frame.minY), size: self.inputActivitiesNode.bounds.size)) + let textFrame = self.textNode.frame transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x - 1.0, y: textFrame.origin.y), size: textFrame.size)) let dateFrame = self.dateNode.frame - transition.updateFrame(node: self.dateNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x + contentRect.size.width - dateFrame.size.width, y: contentRect.origin.y + 2.0), size: dateFrame.size)) + transition.updateFrame(node: self.dateNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x + contentRect.size.width - dateFrame.size.width, y: dateFrame.minY), size: dateFrame.size)) let statusFrame = self.statusNode.frame - transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x + contentRect.size.width - dateFrame.size.width - 2.0 - statusFrame.size.width, y: contentRect.origin.y + 5.0), size: statusFrame.size)) + transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x + contentRect.size.width - dateFrame.size.width - 2.0 - statusFrame.size.width, y: statusFrame.minY), size: statusFrame.size)) var nextTitleIconOrigin: CGFloat = contentRect.origin.x + titleFrame.size.width + 3.0 @@ -828,7 +1007,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { if self.mentionBadgeNode.supernode != nil { let mentionBadgeSize = self.mentionBadgeNode.bounds.size let mentionBadgeOffset: CGFloat - if updatedBadgeBackgroundFrame.size.width.isZero { + if updatedBadgeBackgroundFrame.size.width.isZero || self.badgeBackgroundNode.image == nil { mentionBadgeOffset = contentRect.maxX - mentionBadgeSize.width } else { mentionBadgeOffset = contentRect.maxX - updatedBadgeBackgroundFrame.size.width - 6.0 - mentionBadgeSize.width @@ -840,7 +1019,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } let badgeTextFrame = self.badgeTextNode.frame - transition.updateFrame(node: self.badgeTextNode, frame: CGRect(origin: CGPoint(x: updatedBadgeBackgroundFrame.midX - badgeTextFrame.size.width / 2.0, y: badgeBackgroundFrame.minY + 1.0), size: badgeTextFrame.size)) + transition.updateFrame(node: self.badgeTextNode, frame: CGRect(origin: CGPoint(x: updatedBadgeBackgroundFrame.midX - badgeTextFrame.size.width / 2.0, y: badgeTextFrame.minY), size: badgeTextFrame.size)) } } @@ -861,9 +1040,23 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { if let item = self.item { switch option.key { case RevealOptionKey.pin.rawValue: - item.interaction.setPeerPinned(item.index.messageIndex.id.peerId, true) + let itemId: PinnedItemId + switch item.content { + case .peer: + itemId = .peer(item.index.messageIndex.id.peerId) + case let .groupReference(groupId, _, _, _): + itemId = .group(groupId) + } + item.interaction.setItemPinned(itemId, true) case RevealOptionKey.unpin.rawValue: - item.interaction.setPeerPinned(item.index.messageIndex.id.peerId, false) + let itemId: PinnedItemId + switch item.content { + case .peer: + itemId = .peer(item.index.messageIndex.id.peerId) + case let .groupReference(groupId, _, _, _): + itemId = .group(groupId) + } + item.interaction.setItemPinned(itemId, false) case RevealOptionKey.mute.rawValue: item.interaction.setPeerMuted(item.index.messageIndex.id.peerId, true) close = false @@ -872,6 +1065,10 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { close = false case RevealOptionKey.delete.rawValue: item.interaction.deletePeer(item.index.messageIndex.id.peerId) + case RevealOptionKey.group.rawValue: + item.interaction.updatePeerGrouping(item.index.messageIndex.id.peerId, true) + case RevealOptionKey.ungroup.rawValue: + item.interaction.updatePeerGrouping(item.index.messageIndex.id.peerId, false) default: break } diff --git a/TelegramUI/ChatListItemStrings.swift b/TelegramUI/ChatListItemStrings.swift index a3b3e96073..24e7acf619 100644 --- a/TelegramUI/ChatListItemStrings.swift +++ b/TelegramUI/ChatListItemStrings.swift @@ -89,7 +89,7 @@ public func chatListItemStrings(strings: PresentationStrings, message: Message?, hideAuthor = true switch action.action { case .phoneCall: - if message.effectivelyIncoming { + if message.flags.contains(.Incoming) { messageText = strings.Notification_CallIncoming } else { messageText = strings.Notification_CallOutgoing @@ -115,13 +115,18 @@ public func chatListItemStrings(strings: PresentationStrings, message: Message?, if let secretChat = chatPeer.peers[chatPeer.peerId] as? TelegramSecretChat { switch secretChat.embeddedState { case .active: - messageText = strings.Notification_EncryptedChatAccepted + switch secretChat.role { + case .creator: + messageText = strings.DialogList_EncryptedChatStartedOutgoing(peer?.compactDisplayTitle ?? "").0 + case .participant: + messageText = strings.DialogList_EncryptedChatStartedIncoming(peer?.compactDisplayTitle ?? "").0 + } case .terminated: messageText = strings.DialogList_EncryptionRejected case .handshake: switch secretChat.role { case .creator: - messageText = strings.Notification_EncryptedChatRequested + messageText = strings.DialogList_AwaitingEncryption(peer?.compactDisplayTitle ?? "").0 case .participant: messageText = strings.DialogList_EncryptionProcessing } diff --git a/TelegramUI/ChatListNode.swift b/TelegramUI/ChatListNode.swift index 208309b2d5..d6ab03900e 100644 --- a/TelegramUI/ChatListNode.swift +++ b/TelegramUI/ChatListNode.swift @@ -7,7 +7,7 @@ import Postbox enum ChatListNodeMode { case chatList - case peers + case peers(onlyWriteable: Bool) } struct ChatListNodeListViewTransition { @@ -24,19 +24,23 @@ final class ChatListNodeInteraction { let activateSearch: () -> Void let peerSelected: (Peer) -> Void let messageSelected: (Message) -> Void + let groupSelected: (PeerGroupId) -> Void let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void - let setPeerPinned: (PeerId, Bool) -> Void + let setItemPinned: (PinnedItemId, Bool) -> Void let setPeerMuted: (PeerId, Bool) -> Void let deletePeer: (PeerId) -> Void + let updatePeerGrouping: (PeerId, Bool) -> Void - init(activateSearch: @escaping () -> Void, peerSelected: @escaping (Peer) -> Void, messageSelected: @escaping (Message) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, setPeerPinned: @escaping (PeerId, Bool) -> Void, setPeerMuted: @escaping (PeerId, Bool) -> Void, deletePeer: @escaping (PeerId) -> Void) { + init(activateSearch: @escaping () -> Void, peerSelected: @escaping (Peer) -> Void, messageSelected: @escaping (Message) -> Void, groupSelected: @escaping (PeerGroupId) -> 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) { self.activateSearch = activateSearch self.peerSelected = peerSelected self.messageSelected = messageSelected + self.groupSelected = groupSelected self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions - self.setPeerPinned = setPeerPinned + self.setItemPinned = setItemPinned self.setPeerMuted = setPeerMuted self.deletePeer = deletePeer + self.updatePeerGrouping = updatePeerGrouping } } @@ -49,33 +53,29 @@ final class ChatListNodePeerInputActivities { } struct ChatListNodeState: Equatable { - let theme: PresentationTheme - let strings: PresentationStrings + let presentationData: ChatListPresentationData let editing: Bool let peerIdWithRevealedOptions: PeerId? let peerInputActivities: ChatListNodePeerInputActivities? - func withUpdatedPresentationData(theme: PresentationTheme, strings: PresentationStrings) -> ChatListNodeState { - return ChatListNodeState(theme: theme, strings: strings, editing: self.editing, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, peerInputActivities: self.peerInputActivities) + func withUpdatedPresentationData(_ presentationData: ChatListPresentationData) -> ChatListNodeState { + return ChatListNodeState(presentationData: presentationData, editing: self.editing, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, peerInputActivities: self.peerInputActivities) } func withUpdatedEditing(_ editing: Bool) -> ChatListNodeState { - return ChatListNodeState(theme: self.theme, strings: self.strings, editing: editing, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, peerInputActivities: self.peerInputActivities) + return ChatListNodeState(presentationData: self.presentationData, editing: editing, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, peerInputActivities: self.peerInputActivities) } func withUpdatedPeerIdWithRevealedOptions(_ peerIdWithRevealedOptions: PeerId?) -> ChatListNodeState { - return ChatListNodeState(theme: self.theme, strings: self.strings, editing: self.editing, peerIdWithRevealedOptions: peerIdWithRevealedOptions, peerInputActivities: self.peerInputActivities) + return ChatListNodeState(presentationData: self.presentationData, editing: self.editing, peerIdWithRevealedOptions: peerIdWithRevealedOptions, peerInputActivities: self.peerInputActivities) } func withUpdatedPeerInputActivities(_ peerInputActivities: ChatListNodePeerInputActivities?) -> ChatListNodeState { - return ChatListNodeState(theme: self.theme, strings: self.strings, editing: self.editing, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, peerInputActivities: peerInputActivities) + return ChatListNodeState(presentationData: self.presentationData, editing: self.editing, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, peerInputActivities: peerInputActivities) } static func ==(lhs: ChatListNodeState, rhs: ChatListNodeState) -> Bool { - if lhs.theme !== rhs.theme { - return false - } - if lhs.strings !== rhs.strings { + if lhs.presentationData !== rhs.presentationData { return false } if lhs.editing != rhs.editing { @@ -91,68 +91,86 @@ struct ChatListNodeState: Equatable { } } -private func mappedInsertEntries(account: Account, nodeInteraction: ChatListNodeInteraction, mode: ChatListNodeMode, entries: [ChatListNodeViewTransitionInsertEntry]) -> [ListViewInsertItem] { +private func mappedInsertEntries(account: Account, nodeInteraction: ChatListNodeInteraction, peerGroupId: PeerGroupId?, mode: ChatListNodeMode, entries: [ChatListNodeViewTransitionInsertEntry]) -> [ListViewInsertItem] { return entries.map { entry -> ListViewInsertItem in switch entry.entry { case let .SearchEntry(theme, text): return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListSearchItem(theme: theme, placeholder: text, activate: { nodeInteraction.activateSearch() }), directionHint: entry.directionHint) - case let .PeerEntry(index, theme, strings, message, combinedReadState, notificationSettings, embeddedState, peer, summaryInfo, editing, hasActiveRevealControls, inputActivities): + case let .PeerEntry(index, presentationData, message, combinedReadState, notificationSettings, embeddedState, peer, summaryInfo, editing, hasActiveRevealControls, inputActivities): switch mode { case .chatList: - return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem(theme: theme, strings: strings, account: account, index: index, message: message, peer: peer, combinedReadState: combinedReadState, notificationSettings: notificationSettings, summaryInfo: summaryInfo, embeddedState: embeddedState, editing: editing, hasActiveRevealControls: hasActiveRevealControls, inputActivities: inputActivities, header: nil, interaction: nodeInteraction), directionHint: entry.directionHint) - case .peers: - var peer: Peer? + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem(presentationData: presentationData, account: account, peerGroupId: peerGroupId, index: index, content: .peer(message: message, peer: peer, combinedReadState: combinedReadState, notificationSettings: notificationSettings, summaryInfo: summaryInfo, embeddedState: embeddedState, inputActivities: inputActivities), editing: editing, hasActiveRevealControls: hasActiveRevealControls, header: nil, interaction: nodeInteraction), directionHint: entry.directionHint) + case let .peers(onlyWriteable): + let itemPeer = peer.chatMainPeer var chatPeer: Peer? - if let message = message { - peer = messageMainPeer(message) - chatPeer = message.peers[message.id.peerId] + if let peer = peer.peers[peer.peerId] { + chatPeer = peer } - return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem(theme: theme, strings: strings, account: account, peer: peer, chatPeer: chatPeer, status: .none, selection: .none, hasActiveRevealControls: false, index: nil, header: nil, action: { _ in + var enabled = true + if onlyWriteable { + if let peer = peer.peers[peer.peerId] { + enabled = canSendMessagesToPeer(peer) + } else { + enabled = false + } + } + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem(theme: presentationData.theme, strings: presentationData.strings, account: account, peer: itemPeer, chatPeer: chatPeer, status: .none, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: nil, action: { _ in if let chatPeer = chatPeer { nodeInteraction.peerSelected(chatPeer) } }), directionHint: entry.directionHint) } - case let .HoleEntry(theme): - return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListHoleItem(), directionHint: entry.directionHint) + case let .HoleEntry(_, theme): + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListHoleItem(theme: theme), directionHint: entry.directionHint) + case let .GroupReferenceEntry(index, presentationData, groupId, message, topPeers, counters, editing): + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem(presentationData: presentationData, account: account, peerGroupId: peerGroupId, index: index, content: .groupReference(groupId: groupId, message: message, topPeers: topPeers, counters: counters), editing: editing, hasActiveRevealControls: false, header: nil, interaction: nodeInteraction), directionHint: entry.directionHint) } } } -private func mappedUpdateEntries(account: Account, nodeInteraction: ChatListNodeInteraction, mode: ChatListNodeMode, entries: [ChatListNodeViewTransitionUpdateEntry]) -> [ListViewUpdateItem] { +private func mappedUpdateEntries(account: Account, nodeInteraction: ChatListNodeInteraction, peerGroupId: PeerGroupId?, mode: ChatListNodeMode, entries: [ChatListNodeViewTransitionUpdateEntry]) -> [ListViewUpdateItem] { return entries.map { entry -> ListViewUpdateItem in switch entry.entry { case let .SearchEntry(theme, text): return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListSearchItem(theme: theme, placeholder: text, activate: { nodeInteraction.activateSearch() }), directionHint: entry.directionHint) - case let .PeerEntry(index, theme, strings, message, combinedReadState, notificationSettings, embeddedState, peer, summaryInfo, editing, hasActiveRevealControls, inputActivities): + case let .PeerEntry(index, presentationData, message, combinedReadState, notificationSettings, embeddedState, peer, summaryInfo, editing, hasActiveRevealControls, inputActivities): switch mode { case .chatList: - return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem(theme: theme, strings: strings, account: account, index: index, message: message, peer: peer, combinedReadState: combinedReadState, notificationSettings: notificationSettings, summaryInfo: summaryInfo, embeddedState: embeddedState, editing: editing, hasActiveRevealControls: hasActiveRevealControls, inputActivities: inputActivities, header: nil, interaction: nodeInteraction), directionHint: entry.directionHint) - case .peers: - var peer: Peer? + return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem(presentationData: presentationData, account: account, peerGroupId: peerGroupId, index: index, content: .peer(message: message, peer: peer, combinedReadState: combinedReadState, notificationSettings: notificationSettings, summaryInfo: summaryInfo, embeddedState: embeddedState, inputActivities: inputActivities), editing: editing, hasActiveRevealControls: hasActiveRevealControls, header: nil, interaction: nodeInteraction), directionHint: entry.directionHint) + case let .peers(onlyWriteable): + let itemPeer = peer.chatMainPeer var chatPeer: Peer? - if let message = message { - peer = messageMainPeer(message) - chatPeer = message.peers[message.id.peerId] + if let peer = peer.peers[peer.peerId] { + chatPeer = peer } - return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem(theme: theme, strings: strings, account: account, peer: peer, chatPeer: chatPeer, status: .none, selection: .none, hasActiveRevealControls: false, index: nil, header: nil, action: { _ in + var enabled = true + if onlyWriteable { + if let peer = peer.peers[peer.peerId] { + enabled = canSendMessagesToPeer(peer) + } else { + enabled = false + } + } + return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem(theme: presentationData.theme, strings: presentationData.strings, account: account, peer: itemPeer, chatPeer: chatPeer, status: .none, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: nil, action: { _ in if let chatPeer = chatPeer { nodeInteraction.peerSelected(chatPeer) } }), directionHint: entry.directionHint) } - case let .HoleEntry(theme): - return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListHoleItem(), directionHint: entry.directionHint) + case let .HoleEntry(_, theme): + return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListHoleItem(theme: theme), directionHint: entry.directionHint) + case let .GroupReferenceEntry(index, presentationData, groupId, message, topPeers, counters, editing): + return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem(presentationData: presentationData, account: account, peerGroupId: peerGroupId, index: index, content: .groupReference(groupId: groupId, message: message, topPeers: topPeers, counters: counters), editing: editing, hasActiveRevealControls: false, header: nil, interaction: nodeInteraction), directionHint: entry.directionHint) } } } -private func mappedChatListNodeViewListTransition(account: Account, nodeInteraction: ChatListNodeInteraction, mode: ChatListNodeMode, transition: ChatListNodeViewTransition) -> ChatListNodeListViewTransition { - return ChatListNodeListViewTransition(chatListView: transition.chatListView, deleteItems: transition.deleteItems, insertItems: mappedInsertEntries(account: account, nodeInteraction: nodeInteraction, mode: mode, entries: transition.insertEntries), updateItems: mappedUpdateEntries(account: account, nodeInteraction: nodeInteraction, mode: mode, entries: transition.updateEntries), options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange) +private func mappedChatListNodeViewListTransition(account: Account, nodeInteraction: ChatListNodeInteraction, peerGroupId: PeerGroupId?, mode: ChatListNodeMode, transition: ChatListNodeViewTransition) -> ChatListNodeListViewTransition { + return ChatListNodeListViewTransition(chatListView: transition.chatListView, deleteItems: transition.deleteItems, insertItems: mappedInsertEntries(account: account, nodeInteraction: nodeInteraction, peerGroupId: peerGroupId, mode: mode, entries: transition.insertEntries), updateItems: mappedUpdateEntries(account: account, nodeInteraction: nodeInteraction, peerGroupId: peerGroupId, mode: mode, entries: transition.updateEntries), options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange) } private final class ChatListOpaqueTransactionState { @@ -164,6 +182,10 @@ private final class ChatListOpaqueTransactionState { } final class ChatListNode: ListView { + private let controlsHistoryPreload: Bool + private let account: Account + private let mode: ChatListNodeMode + private let _ready = ValuePromise() private var didSetReady = false var ready: Signal { @@ -171,8 +193,10 @@ final class ChatListNode: ListView { } var peerSelected: ((PeerId) -> Void)? + var groupSelected: ((PeerGroupId) -> Void)? var activateSearch: (() -> Void)? var deletePeerChat: ((PeerId) -> Void)? + var updatePeerGrouping: ((PeerId, Bool) -> Void)? var presentAlert: ((String) -> Void)? private var theme: PresentationTheme @@ -191,8 +215,12 @@ final class ChatListNode: ListView { private let chatListDisposable = MetaDisposable() private var activityStatusesDisposable: Disposable? - init(account: Account, mode: ChatListNodeMode, theme: PresentationTheme, strings: PresentationStrings) { - self.currentState = ChatListNodeState(theme: theme, strings: strings, editing: false, peerIdWithRevealedOptions: nil, peerInputActivities: nil) + init(account: Account, groupId: PeerGroupId?, controlsHistoryPreload: Bool, mode: ChatListNodeMode, theme: PresentationTheme, strings: PresentationStrings, timeFormat: PresentationTimeFormat) { + self.account = account + self.controlsHistoryPreload = controlsHistoryPreload + self.mode = mode + + self.currentState = ChatListNodeState(presentationData: ChatListPresentationData(theme: theme, strings: strings, timeFormat: timeFormat), editing: false, peerIdWithRevealedOptions: nil, peerInputActivities: nil) self.statePromise = ValuePromise(self.currentState, ignoreRepeated: true) self.theme = theme @@ -213,6 +241,10 @@ final class ChatListNode: ListView { if let strongSelf = self, let peerSelected = strongSelf.peerSelected { peerSelected(message.id.peerId) } + }, groupSelected: { [weak self] groupId in + if let strongSelf = self, let groupSelected = strongSelf.groupSelected { + groupSelected(groupId) + } }, setPeerIdWithRevealedOptions: { [weak self] peerId, fromPeerId in if let strongSelf = self { strongSelf.updateState { state in @@ -223,14 +255,14 @@ final class ChatListNode: ListView { } } } - }, setPeerPinned: { peerId, _ in - let _ = (togglePeerChatPinned(postbox: account.postbox, peerId: peerId) |> deliverOnMainQueue).start(next: { [weak self] result in + }, setItemPinned: { [weak self] itemId, _ in + let _ = (toggleItemPinned(postbox: account.postbox, itemId: itemId) |> deliverOnMainQueue).start(next: { result in if let strongSelf = self { switch result { case .done: break case .limitExceeded: - strongSelf.presentAlert?(strongSelf.currentState.strings.DialogList_PinLimitError("5").0) + strongSelf.presentAlert?(strongSelf.currentState.presentationData.strings.DialogList_PinLimitError("5").0) } } }) @@ -242,6 +274,8 @@ final class ChatListNode: ListView { }) }, deletePeer: { [weak self] peerId in self?.deletePeerChat?(peerId) + }, updatePeerGrouping: { [weak self] peerId, group in + self?.updatePeerGrouping?(peerId, group) }) let viewProcessingQueue = self.viewProcessingQueue @@ -249,13 +283,20 @@ final class ChatListNode: ListView { let chastListViewUpdate = self.chatListLocation.get() |> distinctUntilChanged |> mapToSignal { location in - return chatListViewForLocation(location, account: account) + return chatListViewForLocation(groupId: groupId, location: location, account: account) } let previousView = Atomic(value: nil) - let chatListNodeViewTransition = combineLatest(chastListViewUpdate, self.statePromise.get()) |> mapToQueue { (update, state) -> Signal in - let processedView = ChatListNodeView(originalView: update.view, filteredEntries: chatListNodeEntriesForView(update.view, state: state)) + let savedMessagesPeer: Signal + if case .peers(onlyWriteable: true) = mode { + savedMessagesPeer = account.postbox.loadedPeerWithId(account.peerId) |> map(Optional.init) + } else { + savedMessagesPeer = .single(nil) + } + + let chatListNodeViewTransition = combineLatest(savedMessagesPeer, chastListViewUpdate, self.statePromise.get()) |> mapToQueue { (savedMessagesPeer, update, state) -> Signal in + let processedView = ChatListNodeView(originalView: update.view, filteredEntries: chatListNodeEntriesForView(update.view, state: state, savedMessagesPeer: savedMessagesPeer, mode: mode)) let previous = previousView.swap(processedView) let reason: ChatListNodeViewTransitionReason @@ -272,6 +313,8 @@ final class ChatListNode: ListView { previousWasEmptyOrSingleHole = true } + var updatedScrollPosition = update.scrollPosition + if previousWasEmptyOrSingleHole { reason = .initial if previous == nil { @@ -280,6 +323,7 @@ final class ChatListNode: ListView { } else { if previous?.originalView === update.view { reason = .interactiveChanges + updatedScrollPosition = nil } else { switch update.type { case .InitialUnread: @@ -295,8 +339,8 @@ final class ChatListNode: ListView { } } - return preparedChatListNodeViewTransition(from: previous, to: processedView, reason: reason, account: account, scrollPosition: update.scrollPosition) - |> map({ mappedChatListNodeViewListTransition(account: account, nodeInteraction: nodeInteraction, mode: mode, transition: $0) }) + return preparedChatListNodeViewTransition(from: previous, to: processedView, reason: reason, account: account, scrollPosition: updatedScrollPosition) + |> map({ mappedChatListNodeViewListTransition(account: account, nodeInteraction: nodeInteraction, peerGroupId: groupId, mode: mode, transition: $0) }) |> runOn(prepareOnMainQueue ? Queue.mainQueue() : viewProcessingQueue) } @@ -320,6 +364,8 @@ final class ChatListNode: ListView { strongSelf.currentLocation = location strongSelf.chatListLocation.set(location) } + + strongSelf.enqueueHistoryPreloadUpdate() } } @@ -438,17 +484,17 @@ final class ChatListNode: ListView { self.activityStatusesDisposable?.dispose() } - func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { - if theme !== self.currentState.theme || strings !== self.currentState.strings { + func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings, timeFormat: PresentationTimeFormat) { + if theme !== self.currentState.presentationData.theme || strings !== self.currentState.presentationData.strings || timeFormat != self.currentState.presentationData.timeFormat { self.theme = theme self.backgroundColor = theme.chatList.backgroundColor if self.keepTopItemOverscrollBackground != nil { - self.keepTopItemOverscrollBackground = theme.chatList.pinnedItemBackgroundColor + self.keepTopItemOverscrollBackground = ListViewKeepTopItemOverscrollBackground(color: theme.chatList.pinnedItemBackgroundColor, direction: true) } self.updateState { - return $0.withUpdatedPresentationData(theme: theme, strings: strings) + return $0.withUpdatedPresentationData(ChatListPresentationData(theme: theme, strings: strings, timeFormat: timeFormat)) } } } @@ -497,18 +543,20 @@ final class ChatListNode: ListView { strongSelf.chatListView = transition.chatListView var pinnedOverscroll = false - let entryCount = transition.chatListView.filteredEntries.count - if entryCount >= 2 { - if case .SearchEntry = transition.chatListView.filteredEntries[entryCount - 1] { - if transition.chatListView.filteredEntries[entryCount - 2].index.pinningIndex != nil { - pinnedOverscroll = true + if case .chatList = strongSelf.mode { + let entryCount = transition.chatListView.filteredEntries.count + if entryCount >= 2 { + if case .SearchEntry = transition.chatListView.filteredEntries[entryCount - 1] { + if transition.chatListView.filteredEntries[entryCount - 2].index.pinningIndex != nil { + pinnedOverscroll = true + } } } } if pinnedOverscroll != (strongSelf.keepTopItemOverscrollBackground != nil) { if pinnedOverscroll { - strongSelf.keepTopItemOverscrollBackground = strongSelf.theme.chatList.pinnedItemBackgroundColor + strongSelf.keepTopItemOverscrollBackground = ListViewKeepTopItemOverscrollBackground(color: strongSelf.theme.chatList.pinnedItemBackgroundColor, direction: true) } else { strongSelf.keepTopItemOverscrollBackground = nil } @@ -551,4 +599,8 @@ final class ChatListNode: ListView { self.chatListLocation.set(location) } } + + private func enqueueHistoryPreloadUpdate() { + + } } diff --git a/TelegramUI/ChatListNodeEntries.swift b/TelegramUI/ChatListNodeEntries.swift index e40b45040c..cef9e7f40f 100644 --- a/TelegramUI/ChatListNodeEntries.swift +++ b/TelegramUI/ChatListNodeEntries.swift @@ -2,77 +2,71 @@ import Foundation import Postbox import TelegramCore -enum ChatListNodeEntryId: Hashable, CustomStringConvertible { +enum ChatListNodeEntryId: Hashable { case Search case Hole(Int64) case PeerId(Int64) + case GroupId(PeerGroupId) var hashValue: Int { - switch self { - case .Search: - return 0 - case let .Hole(peerId): - return peerId.hashValue - case let .PeerId(peerId): - return peerId.hashValue - } - } - - var description: String { switch self { case .Search: - return "search" - case let .Hole(value): - return "hole(\(value))" - case let .PeerId(value): - return "peerId(\(value))" + return 0 + case let .Hole(peerId): + return peerId.hashValue + case let .PeerId(peerId): + return peerId.hashValue + case let .GroupId(groupId): + return groupId.hashValue } } - static func <(lhs: ChatListNodeEntryId, rhs: ChatListNodeEntryId) -> Bool { - return lhs.hashValue < rhs.hashValue - } - static func ==(lhs: ChatListNodeEntryId, rhs: ChatListNodeEntryId) -> Bool { switch lhs { - case .Search: - switch rhs { case .Search: - return true - default: - return false - } - case let .Hole(lhsId): - switch rhs { - case .Hole(lhsId): - return true - default: - return false - } - case let .PeerId(lhsId): - switch rhs { - case let .PeerId(rhsId): - return lhsId == rhsId - default: - return false - } + if case .Search = rhs { + return true + } else { + return false + } + case let .Hole(id): + if case .Hole(id) = rhs { + return true + } else { + return false + } + case let .PeerId(id): + if case .PeerId(id) = rhs { + return true + } else { + return false + } + case let .GroupId(groupId): + if case .GroupId(groupId) = rhs { + return true + } else { + return false + } } } } enum ChatListNodeEntry: Comparable, Identifiable { case SearchEntry(theme: PresentationTheme, text: String) - case PeerEntry(index: ChatListIndex, theme: PresentationTheme, strings: PresentationStrings, message: Message?, readState: CombinedPeerReadState?, notificationSettings: PeerNotificationSettings?, embeddedInterfaceState: PeerChatListEmbeddedInterfaceState?, peer: RenderedPeer, summaryInfo: ChatListMessageTagSummaryInfo, editing: Bool, hasActiveRevealControls: Bool, inputActivities: [(Peer, PeerInputActivity)]?) + case PeerEntry(index: ChatListIndex, presentationData: ChatListPresentationData, message: Message?, readState: CombinedPeerReadState?, notificationSettings: PeerNotificationSettings?, embeddedInterfaceState: PeerChatListEmbeddedInterfaceState?, peer: RenderedPeer, summaryInfo: ChatListMessageTagSummaryInfo, editing: Bool, hasActiveRevealControls: Bool, inputActivities: [(Peer, PeerInputActivity)]?) case HoleEntry(ChatListHole, theme: PresentationTheme) + case GroupReferenceEntry(index: ChatListIndex, presentationData: ChatListPresentationData, groupId: PeerGroupId, message: Message?, topPeers: [Peer], counters: GroupReferenceUnreadCounters, editing: Bool) var index: ChatListIndex { switch self { case .SearchEntry: return ChatListIndex.absoluteUpperBound - case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _, _): + case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _): return index case let .HoleEntry(hole, _): return ChatListIndex(pinningIndex: nil, messageIndex: hole.index) + case let .GroupReferenceEntry(index, _, _, _, _, _, _): + return index } } @@ -80,10 +74,12 @@ enum ChatListNodeEntry: Comparable, Identifiable { switch self { case .SearchEntry: return .Search - case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _, _): + case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _): return .PeerId(index.messageIndex.id.peerId.toInt64()) case let .HoleEntry(hole, _): return .Hole(Int64(hole.index.id.id)) + case let .GroupReferenceEntry(_, _, groupId, _, _, _, _): + return .GroupId(groupId) } } @@ -99,16 +95,13 @@ enum ChatListNodeEntry: Comparable, Identifiable { } else { return false } - case let .PeerEntry(lhsIndex, lhsTheme, lhsStrings, lhsMessage, lhsUnreadCount, lhsNotificationSettings, lhsEmbeddedState, lhsPeer, lhsSummaryInfo, lhsEditing, lhsHasRevealControls, lhsInputActivities): + case let .PeerEntry(lhsIndex, lhsPresentationData, lhsMessage, lhsUnreadCount, lhsNotificationSettings, lhsEmbeddedState, lhsPeer, lhsSummaryInfo, lhsEditing, lhsHasRevealControls, lhsInputActivities): switch rhs { - case let .PeerEntry(rhsIndex, rhsTheme, rhsStrings, rhsMessage, rhsUnreadCount, rhsNotificationSettings, rhsEmbeddedState, rhsPeer, rhsSummaryInfo, rhsEditing, rhsHasRevealControls, rhsInputActivities): + case let .PeerEntry(rhsIndex, rhsPresentationData, rhsMessage, rhsUnreadCount, rhsNotificationSettings, rhsEmbeddedState, rhsPeer, rhsSummaryInfo, rhsEditing, rhsHasRevealControls, rhsInputActivities): if lhsIndex != rhsIndex { return false } - if lhsTheme !== rhsTheme { - return false - } - if lhsStrings !== rhsStrings { + if lhsPresentationData !== rhsPresentationData { return false } if lhsMessage?.stableVersion != rhsMessage?.stableVersion { @@ -170,22 +163,71 @@ enum ChatListNodeEntry: Comparable, Identifiable { default: return false } + case let .GroupReferenceEntry(lhsIndex, lhsPresentationData, lhsGroupId, lhsMessage, lhsTopPeers, lhsCounters, lhsEditing): + if case let .GroupReferenceEntry(rhsIndex, rhsPresentationData, rhsGroupId, rhsMessage, rhsTopPeers, rhsCounters, rhsEditing) = rhs { + if lhsIndex != rhsIndex { + return false + } + if lhsPresentationData !== rhsPresentationData { + return false + } + if lhsGroupId != rhsGroupId { + return false + } + if lhsMessage?.stableVersion != rhsMessage?.stableVersion { + return false + } + if lhsMessage?.id != rhsMessage?.id || lhsMessage?.flags != rhsMessage?.flags { + return false + } + if lhsTopPeers.count != rhsTopPeers.count { + return false + } else { + for i in 0 ..< lhsTopPeers.count { + if !arePeersEqual(lhsTopPeers[i], rhsTopPeers[i]) { + return false + } + } + } + if lhsCounters != rhsCounters { + return false + } + if lhsEditing != rhsEditing { + return false + } + return true + } else { + return false + } } } } -func chatListNodeEntriesForView(_ view: ChatListView, state: ChatListNodeState) -> [ChatListNodeEntry] { +func chatListNodeEntriesForView(_ view: ChatListView, state: ChatListNodeState, savedMessagesPeer: Peer?, mode: ChatListNodeMode) -> [ChatListNodeEntry] { var result: [ChatListNodeEntry] = [] - for entry in view.entries { + loop: for entry in view.entries { switch entry { case let .MessageEntry(index, message, combinedReadState, notificationSettings, embeddedState, peer, summaryInfo): - result.append(.PeerEntry(index: index, theme: state.theme, strings: state.strings, message: message, readState: combinedReadState, notificationSettings: notificationSettings, embeddedInterfaceState: embeddedState, peer: peer, summaryInfo: summaryInfo, editing: state.editing, hasActiveRevealControls: index.messageIndex.id.peerId == state.peerIdWithRevealedOptions, inputActivities: state.peerInputActivities?.activities[index.messageIndex.id.peerId])) + if let savedMessagesPeer = savedMessagesPeer, savedMessagesPeer.id == index.messageIndex.id.peerId { + continue loop + } + result.append(.PeerEntry(index: index, presentationData: state.presentationData, message: message, readState: combinedReadState, notificationSettings: notificationSettings, embeddedInterfaceState: embeddedState, peer: peer, summaryInfo: summaryInfo, editing: state.editing, hasActiveRevealControls: index.messageIndex.id.peerId == state.peerIdWithRevealedOptions, inputActivities: state.peerInputActivities?.activities[index.messageIndex.id.peerId])) case let .HoleEntry(hole): - result.append(.HoleEntry(hole, theme: state.theme)) + result.append(.HoleEntry(hole, theme: state.presentationData.theme)) + case let .GroupReferenceEntry(groupId, index, message, topPeers, counters): + if case .chatList = mode { + result.append(.GroupReferenceEntry(index: index, presentationData: state.presentationData, groupId: groupId, message: message, topPeers: topPeers, counters: counters, editing: state.editing)) + } } } if view.laterIndex == nil { - result.append(.SearchEntry(theme: state.theme, text: state.strings.DialogList_SearchLabel)) + if let savedMessagesPeer = savedMessagesPeer { + result.append(.PeerEntry(index: ChatListIndex.absoluteUpperBound.predecessor, presentationData: state.presentationData, message: nil, readState: nil, notificationSettings: nil, embeddedInterfaceState: nil, peer: RenderedPeer(peerId: savedMessagesPeer.id, peers: SimpleDictionary([savedMessagesPeer.id: savedMessagesPeer])), summaryInfo: ChatListMessageTagSummaryInfo(), editing: state.editing, hasActiveRevealControls: false, inputActivities: nil)) + } + result.append(.SearchEntry(theme: state.presentationData.theme, text: state.presentationData.strings.DialogList_SearchLabel)) + } + if result.count == 2, case .SearchEntry = result[1], case .HoleEntry = result[0] { + return [] } return result } diff --git a/TelegramUI/ChatListNodeLocation.swift b/TelegramUI/ChatListNodeLocation.swift index dc2a8564b0..069440437b 100644 --- a/TelegramUI/ChatListNodeLocation.swift +++ b/TelegramUI/ChatListNodeLocation.swift @@ -30,17 +30,17 @@ struct ChatListNodeViewUpdate { let scrollPosition: ChatListNodeViewScrollPosition? } -func chatListViewForLocation(_ location: ChatListNodeLocation, account: Account) -> Signal { +func chatListViewForLocation(groupId: PeerGroupId?, location: ChatListNodeLocation, account: Account) -> Signal { switch location { case let .initial(count): let signal: Signal<(ChatListView, ViewUpdateType), NoError> - signal = account.viewTracker.tailChatListView(count: count) + signal = account.viewTracker.tailChatListView(groupId: groupId, count: count) return signal |> map { view, updateType -> ChatListNodeViewUpdate in return ChatListNodeViewUpdate(view: view, type: updateType, scrollPosition: nil) } case let .navigation(index): var first = true - return account.viewTracker.aroundChatListView(index: index, count: 80) |> map { view, updateType -> ChatListNodeViewUpdate in + return account.viewTracker.aroundChatListView(groupId: groupId, index: index, count: 80) |> map { view, updateType -> ChatListNodeViewUpdate in let genericType: ViewUpdateType if first { first = false @@ -54,7 +54,7 @@ func chatListViewForLocation(_ location: ChatListNodeLocation, account: Account) let directionHint: ListViewScrollToItemDirectionHint = sourceIndex > index ? .Down : .Up let chatScrollPosition: ChatListNodeViewScrollPosition = .index(index: index, position: scrollPosition, directionHint: directionHint, animated: animated) var first = true - return account.viewTracker.aroundChatListView(index: index, count: 80) |> map { view, updateType -> ChatListNodeViewUpdate in + return account.viewTracker.aroundChatListView(groupId: groupId, index: index, count: 80) |> map { view, updateType -> ChatListNodeViewUpdate in let genericType: ViewUpdateType let scrollPosition: ChatListNodeViewScrollPosition? = first ? chatScrollPosition : nil if first { diff --git a/TelegramUI/ChatListPresentationData.swift b/TelegramUI/ChatListPresentationData.swift new file mode 100644 index 0000000000..8bcab322f0 --- /dev/null +++ b/TelegramUI/ChatListPresentationData.swift @@ -0,0 +1,13 @@ +import Foundation + +final class ChatListPresentationData { + let theme: PresentationTheme + let strings: PresentationStrings + let timeFormat: PresentationTimeFormat + + init(theme: PresentationTheme, strings: PresentationStrings, timeFormat: PresentationTimeFormat) { + self.theme = theme + self.strings = strings + self.timeFormat = timeFormat + } +} diff --git a/TelegramUI/ChatListRecentPeersListItem.swift b/TelegramUI/ChatListRecentPeersListItem.swift index 13c0128b4a..e4c07fedb2 100644 --- a/TelegramUI/ChatListRecentPeersListItem.swift +++ b/TelegramUI/ChatListRecentPeersListItem.swift @@ -12,23 +12,25 @@ class ChatListRecentPeersListItem: ListViewItem { let account: Account let peers: [Peer] let peerSelected: (Peer) -> Void + let peerLongTapped: (Peer) -> Void let header: ListViewItemHeader? - init(theme: PresentationTheme, strings: PresentationStrings, account: Account, peers: [Peer], peerSelected: @escaping (Peer) -> Void) { + init(theme: PresentationTheme, strings: PresentationStrings, account: Account, peers: [Peer], peerSelected: @escaping (Peer) -> Void, peerLongTapped: @escaping (Peer) -> Void) { self.theme = theme self.strings = strings self.account = account self.peers = peers self.peerSelected = peerSelected + self.peerLongTapped = peerLongTapped self.header = nil } - func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { async { let node = ChatListRecentPeersListItemNode() let makeLayout = node.asyncLayout() - let (nodeLayout, nodeApply) = makeLayout(self, width, nextItem != nil) + let (nodeLayout, nodeApply) = makeLayout(self, params, nextItem != nil) node.contentSize = nodeLayout.contentSize node.insets = nodeLayout.insets @@ -36,12 +38,12 @@ class ChatListRecentPeersListItem: ListViewItem { } } - func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { if let node = node as? ChatListRecentPeersListItemNode { Queue.mainQueue().async { let layout = node.asyncLayout() async { - let (nodeLayout, apply) = layout(self, width, nextItem != nil) + let (nodeLayout, apply) = layout(self, params, nextItem != nil) Queue.mainQueue().async { completion(nodeLayout, { apply().1() @@ -75,21 +77,21 @@ class ChatListRecentPeersListItemNode: ListViewItemNode { self.addSubnode(self.separatorNode) } - override func layoutForWidth(_ width: CGFloat, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { + override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { if let item = self.item { let makeLayout = self.asyncLayout() - let (nodeLayout, nodeApply) = makeLayout(item, width, nextItem == nil) + let (nodeLayout, nodeApply) = makeLayout(item, params, nextItem == nil) self.contentSize = nodeLayout.contentSize self.insets = nodeLayout.insets let _ = nodeApply() } } - func asyncLayout() -> (_ item: ChatListRecentPeersListItem, _ width: CGFloat, _ last: Bool) -> (ListViewItemNodeLayout, () -> (Signal?, () -> Void)) { + func asyncLayout() -> (_ item: ChatListRecentPeersListItem, _ params: ListViewItemLayoutParams, _ last: Bool) -> (ListViewItemNodeLayout, () -> (Signal?, () -> Void)) { let currentItem = self.item - return { [weak self] item, width, last in - let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: width, height: 130.0), insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)) + return { [weak self] item, params, last in + let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 130.0), insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)) return (nodeLayout, { [weak self] in var updatedTheme: PresentationTheme? @@ -102,8 +104,8 @@ class ChatListRecentPeersListItemNode: ListViewItemNode { strongSelf.item = item if let _ = updatedTheme { - strongSelf.separatorNode.backgroundColor = item.theme.list.itemSeparatorColor - strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBackgroundColor + strongSelf.separatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor + strongSelf.backgroundNode.backgroundColor = item.theme.list.plainBackgroundColor } let peersNode: ChatListSearchRecentPeersNode @@ -113,6 +115,8 @@ class ChatListRecentPeersListItemNode: ListViewItemNode { } else { peersNode = ChatListSearchRecentPeersNode(account: item.account, theme: item.theme, mode: .list, strings: item.strings, peerSelected: { peer in self?.item?.peerSelected(peer) + }, peerLongTapped: { peer in + self?.item?.peerLongTapped(peer) }, isPeerSelected: { _ in return false }) @@ -123,6 +127,7 @@ class ChatListRecentPeersListItemNode: ListViewItemNode { let separatorHeight = UIScreenPixel peersNode.frame = CGRect(origin: CGPoint(), size: nodeLayout.contentSize) + peersNode.updateLayout(size: nodeLayout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: nodeLayout.contentSize.width, height: nodeLayout.contentSize.height)) strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: nodeLayout.contentSize.height - separatorHeight), size: CGSize(width: nodeLayout.size.width, height: separatorHeight)) diff --git a/TelegramUI/ChatListSearchContainerNode.swift b/TelegramUI/ChatListSearchContainerNode.swift index bf68976620..9b8849a79a 100644 --- a/TelegramUI/ChatListSearchContainerNode.swift +++ b/TelegramUI/ChatListSearchContainerNode.swift @@ -94,11 +94,13 @@ private enum ChatListRecentEntry: Comparable, Identifiable { } } - func item(account: Account, peerSelected: @escaping (Peer) -> Void, clearRecentlySearchedPeers: @escaping () -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, deletePeer: @escaping (PeerId) -> Void) -> ListViewItem { + func item(account: Account, onlyWriteable: Bool, peerSelected: @escaping (Peer) -> Void, peerLongTapped: @escaping (Peer) -> Void, clearRecentlySearchedPeers: @escaping () -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, deletePeer: @escaping (PeerId) -> Void) -> ListViewItem { switch self { case let .topPeers(peers, theme, strings): return ChatListRecentPeersListItem(theme: theme, strings: strings, account: account, peers: peers, peerSelected: { peer in peerSelected(peer) + }, peerLongTapped: { peer in + peerLongTapped(peer) }) case let .peer(_, peer, associatedPeer, theme, strings, hasRevealControls): let primaryPeer: Peer @@ -108,9 +110,19 @@ private enum ChatListRecentEntry: Comparable, Identifiable { chatPeer = peer } else { primaryPeer = peer - chatPeer = associatedPeer + chatPeer = peer } - return ContactsPeerItem(theme: theme, strings: strings, account: account, peer: primaryPeer, chatPeer: chatPeer, status: .none, selection: .none, hasActiveRevealControls: hasRevealControls, index: nil, header: ChatListSearchItemHeader(type: .recentPeers, theme: theme, strings: strings, actionTitle: strings.WebSearch_RecentSectionClear.uppercased(), action: { + + var enabled = true + if onlyWriteable { + if let peer = chatPeer { + enabled = canSendMessagesToPeer(peer) + } else { + enabled = false + } + } + + return ContactsPeerItem(theme: theme, strings: strings, account: account, peer: primaryPeer, chatPeer: chatPeer, status: .none, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: true, editing: false, revealed: hasRevealControls), index: nil, header: ChatListSearchItemHeader(type: .recentPeers, theme: theme, strings: strings, actionTitle: strings.WebSearch_RecentSectionClear.uppercased(), action: { clearRecentlySearchedPeers() }), action: { _ in peerSelected(peer) @@ -162,16 +174,16 @@ enum ChatListSearchEntryStableId: Hashable { enum ChatListSearchEntry: Comparable, Identifiable { case localPeer(Peer, Peer?, Int, PresentationTheme, PresentationStrings) - case globalPeer(Peer, Int, PresentationTheme, PresentationStrings) - case message(Message, PresentationTheme, PresentationStrings) + case globalPeer(FoundPeer, Int, PresentationTheme, PresentationStrings) + case message(Message, ChatListPresentationData) var stableId: ChatListSearchEntryStableId { switch self { case let .localPeer(peer, _, _, _, _): return .localPeerId(peer.id) case let .globalPeer(peer, _, _, _): - return .globalPeerId(peer.id) - case let .message(message, _, _): + return .globalPeerId(peer.peer.id) + case let .message(message, _): return .messageId(message.id) } } @@ -185,23 +197,20 @@ enum ChatListSearchEntry: Comparable, Identifiable { return false } case let .globalPeer(lhsPeer, lhsIndex, lhsTheme, lhsStrings): - if case let .globalPeer(rhsPeer, rhsIndex, rhsTheme, rhsStrings) = rhs, lhsPeer.isEqual(rhsPeer) && lhsIndex == rhsIndex && lhsTheme === rhsTheme && lhsStrings === rhsStrings { + if case let .globalPeer(rhsPeer, rhsIndex, rhsTheme, rhsStrings) = rhs, lhsPeer == rhsPeer && lhsIndex == rhsIndex && lhsTheme === rhsTheme && lhsStrings === rhsStrings { return true } else { return false } - case let .message(lhsMessage, lhsTheme, lhsStrings): - if case let .message(rhsMessage, rhsTheme, rhsStrings) = rhs { + case let .message(lhsMessage, lhsPresentationData): + if case let .message(rhsMessage, rhsPresentationData) = rhs { if lhsMessage.id != rhsMessage.id { return false } if lhsMessage.stableVersion != rhsMessage.stableVersion { return false } - if lhsTheme !== rhsTheme { - return false - } - if lhsStrings !== rhsStrings { + if lhsPresentationData !== rhsPresentationData { return false } return true @@ -228,8 +237,8 @@ enum ChatListSearchEntry: Comparable, Identifiable { case .message: return true } - case let .message(lhsMessage, _, _): - if case let .message(rhsMessage, _, _) = rhs { + case let .message(lhsMessage, _): + if case let .message(rhsMessage, _) = rhs { return MessageIndex(lhsMessage) < MessageIndex(rhsMessage) } else { return false @@ -237,7 +246,7 @@ enum ChatListSearchEntry: Comparable, Identifiable { } } - func item(account: Account, enableHeaders: Bool, interaction: ChatListNodeInteraction) -> ListViewItem { + func item(account: Account, enableHeaders: Bool, onlyWriteable: Bool, interaction: ChatListNodeInteraction) -> ListViewItem { switch self { case let .localPeer(peer, associatedPeer, _, theme, strings): let primaryPeer: Peer @@ -247,18 +256,43 @@ enum ChatListSearchEntry: Comparable, Identifiable { chatPeer = peer } else { primaryPeer = peer - chatPeer = associatedPeer + chatPeer = peer } - return ContactsPeerItem(theme: theme, strings: strings, account: account, peer: primaryPeer, chatPeer: chatPeer, status: .none, selection: .none, hasActiveRevealControls: false, index: nil, header: ChatListSearchItemHeader(type: .localPeers, theme: theme, strings: strings, actionTitle: nil, action: nil), action: { _ in + var enabled = true + if onlyWriteable { + if let peer = chatPeer { + enabled = canSendMessagesToPeer(peer) + } else { + enabled = false + } + } + + return ContactsPeerItem(theme: theme, strings: strings, account: account, peer: primaryPeer, chatPeer: chatPeer, status: .none, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: ChatListSearchItemHeader(type: .localPeers, theme: theme, strings: strings, actionTitle: nil, action: nil), action: { _ in interaction.peerSelected(peer) }) case let .globalPeer(peer, _, theme, strings): - return ContactsPeerItem(theme: theme, strings: strings, account: account, peer: peer, chatPeer: peer, status: .addressName, selection: .none, hasActiveRevealControls: false, index: nil, header: ChatListSearchItemHeader(type: .globalPeers, theme: theme, strings: strings, actionTitle: nil, action: nil), action: { _ in - interaction.peerSelected(peer) + var enabled = true + if onlyWriteable { + enabled = canSendMessagesToPeer(peer.peer) + } + + var suffixString = "" + if let subscribers = peer.subscribers, subscribers != 0 { + if peer.peer is TelegramUser { + suffixString = ", \(strings.Conversation_StatusSubscribers(subscribers))" + } else if let channel = peer.peer as? TelegramChannel, case .broadcast = channel.info { + suffixString = ", \(strings.Conversation_StatusSubscribers(subscribers))" + } else { + suffixString = ", \(strings.Conversation_StatusMembers(subscribers))" + } + } + + return ContactsPeerItem(theme: theme, strings: strings, account: account, peer: peer.peer, chatPeer: peer.peer, status: .addressName(suffixString), enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: ChatListSearchItemHeader(type: .globalPeers, theme: theme, strings: strings, actionTitle: nil, action: nil), action: { _ in + interaction.peerSelected(peer.peer) }) - case let .message(message, theme, strings): - return ChatListItem(theme: theme, strings: strings, account: account, index: ChatListIndex(pinningIndex: nil, messageIndex: MessageIndex(message)), message: message, peer: RenderedPeer(message: message), combinedReadState: nil, notificationSettings: nil, summaryInfo: ChatListMessageTagSummaryInfo(), embeddedState: nil, editing: false, hasActiveRevealControls: false, inputActivities: nil, header: enableHeaders ? ChatListSearchItemHeader(type: .messages, theme: theme, strings: strings, actionTitle: nil, action: nil) : nil, interaction: interaction) + case let .message(message, presentationData): + return ChatListItem(presentationData: presentationData, account: account, peerGroupId: nil, index: ChatListIndex(pinningIndex: nil, messageIndex: MessageIndex(message)), content: .peer(message: message, peer: RenderedPeer(message: message), combinedReadState: nil, notificationSettings: nil, summaryInfo: ChatListMessageTagSummaryInfo(), embeddedState: nil, inputActivities: nil), editing: false, hasActiveRevealControls: false, header: enableHeaders ? ChatListSearchItemHeader(type: .messages, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil) : nil, interaction: interaction) } } } @@ -276,22 +310,22 @@ struct ChatListSearchContainerTransition { let displayingResults: Bool } -private func chatListSearchContainerPreparedRecentTransition(from fromEntries: [ChatListRecentEntry], to toEntries: [ChatListRecentEntry], account: Account, peerSelected: @escaping (Peer) -> Void, clearRecentlySearchedPeers: @escaping () -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, deletePeer: @escaping (PeerId) -> Void) -> ChatListSearchContainerRecentTransition { +private func chatListSearchContainerPreparedRecentTransition(from fromEntries: [ChatListRecentEntry], to toEntries: [ChatListRecentEntry], account: Account, onlyWriteable: Bool, peerSelected: @escaping (Peer) -> Void, peerLongTapped: @escaping (Peer) -> Void, clearRecentlySearchedPeers: @escaping () -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, deletePeer: @escaping (PeerId) -> Void) -> ChatListSearchContainerRecentTransition { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } - let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, peerSelected: peerSelected, clearRecentlySearchedPeers: clearRecentlySearchedPeers, setPeerIdWithRevealedOptions: setPeerIdWithRevealedOptions, deletePeer: deletePeer), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, peerSelected: peerSelected, clearRecentlySearchedPeers: clearRecentlySearchedPeers, setPeerIdWithRevealedOptions: setPeerIdWithRevealedOptions, deletePeer: deletePeer), directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, onlyWriteable: onlyWriteable, peerSelected: peerSelected, peerLongTapped: peerLongTapped, clearRecentlySearchedPeers: clearRecentlySearchedPeers, setPeerIdWithRevealedOptions: setPeerIdWithRevealedOptions, deletePeer: deletePeer), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, onlyWriteable: onlyWriteable, peerSelected: peerSelected, peerLongTapped: peerLongTapped, clearRecentlySearchedPeers: clearRecentlySearchedPeers, setPeerIdWithRevealedOptions: setPeerIdWithRevealedOptions, deletePeer: deletePeer), directionHint: nil) } return ChatListSearchContainerRecentTransition(deletions: deletions, insertions: insertions, updates: updates) } -func chatListSearchContainerPreparedTransition(from fromEntries: [ChatListSearchEntry], to toEntries: [ChatListSearchEntry], displayingResults: Bool, account: Account, enableHeaders: Bool, interaction: ChatListNodeInteraction) -> ChatListSearchContainerTransition { +func chatListSearchContainerPreparedTransition(from fromEntries: [ChatListSearchEntry], to toEntries: [ChatListSearchEntry], displayingResults: Bool, account: Account, enableHeaders: Bool, onlyWriteable: Bool, interaction: ChatListNodeInteraction) -> ChatListSearchContainerTransition { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } - let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, enableHeaders: enableHeaders, interaction: interaction), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, enableHeaders: enableHeaders, interaction: interaction), directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, enableHeaders: enableHeaders, onlyWriteable: onlyWriteable, interaction: interaction), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, enableHeaders: enableHeaders, onlyWriteable: onlyWriteable, interaction: interaction), directionHint: nil) } return ChatListSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, displayingResults: displayingResults) } @@ -324,7 +358,7 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { private var enqueuedRecentTransitions: [(ChatListSearchContainerRecentTransition, Bool)] = [] private var enqueuedTransitions: [(ChatListSearchContainerTransition, Bool)] = [] - private var hasValidLayout = false + private var validLayout: ContainerViewLayout? private let recentDisposable = MetaDisposable() @@ -334,16 +368,16 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { private var presentationData: PresentationData private var presentationDataDisposable: Disposable? - private let themeAndStringsPromise: Promise<(PresentationTheme, PresentationStrings)> + private let presentationDataPromise: Promise private var stateValue = ChatListSearchContainerNodeState() private let statePromise: ValuePromise - init(account: Account, openPeer: @escaping (Peer) -> Void, openMessage: @escaping (Peer, MessageId) -> Void) { + init(account: Account, onlyWriteable: Bool, groupId: PeerGroupId?, openPeer: @escaping (Peer) -> Void, openRecentPeerOptions: @escaping (Peer) -> Void, openMessage: @escaping (Peer, MessageId) -> Void) { self.account = account self.openMessage = openMessage self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } - self.themeAndStringsPromise = Promise((self.presentationData.theme, self.presentationData.strings)) + self.presentationDataPromise = Promise(ChatListPresentationData(theme: self.presentationData.theme, strings: self.presentationData.strings, timeFormat: self.presentationData.timeFormat)) self.recentListNode = ListView() self.listNode = ListView() @@ -359,40 +393,72 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { self.listNode.isHidden = true - let themeAndStringsPromise = self.themeAndStringsPromise + let presentationDataPromise = self.presentationDataPromise let foundItems = searchQuery.get() |> mapToSignal { query -> Signal<[ChatListSearchEntry]?, NoError> in if let query = query, !query.isEmpty { - let foundLocalPeers = account.postbox.searchPeers(query: query.lowercased()) - let foundRemotePeers: Signal<[Peer], NoError> = .single([]) |> then(searchPeers(account: account, query: query) + let accountPeer = account.postbox.loadedPeerWithId(account.peerId) |> take(1) + let foundLocalPeers = account.postbox.searchPeers(query: query.lowercased(), groupId: groupId) + let foundRemotePeers: Signal<([FoundPeer], [FoundPeer]), NoError> = .single(([], [])) |> then(searchPeers(account: account, query: query) |> delay(0.2, queue: Queue.concurrentDefaultQueue())) - let foundRemoteMessages: Signal<[Message], NoError> = .single([]) |> then(searchMessages(account: account, peerId: nil, query: query) + let location: SearchMessagesLocation + if let groupId = groupId { + location = .group(groupId) + } else { + location = .general + } + let foundRemoteMessages: Signal<[Message], NoError> = .single([]) |> then(searchMessages(account: account, location: location, query: query) |> delay(0.2, queue: Queue.concurrentDefaultQueue())) - return combineLatest(foundLocalPeers, foundRemotePeers, foundRemoteMessages, themeAndStringsPromise.get()) - |> map { foundLocalPeers, foundRemotePeers, foundRemoteMessages, themeAndStrings -> [ChatListSearchEntry]? in + return combineLatest(accountPeer, foundLocalPeers, foundRemotePeers, foundRemoteMessages, presentationDataPromise.get()) + |> map { accountPeer, foundLocalPeers, foundRemotePeers, foundRemoteMessages, presentationData -> [ChatListSearchEntry]? in var entries: [ChatListSearchEntry] = [] var index = 0 + + var existingPeerIds = Set() + + if presentationData.strings.DialogList_SavedMessages.lowercased().hasPrefix(query.lowercased()) { + if !existingPeerIds.contains(accountPeer.id) { + existingPeerIds.insert(accountPeer.id) + entries.append(.localPeer(accountPeer, nil, index, presentationData.theme, presentationData.strings)) + index += 1 + } + } + for renderedPeer in foundLocalPeers { - if let peer = renderedPeer.peers[renderedPeer.peerId] { - var associatedPeer: Peer? - if let associatedPeerId = peer.associatedPeerId { - associatedPeer = renderedPeer.peers[associatedPeerId] + if let peer = renderedPeer.peers[renderedPeer.peerId], peer.id != account.peerId { + if !existingPeerIds.contains(peer.id) { + existingPeerIds.insert(peer.id) + var associatedPeer: Peer? + if let associatedPeerId = peer.associatedPeerId { + associatedPeer = renderedPeer.peers[associatedPeerId] + } + entries.append(.localPeer(peer, associatedPeer, index, presentationData.theme, presentationData.strings)) + index += 1 } - entries.append(.localPeer(peer, associatedPeer, index, themeAndStrings.0, themeAndStrings.1)) + } + } + + for peer in foundRemotePeers.0 { + if !existingPeerIds.contains(peer.peer.id) { + existingPeerIds.insert(peer.peer.id) + entries.append(.localPeer(peer.peer, nil, index, presentationData.theme, presentationData.strings)) index += 1 } } index = 0 - for peer in foundRemotePeers { - entries.append(.globalPeer(peer, index, themeAndStrings.0, themeAndStrings.1)) - index += 1 + for peer in foundRemotePeers.1 { + if !existingPeerIds.contains(peer.peer.id) { + existingPeerIds.insert(peer.peer.id) + entries.append(.globalPeer(peer, index, presentationData.theme, presentationData.strings)) + index += 1 + } } index = 0 for message in foundRemoteMessages { - entries.append(.message(message, themeAndStrings.0, themeAndStrings.1)) + entries.append(.message(message, presentationData)) index += 1 } @@ -415,6 +481,7 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { openMessage(peer, message.id) } self?.listNode.clearHighlightAnimated(true) + }, groupSelected: { _ in }, setPeerIdWithRevealedOptions: { [weak self] peerId, fromPeerId in if let strongSelf = self { strongSelf.updateState { state in @@ -425,17 +492,19 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { } } } - }, setPeerPinned: { _, _ in + }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, deletePeer: { _ in - + }, updatePeerGrouping: { _, _ in }) let previousRecentItems = Atomic<[ChatListRecentEntry]?>(value: nil) - let recentItemsTransition = combineLatest(recentlySearchedPeers(postbox: account.postbox), themeAndStringsPromise.get(), self.statePromise.get()) - |> mapToSignal { [weak self] peers, themeAndStrings, state -> Signal<(ChatListSearchContainerRecentTransition, Bool), NoError> in + let recentItemsTransition = combineLatest(recentlySearchedPeers(postbox: account.postbox), presentationDataPromise.get(), self.statePromise.get()) + |> mapToSignal { [weak self] peers, presentationData, state -> Signal<(ChatListSearchContainerRecentTransition, Bool), NoError> in var entries: [ChatListRecentEntry] = [] - entries.append(.topPeers([], themeAndStrings.0, themeAndStrings.1)) + if groupId == nil { + entries.append(.topPeers([], presentationData.theme, presentationData.strings)) + } var peerIds = Set() var index = 0 loop: for renderedPeer in peers { @@ -449,15 +518,17 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { if let associatedPeerId = peer.associatedPeerId { associatedPeer = renderedPeer.peers[associatedPeerId] } - entries.append(.peer(index: index, peer: peer, associatedPeer: associatedPeer, themeAndStrings.0, themeAndStrings.1, state.peerIdWithRevealedOptions == peer.id)) + entries.append(.peer(index: index, peer: peer, associatedPeer: associatedPeer, presentationData.theme, presentationData.strings, state.peerIdWithRevealedOptions == peer.id)) index += 1 } } let previousEntries = previousRecentItems.swap(entries) - let transition = chatListSearchContainerPreparedRecentTransition(from: previousEntries ?? [], to: entries, account: account, peerSelected: { peer in + let transition = chatListSearchContainerPreparedRecentTransition(from: previousEntries ?? [], to: entries, account: account, onlyWriteable: onlyWriteable, peerSelected: { peer in self?.recentListNode.clearHighlightAnimated(true) openPeer(peer) + }, peerLongTapped: { peer in + openRecentPeerOptions(peer) }, clearRecentlySearchedPeers: { self?.clearRecentSearch() }, setPeerIdWithRevealedOptions: { peerId, fromPeerId in @@ -482,7 +553,7 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { let previousEntries = previousSearchItems.swap(entries) let firstTime = previousEntries == nil - let transition = chatListSearchContainerPreparedTransition(from: previousEntries ?? [], to: entries ?? [], displayingResults: entries != nil, account: account, enableHeaders: true, interaction: interaction) + let transition = chatListSearchContainerPreparedTransition(from: previousEntries ?? [], to: entries ?? [], displayingResults: entries != nil, account: account, enableHeaders: true, onlyWriteable: onlyWriteable, interaction: interaction) strongSelf.enqueueTransition(transition, firstTime: firstTime) } })) @@ -495,8 +566,8 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { strongSelf.presentationData = presentationData - if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings { - strongSelf.updateThemeAndStrings(theme: presentationData.theme, strings: presentationData.strings) + if previousTheme !== presentationData.theme { + strongSelf.updateTheme(theme: presentationData.theme) } } }) @@ -516,7 +587,7 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { self.presentationDataDisposable?.dispose() } - private func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { + private func updateTheme(theme: PresentationTheme) { self.backgroundColor = theme.chatList.backgroundColor } @@ -539,7 +610,7 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { private func enqueueRecentTransition(_ transition: ChatListSearchContainerRecentTransition, firstTime: Bool) { enqueuedRecentTransitions.append((transition, firstTime)) - if self.hasValidLayout { + if self.validLayout != nil { while !self.enqueuedRecentTransitions.isEmpty { self.dequeueRecentTransition() } @@ -565,7 +636,7 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { private func enqueueTransition(_ transition: ChatListSearchContainerTransition, firstTime: Bool) { enqueuedTransitions.append((transition, firstTime)) - if self.hasValidLayout { + if self.validLayout != nil { while !self.enqueuedTransitions.isEmpty { self.dequeueTransition() } @@ -598,6 +669,9 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) + let hadValidLayout = self.validLayout != nil + self.validLayout = layout + var duration: Double = 0.0 var curve: UInt = 0 switch transition { @@ -622,13 +696,12 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { } self.recentListNode.frame = CGRect(origin: CGPoint(), size: layout.size) - self.recentListNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: navigationBarHeight, left: 0.0, bottom: layout.insets(options: [.input]).bottom, right: 0.0), duration: duration, curve: listViewCurve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + self.recentListNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: navigationBarHeight, left: layout.safeInsets.left, bottom: layout.insets(options: [.input]).bottom, right: layout.safeInsets.right), duration: duration, curve: listViewCurve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) self.listNode.frame = CGRect(origin: CGPoint(), size: layout.size) - self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: navigationBarHeight, left: 0.0, bottom: layout.insets(options: [.input]).bottom, right: 0.0), duration: duration, curve: listViewCurve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: navigationBarHeight, left: layout.safeInsets.left, bottom: layout.insets(options: [.input]).bottom, right: layout.safeInsets.right), duration: duration, curve: listViewCurve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) - if !hasValidLayout { - hasValidLayout = true + if !hadValidLayout { while !self.enqueuedRecentTransitions.isEmpty { self.dequeueRecentTransition() } @@ -661,8 +734,13 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { } } else if let selectedItemNode = selectedItemNode as? ContactsPeerItemNode, let peer = selectedItemNode.peer { return (selectedItemNode.view, peer.id) - } else if let selectedItemNode = selectedItemNode as? ChatListItemNode, let peerId = selectedItemNode.item?.peer.peerId { - return (selectedItemNode.view, peerId) + } else if let selectedItemNode = selectedItemNode as? ChatListItemNode, let item = selectedItemNode.item { + switch item.content { + case let .peer(_, peer, _, _, _, _, _): + return (selectedItemNode.view, peer.peerId) + case let .groupReference(groupId, _, _, _): + return (selectedItemNode.view, groupId) + } } return nil } diff --git a/TelegramUI/ChatListSearchItem.swift b/TelegramUI/ChatListSearchItem.swift index 9508896e44..03ac369378 100644 --- a/TelegramUI/ChatListSearchItem.swift +++ b/TelegramUI/ChatListSearchItem.swift @@ -20,7 +20,7 @@ class ChatListSearchItem: ListViewItem { self.activate = activate } - func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { async { let node = ChatListSearchItemNode() node.placeholder = self.placeholder @@ -30,7 +30,7 @@ class ChatListSearchItem: ListViewItem { if let nextItem = nextItem as? ChatListItem, nextItem.index.pinningIndex != nil { nextIsPinned = true } - let (layout, apply) = makeLayout(self, width, nextIsPinned) + let (layout, apply) = makeLayout(self, params, nextIsPinned) node.contentSize = layout.contentSize node.insets = layout.insets @@ -44,7 +44,7 @@ class ChatListSearchItem: ListViewItem { } } - func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { if let node = node as? ChatListSearchItemNode { Queue.mainQueue().async { let layout = node.asyncLayout() @@ -53,7 +53,7 @@ class ChatListSearchItem: ListViewItem { if let nextItem = nextItem as? ChatListItem, nextItem.index.pinningIndex != nil { nextIsPinned = true } - let (nodeLayout, apply) = layout(self, width, nextIsPinned) + let (nodeLayout, apply) = layout(self, params, nextIsPinned) Queue.mainQueue().async { completion(nodeLayout, { apply(animation.isAnimated) @@ -83,28 +83,30 @@ class ChatListSearchItemNode: ListViewItemNode { self.addSubnode(self.searchBarNode) } - override func layoutForWidth(_ width: CGFloat, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { + override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { let makeLayout = self.asyncLayout() var nextIsPinned = false if let nextItem = nextItem as? ChatListItem, nextItem.index.pinningIndex != nil { nextIsPinned = true } - let (layout, apply) = makeLayout(item as! ChatListSearchItem, width, nextIsPinned) + let (layout, apply) = makeLayout(item as! ChatListSearchItem, params, nextIsPinned) apply(false) self.contentSize = layout.contentSize self.insets = layout.insets } - func asyncLayout() -> (_ item: ChatListSearchItem, _ width: CGFloat, _ nextIsPinned: Bool) -> (ListViewItemNodeLayout, (Bool) -> Void) { + func asyncLayout() -> (_ item: ChatListSearchItem, _ params: ListViewItemLayoutParams, _ nextIsPinned: Bool) -> (ListViewItemNodeLayout, (Bool) -> Void) { let searchBarNodeLayout = self.searchBarNode.asyncLayout() let placeholder = self.placeholder - return { item, width, nextIsPinned in + return { item, params, nextIsPinned in + let baseWidth = params.width - params.leftInset - params.rightInset + let backgroundColor = nextIsPinned ? item.theme.chatList.pinnedItemBackgroundColor : item.theme.chatList.itemBackgroundColor - let searchBarApply = searchBarNodeLayout(NSAttributedString(string: placeholder ?? "", font: searchBarFont, textColor: UIColor(rgb: 0x8e8e93)), CGSize(width: width - 16.0, height: CGFloat.greatestFiniteMagnitude), UIColor(rgb: 0x8e8e93), nextIsPinned ? item.theme.chatList.pinnedSearchBarColor : item.theme.chatList.regularSearchBarColor, backgroundColor) + let searchBarApply = searchBarNodeLayout(NSAttributedString(string: placeholder ?? "", font: searchBarFont, textColor: UIColor(rgb: 0x8e8e93)), CGSize(width: baseWidth - 16.0, height: CGFloat.greatestFiniteMagnitude), UIColor(rgb: 0x8e8e93), nextIsPinned ? item.theme.chatList.pinnedSearchBarColor : item.theme.chatList.regularSearchBarColor, backgroundColor) - let layout = ListViewItemNodeLayout(contentSize: CGSize(width: width, height: 44.0 + 4.0), insets: UIEdgeInsets()) + let layout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 44.0), insets: UIEdgeInsets()) return (layout, { [weak self] animated in if let strongSelf = self { @@ -115,10 +117,10 @@ class ChatListSearchItemNode: ListViewItemNode { transition = .immediate } - strongSelf.searchBarNode.frame = CGRect(origin: CGPoint(x: 8.0, y: 8.0), size: CGSize(width: width - 16.0, height: 28.0)) + strongSelf.searchBarNode.frame = CGRect(origin: CGPoint(x: params.leftInset + 8.0, y: 8.0), size: CGSize(width: baseWidth - 16.0, height: 28.0)) searchBarApply() - strongSelf.searchBarNode.bounds = CGRect(origin: CGPoint(), size: CGSize(width: width - 16.0, height: 28.0)) + strongSelf.searchBarNode.bounds = CGRect(origin: CGPoint(), size: CGSize(width: baseWidth - 16.0, height: 28.0)) transition.updateBackgroundColor(node: strongSelf, color: backgroundColor) } diff --git a/TelegramUI/ChatListSearchItemHeader.swift b/TelegramUI/ChatListSearchItemHeader.swift index 0f5f992631..0e11981f3b 100644 --- a/TelegramUI/ChatListSearchItemHeader.swift +++ b/TelegramUI/ChatListSearchItemHeader.swift @@ -76,8 +76,9 @@ final class ChatListSearchItemHeaderNode: ListViewItemHeaderNode { self.addSubnode(self.sectionHeaderNode) } - override func layout() { - self.sectionHeaderNode.frame = CGRect(origin: CGPoint(), size: self.bounds.size) + override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) { + self.sectionHeaderNode.frame = CGRect(origin: CGPoint(), size: size) + self.sectionHeaderNode.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset) } override func animateRemoved(duration: Double) { diff --git a/TelegramUI/ChatListSearchRecentPeersNode.swift b/TelegramUI/ChatListSearchRecentPeersNode.swift index 24d1f253a8..6267e993d0 100644 --- a/TelegramUI/ChatListSearchRecentPeersNode.swift +++ b/TelegramUI/ChatListSearchRecentPeersNode.swift @@ -32,6 +32,7 @@ final class ChatListSearchRecentPeersNode: ASDisplayNode { private let share: Bool private let peerSelected: (Peer) -> Void + private let peerLongTapped: (Peer) -> Void private let isPeerSelected: (PeerId) -> Bool private let disposable = MetaDisposable() @@ -39,12 +40,13 @@ final class ChatListSearchRecentPeersNode: ASDisplayNode { private var items: [ListViewItem] = [] private var itemCustomWidth: CGFloat? - init(account: Account, theme: PresentationTheme, mode: HorizontalPeerItemMode, strings: PresentationStrings, peerSelected: @escaping (Peer) -> Void, isPeerSelected: @escaping (PeerId) -> Bool, share: Bool = false) { + init(account: Account, theme: PresentationTheme, mode: HorizontalPeerItemMode, strings: PresentationStrings, peerSelected: @escaping (Peer) -> Void, peerLongTapped: @escaping (Peer) -> Void, isPeerSelected: @escaping (PeerId) -> Bool, share: Bool = false) { self.theme = theme self.strings = strings self.mode = mode self.share = share self.peerSelected = peerSelected + self.peerLongTapped = peerLongTapped self.isPeerSelected = isPeerSelected self.sectionHeaderNode = ListSectionHeaderNode(theme: theme) @@ -62,7 +64,9 @@ final class ChatListSearchRecentPeersNode: ASDisplayNode { if let strongSelf = self { var items: [ListViewItem] = [] for peer in peers { - items.append(HorizontalPeerItem(theme: strongSelf.theme, strings: strongSelf.strings, mode: mode, account: account, peer: peer, action: peerSelected, isPeerSelected: isPeerSelected, customWidth: strongSelf.itemCustomWidth)) + items.append(HorizontalPeerItem(theme: strongSelf.theme, strings: strongSelf.strings, mode: mode, account: account, peer: peer, action: peerSelected, longTapAction: { peer in + peerLongTapped(peer) + }, isPeerSelected: isPeerSelected, customWidth: strongSelf.itemCustomWidth)) } strongSelf.items = items strongSelf.listView.transaction(deleteIndices: [], insertIndicesAndItems: (0 ..< items.count).map({ ListViewInsertItem(index: $0, previousIndex: nil, item: items[$0], directionHint: .Down) }), updateIndicesAndItems: [], options: [], updateOpaqueState: nil) @@ -88,22 +92,20 @@ final class ChatListSearchRecentPeersNode: ASDisplayNode { return CGSize(width: constrainedSize.width, height: 120.0) } - override func layout() { - super.layout() - - let bounds = self.bounds - - self.sectionHeaderNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: self.bounds.size.width, height: 29.0)) - self.sectionHeaderNode.layout() + func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) { + self.sectionHeaderNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: 29.0)) + self.sectionHeaderNode.updateLayout(size: CGSize(width: size.width, height: 29.0), leftInset: leftInset, rightInset: rightInset) var insets = UIEdgeInsets() + insets.top += leftInset + insets.bottom += rightInset var itemCustomWidth: CGFloat? if self.share { insets.top = 7.0 insets.bottom = 7.0 - itemCustomWidth = calculateItemCustomWidth(width: bounds.size.width) + itemCustomWidth = calculateItemCustomWidth(width: size.width) } var updateItems: [ListViewUpdateItem] = [] @@ -112,15 +114,15 @@ final class ChatListSearchRecentPeersNode: ASDisplayNode { for i in 0 ..< self.items.count { if let item = self.items[i] as? HorizontalPeerItem { - self.items[i] = HorizontalPeerItem(theme: self.theme, strings: self.strings, mode: self.mode, account: item.account, peer: item.peer, action: self.peerSelected, isPeerSelected: self.isPeerSelected, customWidth: itemCustomWidth) + self.items[i] = HorizontalPeerItem(theme: self.theme, strings: self.strings, mode: self.mode, account: item.account, peer: item.peer, action: self.peerSelected, longTapAction: self.peerLongTapped, isPeerSelected: self.isPeerSelected, customWidth: itemCustomWidth) updateItems.append(ListViewUpdateItem(index: i, previousIndex: i, item: self.items[i], directionHint: nil)) } } } - self.listView.bounds = CGRect(x: 0.0, y: 0.0, width: 92.0, height: bounds.size.width) - self.listView.position = CGPoint(x: bounds.size.width / 2.0, y: 92.0 / 2.0 + 29.0) - self.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: updateItems, options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: CGSize(width: 92.0, height: bounds.size.width), insets: insets, duration: 0.0, curve: .Default), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + self.listView.bounds = CGRect(x: 0.0, y: 0.0, width: 92.0, height: size.width) + self.listView.position = CGPoint(x: size.width / 2.0, y: 92.0 / 2.0 + 29.0) + self.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: updateItems, options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: CGSize(width: 92.0, height: size.width), insets: insets, duration: 0.0, curve: .Default), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) } func viewAndPeerAtPoint(_ point: CGPoint) -> (UIView, PeerId)? { diff --git a/TelegramUI/ChatListTypingNode.swift b/TelegramUI/ChatListTypingNode.swift index 1a52d45aa8..40109da498 100644 --- a/TelegramUI/ChatListTypingNode.swift +++ b/TelegramUI/ChatListTypingNode.swift @@ -59,6 +59,7 @@ private final class ChatListInputActivitiesDotsNode: ASDisplayNode { animation.duration = 0.54 animation.repeatCount = Float.infinity animation.calculationMode = kCAAnimationDiscrete + animation.beginTime = 1.0 self.layer.add(animation, forKey: "image") } @@ -116,11 +117,44 @@ final class ChatListInputActivitiesNode: ASDisplayNode { return { [weak self] boundingSize, strings, color, peerId, activities in let string: NSAttributedString? if !activities.isEmpty { + var commonKey: Int32? = activities[0].1.key + for i in 1 ..< activities.count { + if activities[i].1.key != commonKey { + commonKey = nil + break + } + } + if activities.count == 1 { if activities[0].0.id == peerId { - string = NSAttributedString(string: strings.DialogList_Typing, font: textFont, textColor: color) + let text: String + switch activities[0].1 { + case .uploadingVideo: + text = strings.Activity_UploadingVideo + case .uploadingInstantVideo: + text = strings.Activity_UploadingVideoMessage + case .uploadingPhoto: + text = strings.Activity_UploadingPhoto + case .uploadingFile: + text = strings.Activity_UploadingDocument + case .recordingVoice: + text = strings.Activity_RecordingAudio + case .recordingInstantVideo: + text = strings.Activity_RecordingVideoMessage + case .playingGame: + text = strings.Activity_PlayingGame + case .typingText: + text = strings.DialogList_Typing + } + string = NSAttributedString(string: text, font: textFont, textColor: color) } else { - string = NSAttributedString(string: strings.DialogList_SingleTypingSuffix(activities[0].0.compactDisplayTitle).0, font: textFont, textColor: color) + let text: String + if let _ = commonKey, case .typingText = activities[0].1 { + text = strings.DialogList_SingleTypingSuffix(activities[0].0.compactDisplayTitle).0 + } else { + text = activities[0].0.compactDisplayTitle + } + string = NSAttributedString(string: text, font: textFont, textColor: color) } } else { string = NSAttributedString(string: strings.DialogList_MultipleTypingSuffix(activities.count).0, font: textFont, textColor: color) @@ -128,7 +162,7 @@ final class ChatListInputActivitiesNode: ASDisplayNode { } else { string = nil } - let (textLayout, textApply) = makeTextLayout(string, nil, 1, .end, CGSize(width: boundingSize.width - 12.0, height: boundingSize.height), .left, nil, UIEdgeInsets(top: 1.0, left: 0.0, bottom: 1.0, right: 0.0)) + let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: string, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: boundingSize.width - 12.0, height: boundingSize.height), alignment: .left, cutout: nil, insets: UIEdgeInsets(top: 1.0, left: 0.0, bottom: 1.0, right: 0.0))) let dots = getDotsImage(color: color) diff --git a/TelegramUI/ChatListViewTransition.swift b/TelegramUI/ChatListViewTransition.swift index 09df736fdf..fa13de859c 100644 --- a/TelegramUI/ChatListViewTransition.swift +++ b/TelegramUI/ChatListViewTransition.swift @@ -53,7 +53,7 @@ func preparedChatListNodeViewTransition(from fromView: ChatListNodeView?, to toV if let fromView = fromView { previousCount = fromView.filteredEntries.count } else { - previousCount = 0; + previousCount = 0 } for index in deleteIndices { adjustedDeleteIndices.append(ListViewDeleteItem(index: previousCount - 1 - index, directionHint: nil)) @@ -79,20 +79,22 @@ func preparedChatListNodeViewTransition(from fromView: ChatListNodeView?, to toV } } - var minTimestamp: Int32 = Int32.max - var maxTimestamp: Int32 = 0 + var minTimestamp: Int32? + var maxTimestamp: Int32? for (_, item, _) in indicesAndItems { - let timestamp = item.index.messageIndex.timestamp - - if timestamp < minTimestamp { - minTimestamp = timestamp - } - if timestamp > maxTimestamp { - maxTimestamp = timestamp + if case .PeerEntry = item { + let timestamp = item.index.messageIndex.timestamp + + if minTimestamp == nil || timestamp < minTimestamp! { + minTimestamp = timestamp + } + if maxTimestamp == nil || timestamp > maxTimestamp! { + maxTimestamp = timestamp + } } } - if abs(maxTimestamp - minTimestamp) > 60 * 60 { + if let minTimestamp = minTimestamp, let maxTimestamp = maxTimestamp, abs(maxTimestamp - minTimestamp) > 60 * 60 { let _ = options.insert(.AnimateCrossfade) } else { let _ = options.insert(.AnimateAlpha) @@ -122,7 +124,7 @@ func preparedChatListNodeViewTransition(from fromView: ChatListNodeView?, to toV } case .UpperToLower: break - case .AroundIndex: + case .AroundId, .AroundIndex: break } } diff --git a/TelegramUI/ChatLoadingNode.swift b/TelegramUI/ChatLoadingNode.swift index abbb2390fa..cada0d92eb 100644 --- a/TelegramUI/ChatLoadingNode.swift +++ b/TelegramUI/ChatLoadingNode.swift @@ -13,7 +13,7 @@ final class ChatLoadingNode: ASDisplayNode { self.backgroundNode.displaysAsynchronously = false self.backgroundNode.image = PresentationResourcesChat.chatLoadingIndicatorBackgroundImage(theme) - self.activityIndicator = ActivityIndicator(type: .custom(theme.chat.serviceMessage.serviceMessagePrimaryTextColor), speed: .regular) + self.activityIndicator = ActivityIndicator(type: .custom(theme.chat.serviceMessage.serviceMessagePrimaryTextColor, 22.0), speed: .regular) super.init() diff --git a/TelegramUI/ChatMediaInputGifPane.swift b/TelegramUI/ChatMediaInputGifPane.swift index 3c9dd94fe9..0d4c8a8126 100644 --- a/TelegramUI/ChatMediaInputGifPane.swift +++ b/TelegramUI/ChatMediaInputGifPane.swift @@ -26,8 +26,9 @@ final class ChatMediaInputGifPane: ASDisplayNode, UIScrollViewDelegate { self.disposable.dispose() } - func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + func updateLayout(size: CGSize, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) { self.validLayout = size + self.multiplexedNode?.bottomInset = bottomInset self.multiplexedNode?.frame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height)) } diff --git a/TelegramUI/ChatMediaInputMetaSectionItemNode.swift b/TelegramUI/ChatMediaInputMetaSectionItemNode.swift index e9b008585f..1a63a7c96f 100644 --- a/TelegramUI/ChatMediaInputMetaSectionItemNode.swift +++ b/TelegramUI/ChatMediaInputMetaSectionItemNode.swift @@ -27,7 +27,7 @@ final class ChatMediaInputMetaSectionItem: ListViewItem { self.theme = theme } - func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { async { let node = ChatMediaInputMetaSectionItemNode() node.contentSize = CGSize(width: 41.0, height: 41.0) @@ -43,7 +43,7 @@ final class ChatMediaInputMetaSectionItem: ListViewItem { } } - public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { completion(ListViewItemNodeLayout(contentSize: node.contentSize, insets: node.insets), { (node as? ChatMediaInputMetaSectionItemNode)?.setItem(item: self) (node as? ChatMediaInputMetaSectionItemNode)?.updateTheme(theme: self.theme) diff --git a/TelegramUI/ChatMediaInputNode.swift b/TelegramUI/ChatMediaInputNode.swift index f6ead05791..f5cf7535c7 100644 --- a/TelegramUI/ChatMediaInputNode.swift +++ b/TelegramUI/ChatMediaInputNode.swift @@ -113,6 +113,7 @@ private func chatMediaInputPanelEntries(view: ItemCollectionsView, savedStickers index += 1 } } + entries.append(.settings(theme)) return entries } @@ -126,32 +127,34 @@ private func chatMediaInputGridEntries(view: ItemCollectionsView, savedStickers: } } - var savedStickerIds = Set() - if let savedStickers = savedStickers, !savedStickers.items.isEmpty { - let packInfo = StickerPackCollectionInfo(id: ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.savedStickers.rawValue, id: 0), flags: [], accessHash: 0, title: strings.Stickers_Favorited.uppercased(), shortName: "", hash: 0, count: 0) - for i in 0 ..< savedStickers.items.count { - if let item = savedStickers.items[i].contents as? SavedStickerItem { - savedStickerIds.insert(item.file.fileId.id) - let index = ItemCollectionItemIndex(index: Int32(i), id: item.file.fileId.id) - let stickerItem = StickerPackItem(index: index, file: item.file, indexKeys: []) - entries.append(ChatMediaInputGridEntry(index: ItemCollectionViewEntryIndex(collectionIndex: -2, collectionId: packInfo.id, itemIndex: index), stickerItem: stickerItem, stickerPackInfo: packInfo, theme: theme)) + if view.lower == nil { + var savedStickerIds = Set() + if let savedStickers = savedStickers, !savedStickers.items.isEmpty { + let packInfo = StickerPackCollectionInfo(id: ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.savedStickers.rawValue, id: 0), flags: [], accessHash: 0, title: strings.Stickers_FavoriteStickers.uppercased(), shortName: "", hash: 0, count: 0) + for i in 0 ..< savedStickers.items.count { + if let item = savedStickers.items[i].contents as? SavedStickerItem { + savedStickerIds.insert(item.file.fileId.id) + let index = ItemCollectionItemIndex(index: Int32(i), id: item.file.fileId.id) + let stickerItem = StickerPackItem(index: index, file: item.file, indexKeys: []) + entries.append(ChatMediaInputGridEntry(index: ItemCollectionViewEntryIndex(collectionIndex: -2, collectionId: packInfo.id, itemIndex: index), stickerItem: stickerItem, stickerPackInfo: packInfo, theme: theme)) + } } } - } - - if let recentStickers = recentStickers, !recentStickers.items.isEmpty { - let packInfo = StickerPackCollectionInfo(id: ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.recentStickers.rawValue, id: 0), flags: [], accessHash: 0, title: strings.Stickers_FrequentlyUsed.uppercased(), shortName: "", hash: 0, count: 0) - var addedCount = 0 - for i in 0 ..< recentStickers.items.count { - if addedCount >= 20 { - break - } - if let item = recentStickers.items[i].contents as? RecentMediaItem, let file = item.media as? TelegramMediaFile, let mediaId = item.media.id { - if !savedStickerIds.contains(mediaId.id) { - let index = ItemCollectionItemIndex(index: Int32(i), id: mediaId.id) - let stickerItem = StickerPackItem(index: index, file: file, indexKeys: []) - entries.append(ChatMediaInputGridEntry(index: ItemCollectionViewEntryIndex(collectionIndex: -1, collectionId: packInfo.id, itemIndex: index), stickerItem: stickerItem, stickerPackInfo: packInfo, theme: theme)) - addedCount += 1 + + if let recentStickers = recentStickers, !recentStickers.items.isEmpty { + let packInfo = StickerPackCollectionInfo(id: ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.recentStickers.rawValue, id: 0), flags: [], accessHash: 0, title: strings.Stickers_FrequentlyUsed.uppercased(), shortName: "", hash: 0, count: 0) + var addedCount = 0 + for i in 0 ..< recentStickers.items.count { + if addedCount >= 20 { + break + } + if let item = recentStickers.items[i].contents as? RecentMediaItem, let file = item.media as? TelegramMediaFile, let mediaId = item.media.id { + if !savedStickerIds.contains(mediaId.id) { + let index = ItemCollectionItemIndex(index: Int32(i), id: mediaId.id) + let stickerItem = StickerPackItem(index: index, file: file, indexKeys: []) + entries.append(ChatMediaInputGridEntry(index: ItemCollectionViewEntryIndex(collectionIndex: -1, collectionId: packInfo.id, itemIndex: index), stickerItem: stickerItem, stickerPackInfo: packInfo, theme: theme)) + addedCount += 1 + } } } } @@ -198,13 +201,15 @@ private enum StickerPacksCollectionUpdate { final class ChatMediaInputNodeInteraction { let navigateToCollectionId: (ItemCollectionId) -> Void + let openSettings: () -> Void var highlightedStickerItemCollectionId: ItemCollectionId? var highlightedItemCollectionId: ItemCollectionId? var previewedStickerPackItem: StickerPackItem? - init(navigateToCollectionId: @escaping (ItemCollectionId) -> Void) { + init(navigateToCollectionId: @escaping (ItemCollectionId) -> Void, openSettings: @escaping () -> Void) { self.navigateToCollectionId = navigateToCollectionId + self.openSettings = openSettings } } @@ -220,9 +225,6 @@ private func clipScrollPosition(_ position: StickerPacksCollectionPosition) -> S return position } -private let defaultPortraitPanelHeight: CGFloat = UIScreenScale.isEqual(to: 3.0) ? 271.0 : 258.0 -private let defaultLandscapePanelHeight: CGFloat = UIScreenScale.isEqual(to: 3.0) ? 194.0 : 194.0 - private enum ChatMediaInputPane { case gifs case stickers @@ -245,6 +247,7 @@ private struct ChatMediaInputPaneArrangement { final class ChatMediaInputNode: ChatInputNode { private let account: Account private let controllerInteraction: ChatControllerInteraction + private let gifPaneIsActiveUpdated: (Bool) -> Void private var inputNodeInteraction: ChatMediaInputNodeInteraction! @@ -256,7 +259,9 @@ final class ChatMediaInputNode: ChatInputNode { private let listView: ListView private let stickerPane: ChatMediaInputStickerPane + private var animatingStickerPaneOut = false private let gifPane: ChatMediaInputGifPane + private var animatingGifPaneOut = false private let itemCollectionsViewPosition = Promise() private var currentStickerPacksCollectionPosition: StickerPacksCollectionPosition? @@ -264,18 +269,19 @@ final class ChatMediaInputNode: ChatInputNode { private var stickerPreviewController: StickerPreviewController? - private var validLayout: (CGFloat, ChatPresentationInterfaceState)? + private var validLayout: (CGFloat, CGFloat, CGFloat, CGFloat, ChatPresentationInterfaceState)? private var paneArrangement: ChatMediaInputPaneArrangement private var theme: PresentationTheme private var strings: PresentationStrings private let themeAndStringsPromise: Promise<(PresentationTheme, PresentationStrings)> - init(account: Account, controllerInteraction: ChatControllerInteraction, theme: PresentationTheme, strings: PresentationStrings) { + init(account: Account, controllerInteraction: ChatControllerInteraction, theme: PresentationTheme, strings: PresentationStrings, gifPaneIsActiveUpdated: @escaping (Bool) -> Void) { self.account = account self.controllerInteraction = controllerInteraction self.theme = theme self.strings = strings + self.gifPaneIsActiveUpdated = gifPaneIsActiveUpdated self.themeAndStringsPromise = Promise((theme, strings)) @@ -321,6 +327,10 @@ final class ChatMediaInputNode: ChatInputNode { } } } + }, openSettings: { [weak self] in + if let strongSelf = self { + strongSelf.controllerInteraction.presentController(installedStickerPacksController(account: account, mode: .modal), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + } }) self.clipsToBounds = true @@ -470,14 +480,26 @@ final class ChatMediaInputNode: ChatInputNode { } private func heightForWidth(width: CGFloat) -> CGFloat { + let defaultPortraitPanelHeight: CGFloat = UIScreenScale.isEqual(to: 3.0) ? 271.0 : 258.0 + let defaultLandscapePanelHeight: CGFloat = UIScreenScale.isEqual(to: 3.0) ? 194.0 : 194.0 + + if width.isEqual(to: 812.0) { + return defaultLandscapePanelHeight + } + return defaultPortraitPanelHeight } private func setCurrentPane(_ pane: ChatMediaInputPane, transition: ContainedViewLayoutTransition) { if let index = self.paneArrangement.panes.index(of: pane), index != self.paneArrangement.currentIndex { + let previousGifPanelWasActive = self.paneArrangement.panes[self.paneArrangement.currentIndex] == .gifs self.paneArrangement = self.paneArrangement.withIndexTransition(0.0).withCurrentIndex(index) - if let (width, interfaceState) = self.validLayout { - let _ = self.updateLayout(width: width, transition: .animated(duration: 0.25, curve: .spring), interfaceState: interfaceState) + if let (width, leftInset, rightInset, bottomInset, interfaceState) = self.validLayout { + let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, transition: .animated(duration: 0.25, curve: .spring), interfaceState: interfaceState) + } + let updatedGifPanelWasActive = self.paneArrangement.panes[self.paneArrangement.currentIndex] == .gifs + if updatedGifPanelWasActive != previousGifPanelWasActive { + self.gifPaneIsActiveUpdated(updatedGifPanelWasActive) } switch pane { case .gifs: @@ -487,6 +509,10 @@ final class ChatMediaInputNode: ChatInputNode { self.setHighlightedItemCollectionId(highlightedStickerCollectionId) } } + } else { + if let (width, leftInset, rightInset, bottomInset, interfaceState) = self.validLayout { + let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, transition: .animated(duration: 0.25, curve: .spring), interfaceState: interfaceState) + } } } @@ -538,15 +564,15 @@ final class ChatMediaInputNode: ChatInputNode { } } - override func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { - self.validLayout = (width, interfaceState) + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { + self.validLayout = (width, leftInset, rightInset, bottomInset, interfaceState) if self.theme !== interfaceState.theme || self.strings !== interfaceState.strings { self.updateThemeAndStrings(theme: interfaceState.theme, strings: interfaceState.strings) } let separatorHeight = UIScreenPixel - let panelHeight = self.heightForWidth(width: width) + let panelHeight = self.heightForWidth(width: width) + bottomInset transition.updateFrame(node: self.collectionListPanel, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: 41.0))) transition.updateFrame(node: self.collectionListSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: 41.0), size: CGSize(width: width, height: separatorHeight))) @@ -576,7 +602,7 @@ final class ChatMediaInputNode: ChatInputNode { listViewCurve = .Default } - let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: CGSize(width: 41.0, height: width), insets: UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0), duration: duration, curve: listViewCurve) + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: CGSize(width: 41.0, height: width), insets: UIEdgeInsets(top: 4.0 + leftInset, left: 0.0, bottom: 4.0 + rightInset, right: 0.0), duration: duration, curve: listViewCurve) self.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) @@ -592,61 +618,80 @@ final class ChatMediaInputNode: ChatInputNode { } for (pane, paneOrigin) in visiblePanes { + let paneFrame = CGRect(origin: CGPoint(x: paneOrigin + leftInset, y: 41.0), size: CGSize(width: width - leftInset - rightInset, height: panelHeight - 41.0)) switch pane { case .gifs: if self.gifPane.supernode == nil { self.addSubnode(self.gifPane) self.gifPane.frame = CGRect(origin: CGPoint(x: -width, y: 41.0), size: CGSize(width: width, height: panelHeight - 41.0)) } - self.gifPane.layer.removeAnimation(forKey: "position") - transition.updateFrame(node: self.gifPane, frame: CGRect(origin: CGPoint(x: paneOrigin, y: 41.0), size: CGSize(width: width, height: panelHeight - 41.0))) + if self.gifPane.frame != paneFrame { + self.gifPane.layer.removeAnimation(forKey: "position") + transition.updateFrame(node: self.gifPane, frame: paneFrame) + } case .stickers: if self.stickerPane.supernode == nil { self.addSubnode(self.stickerPane) self.stickerPane.frame = CGRect(origin: CGPoint(x: width, y: 41.0), size: CGSize(width: width, height: panelHeight - 41.0)) } - self.stickerPane.layer.removeAnimation(forKey: "position") - transition.updateFrame(node: self.stickerPane, frame: CGRect(origin: CGPoint(x: paneOrigin, y: 41.0), size: CGSize(width: width, height: panelHeight - 41.0))) + if self.stickerPane.frame != paneFrame { + self.stickerPane.layer.removeAnimation(forKey: "position") + transition.updateFrame(node: self.stickerPane, frame: paneFrame) + } } } - self.gifPane.updateLayout(size: CGSize(width: width, height: panelHeight - 41.0), transition: transition) - self.stickerPane.updateLayout(size: CGSize(width: width, height: panelHeight - 41.0), transition: transition) + self.gifPane.updateLayout(size: CGSize(width: width - leftInset - rightInset, height: panelHeight - 41.0), bottomInset: bottomInset, transition: transition) + self.stickerPane.updateLayout(size: CGSize(width: width - leftInset - rightInset, height: panelHeight - 41.0), bottomInset: bottomInset, transition: transition) if self.gifPane.supernode != nil { if !visiblePanes.contains(where: { $0.0 == .gifs }) { if case .animated = transition { - var toLeft = false - if let index = self.paneArrangement.panes.index(of: .gifs), index < self.paneArrangement.currentIndex { - toLeft = true - } - transition.animatePosition(node: self.gifPane, to: CGPoint(x: (toLeft ? -width : width) + width / 2.0, y: self.gifPane.layer.position.y), removeOnCompletion: false, completion: { [weak self] value in - if let strongSelf = self, value { - strongSelf.gifPane.removeFromSupernode() + if !self.animatingGifPaneOut { + self.animatingGifPaneOut = true + var toLeft = false + if let index = self.paneArrangement.panes.index(of: .gifs), index < self.paneArrangement.currentIndex { + toLeft = true } - }) + transition.animatePosition(node: self.gifPane, to: CGPoint(x: (toLeft ? -width : width) + width / 2.0, y: self.gifPane.layer.position.y), removeOnCompletion: false, completion: { [weak self] value in + if let strongSelf = self, value { + strongSelf.animatingGifPaneOut = false + strongSelf.gifPane.removeFromSupernode() + } + }) + } } else { + self.animatingGifPaneOut = false self.gifPane.removeFromSupernode() } } + } else { + self.animatingGifPaneOut = false } if self.stickerPane.supernode != nil { if !visiblePanes.contains(where: { $0.0 == .stickers }) { if case .animated = transition { - var toLeft = false - if let index = self.paneArrangement.panes.index(of: .stickers), index < self.paneArrangement.currentIndex { - toLeft = true - } - transition.animatePosition(node: self.stickerPane, to: CGPoint(x: (toLeft ? -width : width) + width / 2.0, y: self.stickerPane.layer.position.y), removeOnCompletion: false, completion: { [weak self] value in - if let strongSelf = self, value { - strongSelf.stickerPane.removeFromSupernode() + if !self.animatingStickerPaneOut { + self.animatingStickerPaneOut = true + var toLeft = false + if let index = self.paneArrangement.panes.index(of: .stickers), index < self.paneArrangement.currentIndex { + toLeft = true } - }) + transition.animatePosition(node: self.stickerPane, to: CGPoint(x: (toLeft ? -width : width) + width / 2.0, y: self.stickerPane.layer.position.y), removeOnCompletion: false, completion: { [weak self] value in + if let strongSelf = self, value { + strongSelf.animatingStickerPaneOut = false + strongSelf.stickerPane.removeFromSupernode() + } + }) + } } else { + self.animatingStickerPaneOut = false self.stickerPane.removeFromSupernode() } } + } else { + self.animatingStickerPaneOut = false } return panelHeight @@ -735,7 +780,7 @@ final class ChatMediaInputNode: ChatInputNode { case .began: break case .changed: - if let (width, interfaceState) = self.validLayout { + if let (width, leftInset, rightInset, bottomInset, interfaceState) = self.validLayout { let translationX = -recognizer.translation(in: self.view).x var indexTransition = translationX / width if self.paneArrangement.currentIndex == 0 { @@ -744,10 +789,10 @@ final class ChatMediaInputNode: ChatInputNode { indexTransition = min(0.0, indexTransition) } self.paneArrangement = self.paneArrangement.withIndexTransition(indexTransition) - let _ = self.updateLayout(width: width, transition: .immediate, interfaceState: interfaceState) + let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, transition: .immediate, interfaceState: interfaceState) } case .ended: - if let (width, _) = self.validLayout { + if let (width, _, _, _, _) = self.validLayout { var updatedIndex = self.paneArrangement.currentIndex if abs(self.paneArrangement.indexTransition * width) > 30.0 { if self.paneArrangement.indexTransition < 0.0 { @@ -756,12 +801,13 @@ final class ChatMediaInputNode: ChatInputNode { updatedIndex = min(self.paneArrangement.panes.count - 1, self.paneArrangement.currentIndex + 1) } } + self.paneArrangement = self.paneArrangement.withIndexTransition(0.0) self.setCurrentPane(self.paneArrangement.panes[updatedIndex], transition: .animated(duration: 0.25, curve: .spring)) } case .cancelled: - if let (width, interfaceState) = self.validLayout { + if let (width, leftInset, rightInset, bottomInset, interfaceState) = self.validLayout { self.paneArrangement = self.paneArrangement.withIndexTransition(0.0) - let _ = self.updateLayout(width: width, transition: .animated(duration: 0.25, curve: .spring), interfaceState: interfaceState) + let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, transition: .animated(duration: 0.25, curve: .spring), interfaceState: interfaceState) } default: break diff --git a/TelegramUI/ChatMediaInputPanelEntries.swift b/TelegramUI/ChatMediaInputPanelEntries.swift index 6b916edeff..d9a3a60082 100644 --- a/TelegramUI/ChatMediaInputPanelEntries.swift +++ b/TelegramUI/ChatMediaInputPanelEntries.swift @@ -8,6 +8,7 @@ enum ChatMediaInputPanelAuxiliaryNamespace: Int32 { case savedStickers = 2 case recentStickers = 4 case trending = 5 + case settings = 6 } enum ChatMediaInputPanelEntryStableId: Hashable { @@ -15,6 +16,8 @@ enum ChatMediaInputPanelEntryStableId: Hashable { case savedStickers case recentPacks case stickerPack(Int64) + case trending + case settings static func ==(lhs: ChatMediaInputPanelEntryStableId, rhs: ChatMediaInputPanelEntryStableId) -> Bool { switch lhs { @@ -42,6 +45,18 @@ enum ChatMediaInputPanelEntryStableId: Hashable { } else { return false } + case .trending: + if case .trending = rhs { + return true + } else { + return false + } + case .settings: + if case .settings = rhs { + return true + } else { + return false + } } } @@ -53,6 +68,10 @@ enum ChatMediaInputPanelEntryStableId: Hashable { return 1 case .recentPacks: return 2 + case .trending: + return 2 + case .settings: + return 2 case let .stickerPack(id): return id.hashValue } @@ -63,6 +82,8 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable { case recentGifs(PresentationTheme) case savedStickers(PresentationTheme) case recentPacks(PresentationTheme) + case trending(Bool, PresentationTheme) + case settings(PresentationTheme) case stickerPack(index: Int, info: StickerPackCollectionInfo, topItem: StickerPackItem?, theme: PresentationTheme) var stableId: ChatMediaInputPanelEntryStableId { @@ -73,6 +94,10 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable { return .savedStickers case .recentPacks: return .recentPacks + case .trending: + return .trending + case .settings: + return .settings case let .stickerPack(_, info, _, _): return .stickerPack(info.id.id) } @@ -98,6 +123,18 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable { } else { return false } + case let .trending(lhsElevated, lhsTheme): + if case let .trending(rhsElevated, rhsTheme) = rhs, lhsTheme === rhsTheme, lhsElevated == rhsElevated { + return true + } else { + return false + } + case let .settings(lhsTheme): + if case let .settings(rhsTheme) = rhs, lhsTheme === rhsTheme { + return true + } else { + return false + } case let .stickerPack(index, info, topItem, lhsTheme): if case let .stickerPack(rhsIndex, rhsInfo, rhsTopItem, rhsTheme) = rhs, index == rhsIndex, info == rhsInfo, topItem == rhsTopItem, lhsTheme === rhsTheme { return true @@ -120,6 +157,8 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable { switch rhs { case .recentGifs, savedStickers: return false + case let .trending(elevated, _) where elevated: + return false default: return true } @@ -127,6 +166,8 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable { switch rhs { case .recentGifs, .savedStickers, recentPacks: return false + case let .trending(elevated, _) where elevated: + return false default: return true } @@ -134,6 +175,14 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable { switch rhs { case .recentGifs, .savedStickers, .recentPacks: return false + case let .trending(elevated, _): + if elevated { + return false + } else { + return true + } + case .settings: + return true case let .stickerPack(rhsIndex, rhsInfo, _, _): if lhsIndex == rhsIndex { return lhsInfo.id.id < rhsInfo.id.id @@ -141,6 +190,23 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable { return lhsIndex <= rhsIndex } } + case let .trending(elevated, _): + if elevated { + switch rhs { + case .recentGifs, .trending: + return false + default: + return true + } + } else { + if case .settings = rhs { + return true + } else { + return false + } + } + case .settings: + return false } } @@ -161,6 +227,15 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable { let collectionId = ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.recentStickers.rawValue, id: 0) inputNodeInteraction.navigateToCollectionId(collectionId) }) + case let .trending(_, theme): + return ChatMediaInputTrendingItem(inputNodeInteraction: inputNodeInteraction, theme: theme, selected: { + let collectionId = ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.trending.rawValue, id: 0) + inputNodeInteraction.navigateToCollectionId(collectionId) + }) + case let .settings(theme): + return ChatMediaInputSettingsItem(inputNodeInteraction: inputNodeInteraction, theme: theme, selected: { + inputNodeInteraction.openSettings() + }) case let .stickerPack(index, info, topItem, theme): return ChatMediaInputStickerPackItem(account: account, inputNodeInteraction: inputNodeInteraction, collectionId: info.id, stickerPackItem: topItem, index: index, theme: theme, selected: { inputNodeInteraction.navigateToCollectionId(info.id) diff --git a/TelegramUI/ChatMediaInputRecentGifsItem.swift b/TelegramUI/ChatMediaInputRecentGifsItem.swift index f717c021d0..4dbedcef75 100644 --- a/TelegramUI/ChatMediaInputRecentGifsItem.swift +++ b/TelegramUI/ChatMediaInputRecentGifsItem.swift @@ -20,7 +20,7 @@ final class ChatMediaInputRecentGifsItem: ListViewItem { self.theme = theme } - func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { async { let node = ChatMediaInputRecentGifsItemNode() node.contentSize = CGSize(width: 41.0, height: 41.0) @@ -33,7 +33,7 @@ final class ChatMediaInputRecentGifsItem: ListViewItem { } } - public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { completion(ListViewItemNodeLayout(contentSize: node.contentSize, insets: node.insets), { (node as? ChatMediaInputRecentGifsItemNode)?.updateTheme(theme: self.theme) }) @@ -49,8 +49,6 @@ private let boundingImageSize = CGSize(width: 30.0, height: 30.0) private let highlightSize = CGSize(width: 35.0, height: 35.0) private let verticalOffset: CGFloat = 3.0 + UIScreenPixel -private let highlightBackground = generateStretchableFilledCircleImage(radius: 9.0, color: UIColor(rgb: 0x9099A2, alpha: 0.2)) - final class ChatMediaInputRecentGifsItemNode: ListViewItemNode { private let imageNode: ASImageNode private let highlightNode: ASImageNode diff --git a/TelegramUI/ChatMediaInputSettingsItem.swift b/TelegramUI/ChatMediaInputSettingsItem.swift new file mode 100644 index 0000000000..99fea7e2ff --- /dev/null +++ b/TelegramUI/ChatMediaInputSettingsItem.swift @@ -0,0 +1,91 @@ +import Foundation +import Display +import AsyncDisplayKit +import TelegramCore +import SwiftSignalKit +import Postbox + +final class ChatMediaInputSettingsItem: ListViewItem { + let inputNodeInteraction: ChatMediaInputNodeInteraction + let selectedItem: () -> Void + let theme: PresentationTheme + + var selectable: Bool { + return true + } + + init(inputNodeInteraction: ChatMediaInputNodeInteraction, theme: PresentationTheme, selected: @escaping () -> Void) { + self.inputNodeInteraction = inputNodeInteraction + self.selectedItem = selected + self.theme = theme + } + + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + async { + let node = ChatMediaInputSettingsItemNode() + node.contentSize = CGSize(width: 41.0, height: 41.0) + node.insets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0) + node.inputNodeInteraction = self.inputNodeInteraction + node.updateTheme(theme: self.theme) + completion(node, { + return (nil, {}) + }) + } + } + + public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + completion(ListViewItemNodeLayout(contentSize: node.contentSize, insets: node.insets), { + (node as? ChatMediaInputSettingsItemNode)?.updateTheme(theme: self.theme) + }) + } + + func selected(listView: ListView) { + self.selectedItem() + } +} + +private let boundingSize = CGSize(width: 41.0, height: 41.0) +private let boundingImageSize = CGSize(width: 30.0, height: 30.0) +private let highlightSize = CGSize(width: 35.0, height: 35.0) +private let verticalOffset: CGFloat = 3.0 + UIScreenPixel + +final class ChatMediaInputSettingsItemNode: ListViewItemNode { + private let buttonNode: HighlightableButtonNode + private let imageNode: ASImageNode + + var currentCollectionId: ItemCollectionId? + var inputNodeInteraction: ChatMediaInputNodeInteraction? + + var theme: PresentationTheme? + + init() { + self.buttonNode = HighlightableButtonNode() + + self.imageNode = ASImageNode() + self.imageNode.isLayerBacked = true + + self.buttonNode.frame = CGRect(origin: CGPoint(), size: boundingSize) + + self.imageNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) + + super.init(layerBacked: false, dynamicBounce: false) + + self.addSubnode(self.buttonNode) + self.buttonNode.addSubnode(self.imageNode) + + let imageSize = CGSize(width: 26.0, height: 26.0) + self.imageNode.frame = CGRect(origin: CGPoint(x: floor((boundingSize.width - imageSize.width) / 2.0) + verticalOffset, y: floor((boundingSize.height - imageSize.height) / 2.0) + UIScreenPixel), size: imageSize) + } + + deinit { + } + + func updateTheme(theme: PresentationTheme) { + if self.theme !== theme { + self.theme = theme + + self.imageNode.image = PresentationResourcesChat.chatInputMediaPanelSettingsIconImage(theme) + } + } +} + diff --git a/TelegramUI/ChatMediaInputStickerGridItem.swift b/TelegramUI/ChatMediaInputStickerGridItem.swift index 691c6186eb..b2b96d1ac8 100644 --- a/TelegramUI/ChatMediaInputStickerGridItem.swift +++ b/TelegramUI/ChatMediaInputStickerGridItem.swift @@ -57,7 +57,7 @@ final class ChatMediaInputStickerGridSectionNode: ASDisplayNode { let bounds = self.bounds let titleSize = self.titleNode.measure(CGSize(width: bounds.size.width - 24.0, height: CGFloat.greatestFiniteMagnitude)) - self.titleNode.frame = CGRect(origin: CGPoint(x: 12.0, y: 8.0), size: titleSize) + self.titleNode.frame = CGRect(origin: CGPoint(x: 12.0, y: 9.0), size: titleSize) } } @@ -139,7 +139,7 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode { func setup(account: Account, stickerItem: StickerPackItem) { if self.currentState == nil || self.currentState!.0 !== account || self.currentState!.1 != stickerItem { if let dimensions = stickerItem.file.dimensions { - self.imageNode.setSignal(account: account, signal: chatMessageSticker(account: account, file: stickerItem.file, small: true)) + self.imageNode.setSignal(chatMessageSticker(account: account, file: stickerItem.file, small: true)) self.stickerFetchedDisposable.set(freeMediaFileInteractiveFetched(account: account, file: stickerItem.file).start()) self.currentState = (account, stickerItem, dimensions) diff --git a/TelegramUI/ChatMediaInputStickerPackItem.swift b/TelegramUI/ChatMediaInputStickerPackItem.swift index 0b9433b1d4..93d5503926 100644 --- a/TelegramUI/ChatMediaInputStickerPackItem.swift +++ b/TelegramUI/ChatMediaInputStickerPackItem.swift @@ -28,7 +28,7 @@ final class ChatMediaInputStickerPackItem: ListViewItem { self.theme = theme } - func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { async { let node = ChatMediaInputStickerPackItemNode() node.contentSize = CGSize(width: 41.0, height: 41.0) @@ -41,7 +41,7 @@ final class ChatMediaInputStickerPackItem: ListViewItem { } } - public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { completion(ListViewItemNodeLayout(contentSize: node.contentSize, insets: node.insets), { (node as? ChatMediaInputStickerPackItemNode)?.updateStickerPackItem(account: self.account, item: self.stickerPackItem, collectionId: self.collectionId, theme: self.theme) }) @@ -79,7 +79,7 @@ final class ChatMediaInputStickerPackItemNode: ListViewItemNode { self.highlightNode.frame = CGRect(origin: CGPoint(x: floor((boundingSize.width - highlightSize.width) / 2.0) + verticalOffset, y: floor((boundingSize.height - highlightSize.height) / 2.0)), size: highlightSize) self.imageNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) - self.imageNode.alphaTransitionOnFirstUpdate = true + self.imageNode.contentAnimations = [.firstUpdate] super.init(layerBacked: false, dynamicBounce: false) @@ -107,7 +107,7 @@ final class ChatMediaInputStickerPackItemNode: ListViewItemNode { let imageSize = dimensions.aspectFitted(boundingImageSize) let imageApply = self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: boundingImageSize, intrinsicInsets: UIEdgeInsets())) imageApply() - self.imageNode.setSignal(account: account, signal: chatMessageSticker(account: account, file: item.file, small: true)) + self.imageNode.setSignal(chatMessageSticker(account: account, file: item.file, small: true)) self.stickerFetchedDisposable.set(freeMediaFileInteractiveFetched(account: account, file: item.file).start()) self.imageNode.frame = CGRect(origin: CGPoint(x: floor((boundingSize.width - imageSize.width) / 2.0) + verticalOffset, y: floor((boundingSize.height - imageSize.height) / 2.0)), size: imageSize) } diff --git a/TelegramUI/ChatMediaInputStickerPane.swift b/TelegramUI/ChatMediaInputStickerPane.swift index bb67591c34..1c1495cea9 100644 --- a/TelegramUI/ChatMediaInputStickerPane.swift +++ b/TelegramUI/ChatMediaInputStickerPane.swift @@ -16,8 +16,8 @@ final class ChatMediaInputStickerPane: ASDisplayNode { self.addSubnode(self.gridNode) } - func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { - self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: size, insets: UIEdgeInsets(), preloadSize: 300.0, type: .fixed(itemSize: CGSize(width: 75.0, height: 75.0), lineSpacing: 0.0)), transition: .immediate), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) + func updateLayout(size: CGSize, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) { + self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: size, insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: bottomInset, right: 0.0), preloadSize: 300.0, type: .fixed(itemSize: CGSize(width: 75.0, height: 75.0), lineSpacing: 0.0)), transition: .immediate), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) self.gridNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height)) } diff --git a/TelegramUI/ChatMediaInputTrendingItem.swift b/TelegramUI/ChatMediaInputTrendingItem.swift index c1bcb5f7b8..9c586f8e71 100644 --- a/TelegramUI/ChatMediaInputTrendingItem.swift +++ b/TelegramUI/ChatMediaInputTrendingItem.swift @@ -5,49 +5,37 @@ import TelegramCore import SwiftSignalKit import Postbox -private let iconImage = generateImage(CGSize(width: 26.0, height: 26.0), rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setStrokeColor(UIColor(rgb: 0x9099A2).cgColor) - context.setLineWidth(2.0) - context.setLineCap(.round) - let diameter: CGFloat = 22.0 - context.strokeEllipse(in: CGRect(origin: CGPoint(x: floor((size.width - diameter) / 2.0), y: floor((size.width - diameter) / 2.0)), size: CGSize(width: diameter, height: diameter))) - context.setFillColor(UIColor(rgb: 0x9099A2).cgColor) - UIGraphicsPushContext(context) - - context.setTextDrawingMode(.stroke) - context.setLineWidth(0.65) - - UIGraphicsPopContext() -}) - final class ChatMediaInputTrendingItem: ListViewItem { let inputNodeInteraction: ChatMediaInputNodeInteraction let selectedItem: () -> Void + let theme: PresentationTheme var selectable: Bool { return true } - init(inputNodeInteraction: ChatMediaInputNodeInteraction, selected: @escaping () -> Void) { + init(inputNodeInteraction: ChatMediaInputNodeInteraction, theme: PresentationTheme, selected: @escaping () -> Void) { self.inputNodeInteraction = inputNodeInteraction self.selectedItem = selected + self.theme = theme } - func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { async { let node = ChatMediaInputTrendingItemNode() node.contentSize = CGSize(width: 41.0, height: 41.0) node.insets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0) node.inputNodeInteraction = self.inputNodeInteraction + node.updateTheme(theme: self.theme) completion(node, { return (nil, {}) }) } } - public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { completion(ListViewItemNodeLayout(contentSize: node.contentSize, insets: node.insets), { + (node as? ChatMediaInputTrendingItemNode)?.updateTheme(theme: self.theme) }) } @@ -61,8 +49,6 @@ private let boundingImageSize = CGSize(width: 30.0, height: 30.0) private let highlightSize = CGSize(width: 35.0, height: 35.0) private let verticalOffset: CGFloat = 3.0 + UIScreenPixel -private let highlightBackground = generateStretchableFilledCircleImage(radius: 9.0, color: UIColor(rgb: 0x9099A2, alpha: 0.2)) - final class ChatMediaInputTrendingItemNode: ListViewItemNode { private let imageNode: ASImageNode private let highlightNode: ASImageNode @@ -70,10 +56,11 @@ final class ChatMediaInputTrendingItemNode: ListViewItemNode { var currentCollectionId: ItemCollectionId? var inputNodeInteraction: ChatMediaInputNodeInteraction? + var theme: PresentationTheme? + init() { self.highlightNode = ASImageNode() self.highlightNode.isLayerBacked = true - self.highlightNode.image = highlightBackground self.highlightNode.isHidden = true self.imageNode = ASImageNode() @@ -81,7 +68,6 @@ final class ChatMediaInputTrendingItemNode: ListViewItemNode { self.highlightNode.frame = CGRect(origin: CGPoint(x: floor((boundingSize.width - highlightSize.width) / 2.0) + verticalOffset, y: floor((boundingSize.height - highlightSize.height) / 2.0)), size: highlightSize) - self.imageNode.image = iconImage self.imageNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) super.init(layerBacked: false, dynamicBounce: false) @@ -98,9 +84,13 @@ final class ChatMediaInputTrendingItemNode: ListViewItemNode { deinit { } - func updateStickerPackItem(account: Account, item: StickerPackItem?, collectionId: ItemCollectionId) { - self.currentCollectionId = collectionId - self.updateIsHighlighted() + func updateTheme(theme: PresentationTheme) { + if self.theme !== theme { + self.theme = theme + + self.highlightNode.image = PresentationResourcesChat.chatMediaInputPanelHighlightedIconImage(theme) + self.imageNode.image = PresentationResourcesChat.chatInputMediaPanelTrendingIconImage(theme) + } } func updateIsHighlighted() { @@ -109,3 +99,4 @@ final class ChatMediaInputTrendingItemNode: ListViewItemNode { } } } + diff --git a/TelegramUI/ChatMessageActionButtonsNode.swift b/TelegramUI/ChatMessageActionButtonsNode.swift index 3bc2aae7dc..2eb3c62b7a 100644 --- a/TelegramUI/ChatMessageActionButtonsNode.swift +++ b/TelegramUI/ChatMessageActionButtonsNode.swift @@ -19,7 +19,7 @@ private final class ChatMessageActionButtonNode: ASDisplayNode { self.backgroundNode.displayWithoutProcessing = true self.backgroundNode.displaysAsynchronously = false self.backgroundNode.isLayerBacked = true - self.backgroundNode.alpha = 0.35 + self.backgroundNode.alpha = 1.0 self.backgroundNode.isUserInteractionEnabled = false super.init() @@ -40,8 +40,8 @@ private final class ChatMessageActionButtonNode: ASDisplayNode { strongSelf.backgroundNode.layer.removeAnimation(forKey: "opacity") strongSelf.backgroundNode.alpha = 0.55 } else { - strongSelf.backgroundNode.alpha = 0.35 - strongSelf.backgroundNode.layer.animateAlpha(from: 0.55, to: 0.35, duration: 0.2) + strongSelf.backgroundNode.alpha = 1.0 + strongSelf.backgroundNode.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2) } } } @@ -53,13 +53,15 @@ private final class ChatMessageActionButtonNode: ASDisplayNode { } } - class func asyncLayout(_ maybeNode: ChatMessageActionButtonNode?) -> (_ theme: PresentationTheme, _ strings: PresentationStrings, _ message: Message, _ button: ReplyMarkupButton, _ constrainedWidth: CGFloat, _ position: MessageBubbleActionButtonPosition) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, () -> ChatMessageActionButtonNode))) { + class func asyncLayout(_ maybeNode: ChatMessageActionButtonNode?) -> (_ account: Account, _ theme: PresentationTheme, _ strings: PresentationStrings, _ message: Message, _ button: ReplyMarkupButton, _ constrainedWidth: CGFloat, _ position: MessageBubbleActionButtonPosition) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, () -> ChatMessageActionButtonNode))) { let titleLayout = TextNode.asyncLayout(maybeNode?.titleNode) - return { theme, strings, message, button, constrainedWidth, position in + return { account, theme, strings, message, button, constrainedWidth, position in let sideInset: CGFloat = 8.0 let minimumSideInset: CGFloat = 4.0 + let incoming = message.effectivelyIncoming(account.peerId) + var title = button.title if case .payment = button.action { for media in message.media { @@ -71,18 +73,18 @@ private final class ChatMessageActionButtonNode: ASDisplayNode { } } - let (titleSize, titleApply) = titleLayout(NSAttributedString(string: title, font: titleFont, textColor: theme.chat.bubble.actionButtonsTextColor), nil, 1, .end, CGSize(width: max(1.0, constrainedWidth - minimumSideInset - minimumSideInset), height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let (titleSize, titleApply) = titleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: titleFont, textColor: incoming ? theme.chat.bubble.actionButtonsIncomingTextColor : theme.chat.bubble.actionButtonsOutgoingTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(1.0, constrainedWidth - minimumSideInset - minimumSideInset), height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) let backgroundImage: UIImage? switch position { case .middle: - backgroundImage = PresentationResourcesChat.chatBubbleActionButtonMiddleImage(theme) + backgroundImage = incoming ? PresentationResourcesChat.chatBubbleActionButtonIncomingMiddleImage(theme) : PresentationResourcesChat.chatBubbleActionButtonOutgoingMiddleImage(theme) case .bottomLeft: - backgroundImage = PresentationResourcesChat.chatBubbleActionButtonBottomLeftImage(theme) + backgroundImage = incoming ? PresentationResourcesChat.chatBubbleActionButtonIncomingBottomLeftImage(theme) : PresentationResourcesChat.chatBubbleActionButtonOutgoingBottomLeftImage(theme) case .bottomRight: - backgroundImage = PresentationResourcesChat.chatBubbleActionButtonBottomRightImage(theme) + backgroundImage = incoming ? PresentationResourcesChat.chatBubbleActionButtonIncomingBottomRightImage(theme) : PresentationResourcesChat.chatBubbleActionButtonOutgoingBottomRightImage(theme) case .bottomSingle: - backgroundImage = PresentationResourcesChat.chatBubbleActionButtonBottomSingleImage(theme) + backgroundImage = incoming ? PresentationResourcesChat.chatBubbleActionButtonIncomingBottomSingleImage(theme) : PresentationResourcesChat.chatBubbleActionButtonOutgoingBottomSingleImage(theme) } return (titleSize.size.width + sideInset + sideInset, { width in @@ -132,10 +134,10 @@ final class ChatMessageActionButtonsNode: ASDisplayNode { } } - class func asyncLayout(_ maybeNode: ChatMessageActionButtonsNode?) -> (_ theme: PresentationTheme, _ strings: PresentationStrings, _ replyMarkup: ReplyMarkupMessageAttribute, _ message: Message, _ constrainedWidth: CGFloat) -> (minWidth: CGFloat, layout: (CGFloat) -> (CGSize, (_ animated: Bool) -> ChatMessageActionButtonsNode)) { + class func asyncLayout(_ maybeNode: ChatMessageActionButtonsNode?) -> (_ account: Account, _ theme: PresentationTheme, _ strings: PresentationStrings, _ replyMarkup: ReplyMarkupMessageAttribute, _ message: Message, _ constrainedWidth: CGFloat) -> (minWidth: CGFloat, layout: (CGFloat) -> (CGSize, (_ animated: Bool) -> ChatMessageActionButtonsNode)) { let currentButtonLayouts = maybeNode?.buttonNodes.map { ChatMessageActionButtonNode.asyncLayout($0) } ?? [] - return { theme, strings, replyMarkup, message, constrainedWidth in + return { account, theme, strings, replyMarkup, message, constrainedWidth in let buttonHeight: CGFloat = 42.0 let buttonSpacing: CGFloat = 4.0 @@ -168,9 +170,9 @@ final class ChatMessageActionButtonsNode: ASDisplayNode { let prepareButtonLayout: (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, () -> ChatMessageActionButtonNode))) if buttonIndex < currentButtonLayouts.count { - prepareButtonLayout = currentButtonLayouts[buttonIndex](theme, strings, message, button, maximumButtonWidth, buttonPosition) + prepareButtonLayout = currentButtonLayouts[buttonIndex](account, theme, strings, message, button, maximumButtonWidth, buttonPosition) } else { - prepareButtonLayout = ChatMessageActionButtonNode.asyncLayout(nil)(theme, strings, message, button, maximumButtonWidth, buttonPosition) + prepareButtonLayout = ChatMessageActionButtonNode.asyncLayout(nil)(account, theme, strings, message, button, maximumButtonWidth, buttonPosition) } maximumRowButtonWidth = max(maximumRowButtonWidth, prepareButtonLayout.minimumWidth) diff --git a/TelegramUI/ChatMessageActionItemNode.swift b/TelegramUI/ChatMessageActionItemNode.swift index 78884205e0..dd4726fc6f 100644 --- a/TelegramUI/ChatMessageActionItemNode.swift +++ b/TelegramUI/ChatMessageActionItemNode.swift @@ -49,11 +49,11 @@ private func universalServiceMessageString(theme: PresentationTheme?, strings: P } switch action.action { - case .groupCreated: + case let .groupCreated(title): if isChannel { attributedString = NSAttributedString(string: strings.Notification_CreatedChannel, font: titleFont, textColor: primaryTextColor) } else { - attributedString = NSAttributedString(string: strings.Notification_CreatedGroup, font: titleFont, textColor: primaryTextColor) + attributedString = addAttributesToStringWithRanges(strings.Notification_CreatedChatWithTitle(authorName, title), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, message.author?.id)])) } case let .addedMembers(peerIds): if let peerId = peerIds.first, peerId == message.author?.id { @@ -99,11 +99,7 @@ private func universalServiceMessageString(theme: PresentationTheme?, strings: P } case let .titleUpdated(title): if authorName.isEmpty || isChannel { - if isChannel { - attributedString = NSAttributedString(string: strings.Channel_MessageTitleUpdated(title).0, font: titleFont, textColor: primaryTextColor) - } else { - attributedString = NSAttributedString(string: strings.Group_MessageTitleUpdated(title).0, font: titleFont, textColor: primaryTextColor) - } + attributedString = NSAttributedString(string: strings.Channel_MessageTitleUpdated(title).0, font: titleFont, textColor: primaryTextColor) } else { attributedString = addAttributesToStringWithRanges(strings.Notification_ChangedGroupName(authorName, title), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, message.author?.id)])) } @@ -341,9 +337,27 @@ private func universalServiceMessageString(theme: PresentationTheme?, strings: P } else { attributedString = NSAttributedString(string: strings.Message_PaymentSent(formatCurrencyAmount(totalAmount, currency: currency)).0, font: titleFont, textColor: primaryTextColor) } - case .phoneCall: - break - default: + case let .phoneCall(_, discardReason, _): + var titleString: String + if message.flags.contains(.Incoming) { + titleString = strings.Notification_CallIncoming + } else { + titleString = strings.Notification_CallOutgoing + } + if let discardReason = discardReason { + switch discardReason { + case .busy, .disconnect: + titleString = strings.Notification_CallCanceled + case .missed: + titleString = strings.Notification_CallMissed + case .hangup: + break + } + } + attributedString = NSAttributedString(string: titleString, font: titleFont, textColor: primaryTextColor) + case let .customText(text): + attributedString = NSAttributedString(string: text, font: titleFont, textColor: primaryTextColor) + case .unknown: attributedString = nil } @@ -406,16 +420,16 @@ class ChatMessageActionItemNode: ChatMessageItemView { self.view.addGestureRecognizer(recognizer) } - override func asyncLayout() -> (_ item: ChatMessageItem, _ width: CGFloat, _ mergedTop: Bool, _ mergedBottom: Bool, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { + override func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { let makeLabelLayout = TextNode.asyncLayout(self.labelNode) let layoutConstants = self.layoutConstants let backgroundLayout = self.filledBackgroundNode.asyncLayout() - return { item, width, mergedTop, mergedBottom, dateHeaderAtBottom in - let attributedString = attributedServiceMessageString(theme: item.theme, strings: item.strings, message: item.message, accountPeerId: item.account.peerId) + return { item, params, mergedTop, mergedBottom, dateHeaderAtBottom in + let attributedString = attributedServiceMessageString(theme: item.presentationData.theme, strings: item.presentationData.strings, message: item.message, accountPeerId: item.account.peerId) - let (labelLayout, apply) = makeLabelLayout(attributedString, nil, 0, .end, CGSize(width: width - 32.0, height: CGFloat.greatestFiniteMagnitude), .center, nil, UIEdgeInsets()) + let (labelLayout, apply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: attributedString, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) var labelRects = labelLayout.linesRects() if labelRects.count > 1 { @@ -447,7 +461,7 @@ class ChatMessageActionItemNode: ChatMessageItemView { labelRects[i].origin.x = floor((labelLayout.size.width - labelRects[i].width) / 2.0) } - let backgroundApply = backgroundLayout(item.theme.chat.serviceMessage.serviceMessageFillColor, labelRects, 10.0, 10.0, 0.0) + let backgroundApply = backgroundLayout(item.presentationData.theme.chat.serviceMessage.serviceMessageFillColor, labelRects, 10.0, 10.0, 0.0) let backgroundSize = CGSize(width: labelLayout.size.width + 8.0 + 8.0, height: labelLayout.size.height + 4.0) var layoutInsets = UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0) @@ -455,14 +469,14 @@ class ChatMessageActionItemNode: ChatMessageItemView { layoutInsets.top += layoutConstants.timestampHeaderHeight } - return (ListViewItemNodeLayout(contentSize: CGSize(width: width, height: labelLayout.size.height + 4.0), insets: layoutInsets), { [weak self] animation in + return (ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: labelLayout.size.height + 4.0), insets: layoutInsets), { [weak self] animation in if let strongSelf = self { strongSelf.appliedItem = item let _ = apply() let _ = backgroundApply() - let labelFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((width - labelLayout.size.width) / 2.0), y: floorToScreenPixels((backgroundSize.height - labelLayout.size.height) / 2.0) - 1.0), size: labelLayout.size) + let labelFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((params.width - labelLayout.size.width) / 2.0), y: floorToScreenPixels((backgroundSize.height - labelLayout.size.height) / 2.0) - 1.0), size: labelLayout.size) strongSelf.labelNode.frame = labelFrame strongSelf.filledBackgroundNode.frame = labelFrame.offsetBy(dx: 0.0, dy: -11.0) } @@ -503,47 +517,37 @@ class ChatMessageActionItemNode: ChatMessageItemView { break case let .url(url): foundTapAction = true - if let controllerInteraction = self.controllerInteraction { - controllerInteraction.openUrl(url) - } + self.item?.controllerInteraction.openUrl(url) case let .peerMention(peerId, _): foundTapAction = true - if let controllerInteraction = self.controllerInteraction { - controllerInteraction.openPeer(peerId, .chat(textInputState: nil), nil) - } + self.item?.controllerInteraction.openPeer(peerId, .info, nil) case let .textMention(name): foundTapAction = true - if let controllerInteraction = self.controllerInteraction { - controllerInteraction.openPeerMention(name) - } + self.item?.controllerInteraction.openPeerMention(name) case let .botCommand(command): foundTapAction = true - if let item = self.item, let controllerInteraction = self.controllerInteraction { - controllerInteraction.sendBotCommand(item.message.id, command) + if let item = self.item { + item.controllerInteraction.sendBotCommand(item.message.id, command) } case let .hashtag(peerName, hashtag): foundTapAction = true - if let controllerInteraction = self.controllerInteraction { - controllerInteraction.openHashtag(peerName, hashtag) - } + self.item?.controllerInteraction.openHashtag(peerName, hashtag) case .instantPage: foundTapAction = true - if let item = self.item, let controllerInteraction = self.controllerInteraction { - controllerInteraction.openInstantPage(item.message.id) + if let item = self.item { + item.controllerInteraction.openInstantPage(item.message.id) } case .holdToPreviewSecretMedia: foundTapAction = true case let .call(peerId): foundTapAction = true - if let controllerInteraction = self.controllerInteraction { - controllerInteraction.callPeer(peerId) - } + self.item?.controllerInteraction.callPeer(peerId) } if !foundTapAction { if let item = self.item { for attribute in item.message.attributes { if let attribute = attribute as? ReplyMessageAttribute { - self.controllerInteraction?.navigateToMessage(item.message.id, attribute.messageId) + item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId) foundTapAction = true break } @@ -551,7 +555,7 @@ class ChatMessageActionItemNode: ChatMessageItemView { } } if !foundTapAction { - self.controllerInteraction?.clickThroughMessage() + self.item?.controllerInteraction.clickThroughMessage() } case .longTap, .doubleTap: if let item = self.item, self.labelNode.frame.contains(location) { @@ -562,29 +566,19 @@ class ChatMessageActionItemNode: ChatMessageItemView { break case let .url(url): foundTapAction = true - if let controllerInteraction = self.controllerInteraction { - controllerInteraction.longTap(.url(url)) - } + item.controllerInteraction.longTap(.url(url)) case let .peerMention(peerId, mention): foundTapAction = true - if let controllerInteraction = self.controllerInteraction { - controllerInteraction.longTap(.peerMention(peerId, mention)) - } + item.controllerInteraction.longTap(.peerMention(peerId, mention)) case let .textMention(name): foundTapAction = true - if let controllerInteraction = self.controllerInteraction { - controllerInteraction.longTap(.mention(name)) - } + item.controllerInteraction.longTap(.mention(name)) case let .botCommand(command): foundTapAction = true - if let _ = self.item, let controllerInteraction = self.controllerInteraction { - controllerInteraction.longTap(.command(command)) - } + item.controllerInteraction.longTap(.command(command)) case let .hashtag(_, hashtag): foundTapAction = true - if let controllerInteraction = self.controllerInteraction { - controllerInteraction.longTap(.hashtag(hashtag)) - } + item.controllerInteraction.longTap(.hashtag(hashtag)) case .instantPage: break case .holdToPreviewSecretMedia: @@ -594,7 +588,7 @@ class ChatMessageActionItemNode: ChatMessageItemView { } if !foundTapAction { - self.controllerInteraction?.openMessageContextMenu(item.message.id, self, self.filledBackgroundNode.frame) + item.controllerInteraction.openMessageContextMenu(item.message.id, self, self.filledBackgroundNode.frame) } } case .hold: @@ -643,7 +637,7 @@ class ChatMessageActionItemNode: ChatMessageItemView { if let current = self.linkHighlightingNode { linkHighlightingNode = current } else { - linkHighlightingNode = LinkHighlightingNode(color: item.theme.chat.serviceMessage.serviceMessageLinkHighlightColor) + linkHighlightingNode = LinkHighlightingNode(color: item.presentationData.theme.chat.serviceMessage.serviceMessageLinkHighlightColor) linkHighlightingNode.inset = 2.5 self.linkHighlightingNode = linkHighlightingNode self.insertSubnode(linkHighlightingNode, belowSubnode: self.labelNode) diff --git a/TelegramUI/ChatMessageAttachedContentNode.swift b/TelegramUI/ChatMessageAttachedContentNode.swift index b7485f53c7..ab82fe01be 100644 --- a/TelegramUI/ChatMessageAttachedContentNode.swift +++ b/TelegramUI/ChatMessageAttachedContentNode.swift @@ -101,7 +101,7 @@ private final class ChatMessageAttachedContentButtonNode: HighlightTrackingButto let maybeMakeTextLayout = (current?.textNode).flatMap(TextNode.asyncLayout) let maybeMakeHighlightedTextLayout = (current?.highlightedTextNode).flatMap(TextNode.asyncLayout) - return { width, regularImage, highlightedImage, iconImage, highlightedIconImage, title, titleColor, highlightedTitleColor in + return { width, regularImage, highlightedImage, iconImage, highlightedIconImage, title, titleColor, highlightedTitleColor in let targetNode: ChatMessageAttachedContentButtonNode if let current = current { targetNode = current @@ -109,14 +109,14 @@ private final class ChatMessageAttachedContentButtonNode: HighlightTrackingButto targetNode = ChatMessageAttachedContentButtonNode() } - let makeTextLayout: (NSAttributedString?, UIColor?, Int, CTLineTruncationType, CGSize, NSTextAlignment, TextNodeCutout?, UIEdgeInsets) -> (TextNodeLayout, () -> TextNode) + let makeTextLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode) if let maybeMakeTextLayout = maybeMakeTextLayout { makeTextLayout = maybeMakeTextLayout } else { makeTextLayout = TextNode.asyncLayout(targetNode.textNode) } - let makeHighlightedTextLayout: (NSAttributedString?, UIColor?, Int, CTLineTruncationType, CGSize, NSTextAlignment, TextNodeCutout?, UIEdgeInsets) -> (TextNodeLayout, () -> TextNode) + let makeHighlightedTextLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode) if let maybeMakeHighlightedTextLayout = maybeMakeHighlightedTextLayout { makeHighlightedTextLayout = maybeMakeHighlightedTextLayout } else { @@ -150,9 +150,9 @@ private final class ChatMessageAttachedContentButtonNode: HighlightTrackingButto let labelInset: CGFloat = 8.0 - let (textSize, textApply) = makeTextLayout(NSAttributedString(string: title, font: buttonFont, textColor: titleColor), nil, 1, .end, CGSize(width: max(1.0, width - labelInset * 2.0 - iconWidth), height: CGFloat.greatestFiniteMagnitude), .left, nil, UIEdgeInsets()) + let (textSize, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: buttonFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(1.0, width - labelInset * 2.0 - iconWidth), height: CGFloat.greatestFiniteMagnitude), alignment: .left, cutout: nil, insets: UIEdgeInsets())) - let (_, highlightedTextApply) = makeHighlightedTextLayout(NSAttributedString(string: title, font: buttonFont, textColor: highlightedTitleColor), nil, 1, .end, CGSize(width: max(1.0, width - labelInset * 2.0), height: CGFloat.greatestFiniteMagnitude), .left, nil, UIEdgeInsets()) + let (_, highlightedTextApply) = makeHighlightedTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: buttonFont, textColor: highlightedTitleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(1.0, width - labelInset * 2.0), height: CGFloat.greatestFiniteMagnitude), alignment: .left, cutout: nil, insets: UIEdgeInsets())) return (textSize.size.width + labelInset * 2.0, { refinedWidth in return (CGSize(width: refinedWidth, height: 33.0), { @@ -185,10 +185,10 @@ private final class ChatMessageAttachedContentButtonNode: HighlightTrackingButto let _ = highlightedTextApply() targetNode.backgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: refinedWidth, height: 33.0)) - var textFrame = CGRect(origin: CGPoint(x: floor((refinedWidth - textSize.size.width) / 2.0), y: floor((33.0 - textSize.size.height) / 2.0)), size: textSize.size) + var textFrame = CGRect(origin: CGPoint(x: floor((refinedWidth - textSize.size.width) / 2.0), y: floor((34.0 - textSize.size.height) / 2.0)), size: textSize.size) if let image = targetNode.iconNode.image { textFrame.origin.x += floor(image.size.width / 2.0) - targetNode.iconNode.frame = CGRect(origin: CGPoint(x: textFrame.minX - image.size.width - 5.0, y: textFrame.minY + 3.0), size: image.size) + targetNode.iconNode.frame = CGRect(origin: CGPoint(x: textFrame.minX - image.size.width - 5.0, y: textFrame.minY + 2.0), size: image.size) if targetNode.iconNode.supernode == nil { targetNode.addSubnode(targetNode.iconNode) } @@ -216,9 +216,13 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { private var buttonNode: ChatMessageAttachedContentButtonNode? private let statusNode: ChatMessageDateAndStatusNode + private var additionalImageBadgeNode: ChatMessageInteractiveMediaBadge? + private var linkHighlightingNode: LinkHighlightingNode? + private var account: Account? private var message: Message? private var media: Media? + private var theme: PresentationTheme? var openMedia: (() -> Void)? var activateAction: (() -> Void)? @@ -242,6 +246,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { self.textNode.contentMode = .topLeft self.inlineImageNode = TransformImageNode() + self.inlineImageNode.contentAnimations = [.subsequentUpdates] self.inlineImageNode.isLayerBacked = true self.inlineImageNode.displaysAsynchronously = false @@ -255,7 +260,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { self.addSubnode(self.statusNode) } - func asyncLayout() -> (_ theme: PresentationTheme, _ strings: PresentationStrings, _ automaticDownloadSettings: AutomaticMediaDownloadSettings, _ account: Account, _ message: Message, _ messageRead: Bool, _ title: String?, _ subtitle: String?, _ text: String?, _ entities: [MessageTextEntity]?, _ media: (Media, ChatMessageAttachedContentNodeMediaFlags)?, _ actionIcon: ChatMessageAttachedContentActionIcon?, _ actionTitle: String?, _ displayLine: Bool, _ layoutConstants: ChatMessageItemLayoutConstants, _ position: ChatMessageBubbleContentPosition, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))) { + func asyncLayout() -> (_ presentationData: ChatPresentationData, _ automaticDownloadSettings: AutomaticMediaDownloadSettings, _ account: Account, _ message: Message, _ messageRead: Bool, _ title: String?, _ subtitle: String?, _ text: String?, _ entities: [MessageTextEntity]?, _ media: (Media, ChatMessageAttachedContentNodeMediaFlags)?, _ actionIcon: ChatMessageAttachedContentActionIcon?, _ actionTitle: String?, _ displayLine: Bool, _ layoutConstants: ChatMessageItemLayoutConstants, _ constrainedSize: CGSize) -> (CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))) { let textAsyncLayout = TextNode.asyncLayout(self.textNode) let currentImage = self.media as? TelegramMediaImage let imageLayout = self.inlineImageNode.asyncLayout() @@ -265,18 +270,14 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { let makeButtonLayout = ChatMessageAttachedContentButtonNode.asyncLayout(self.buttonNode) - return { theme, strings, automaticDownloadSettings, account, message, messageRead, title, subtitle, text, entities, mediaAndFlags, actionIcon, actionTitle, displayLine, layoutConstants, position, constrainedSize in - let incoming = message.effectivelyIncoming + let currentAdditionalImageBadgeNode = self.additionalImageBadgeNode + + return { presentationData, automaticDownloadSettings, account, message, messageRead, title, subtitle, text, entities, mediaAndFlags, actionIcon, actionTitle, displayLine, layoutConstants, constrainedSize in + let incoming = message.effectivelyIncoming(account.peerId) - var insets = UIEdgeInsets(top: 0.0, left: 12.0, bottom: 5.0, right: 12.0) - switch position.top { - case .None: - insets.top += 8.0 - default: - break - } + var horizontalInsets = UIEdgeInsets(top: 0.0, left: 12.0, bottom: 0.0, right: 12.0) if displayLine { - insets.left += 10.0 + horizontalInsets.left += 10.0 } var preferMediaBeforeText = false @@ -284,10 +285,6 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { preferMediaBeforeText = true } - var t = Int(message.timestamp) - var timeinfo = tm() - localtime_r(&t, &timeinfo) - var edited = false var sentViaBot = false var viewCount: Int? @@ -301,7 +298,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { } } - var dateText = String(format: "%02d:%02d", arguments: [Int(timeinfo.tm_hour), Int(timeinfo.tm_min)]) + var dateText = stringForMessageTimestamp(timestamp: message.timestamp, timeFormat: presentationData.timeFormat) if let author = message.author as? TelegramUser { if author.botInfo != nil { @@ -312,22 +309,39 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { } } + var webpageGalleryMediaCount: Int? + for media in message.media { + if let media = media as? TelegramMediaWebpage { + if case let .Loaded(content) = media.content, let instantPage = content.instantPage, let image = content.image { + switch websiteType(of: content) { + case .instagram, .twitter: + let count = instantPageGalleryMedia(webpageId: media.webpageId, page: instantPage, galleryMedia: image).count + if count > 1 { + webpageGalleryMediaCount = count + } + default: + break + } + } + } + } + var textString: NSAttributedString? var inlineImageDimensions: CGSize? var inlineImageSize: CGSize? var updateInlineImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? var textCutout: TextNodeCutout? var initialWidth: CGFloat = CGFloat.greatestFiniteMagnitude - var refineContentImageLayout: ((CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> ChatMessageInteractiveMediaNode)))? + var refineContentImageLayout: ((CGSize, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ContainedViewLayoutTransition) -> ChatMessageInteractiveMediaNode)))? var refineContentFileLayout: ((CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> ChatMessageInteractiveFileNode)))? let string = NSMutableAttributedString() var notEmpty = false - let bubbleTheme = theme.chat.bubble + let bubbleTheme = presentationData.theme.chat.bubble if let title = title, !title.isEmpty { - string.append(NSAttributedString(string: title, font: titleFont, textColor: incoming ? bubbleTheme.incomingAccentColor : bubbleTheme.outgoingAccentColor)) + string.append(NSAttributedString(string: title, font: titleFont, textColor: incoming ? bubbleTheme.incomingAccentTextColor : bubbleTheme.outgoingAccentTextColor)) notEmpty = true } @@ -352,6 +366,9 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { } textString = string + if string.length > 1000 { + textString = string.attributedSubstring(from: NSMakeRange(0, 1000)) + } if let (media, flags) = mediaAndFlags { if let file = media as? TelegramMediaFile { @@ -362,22 +379,36 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { } else if file.isInstantVideo { automaticDownload = automaticDownloadSettings.categories.getInstantVideo(message.id.peerId) } - let (initialImageWidth, _, refineLayout) = contentImageLayout(account, theme, strings, message, file, ImageCorners(radius: 4.0), automaticDownload, CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height), layoutConstants) - initialWidth = initialImageWidth + insets.left + insets.right + let (_, initialImageWidth, refineLayout) = contentImageLayout(account, presentationData.theme, presentationData.strings, message, file, automaticDownload, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants) + initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right refineContentImageLayout = refineLayout } else { var automaticDownload = false if file.isVoice { automaticDownload = automaticDownloadSettings.categories.getVoice(message.id.peerId) } - let (_, refineLayout) = contentFileLayout(account, theme, strings, message, file, automaticDownload, message.effectivelyIncoming, nil, CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height)) + + let statusType: ChatMessageDateAndStatusType + if message.effectivelyIncoming(account.peerId) { + statusType = .BubbleIncoming + } else { + if message.flags.contains(.Failed) { + statusType = .BubbleOutgoing(.Failed) + } else if message.flags.isSending { + statusType = .BubbleOutgoing(.Sending) + } else { + statusType = .BubbleOutgoing(.Sent(read: messageRead)) + } + } + + let (_, refineLayout) = contentFileLayout(account, presentationData, message, file, automaticDownload, message.effectivelyIncoming(account.peerId), statusType, CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)) refineContentFileLayout = refineLayout } } else if let image = media as? TelegramMediaImage { if !flags.contains(.preferMediaInline) { let automaticDownload = automaticDownloadSettings.categories.getPhoto(message.id.peerId) - let (initialImageWidth, _, refineLayout) = contentImageLayout(account, theme, strings, message, image, ImageCorners(radius: 4.0), automaticDownload, CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height), layoutConstants) - initialWidth = initialImageWidth + insets.left + insets.right + let (_, initialImageWidth, refineLayout) = contentImageLayout(account, presentationData.theme, presentationData.strings, message, image, automaticDownload, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants) + initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right refineContentImageLayout = refineLayout } else if let dimensions = largestImageRepresentation(image.representations)?.dimensions { inlineImageDimensions = dimensions @@ -388,8 +419,8 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { } } else if let image = media as? TelegramMediaWebFile { let automaticDownload = automaticDownloadSettings.categories.getPhoto(message.id.peerId) - let (initialImageWidth, _, refineLayout) = contentImageLayout(account, theme, strings, message, image, ImageCorners(radius: 4.0), automaticDownload, CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height), layoutConstants) - initialWidth = initialImageWidth + insets.left + insets.right + let (_, initialImageWidth, refineLayout) = contentImageLayout(account, presentationData.theme, presentationData.strings, message, image, automaticDownload, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants) + initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right refineContentImageLayout = refineLayout } } @@ -402,52 +433,76 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { } } - return (initialWidth, { constrainedSize in - var statusInText = false - var statusSizeAndApply: (CGSize, (Bool) -> Void)? - - let textConstrainedSize = CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height - insets.top - insets.bottom) - - switch position.bottom { - case .None: - let imageMode = !((refineContentImageLayout == nil && refineContentFileLayout == nil) || preferMediaBeforeText) - statusInText = !imageMode - - let statusType: ChatMessageDateAndStatusType - if message.effectivelyIncoming { - if imageMode { - statusType = .ImageIncoming - } else { - statusType = .BubbleIncoming - } - } else { - if message.flags.contains(.Failed) { - if imageMode { - statusType = .ImageOutgoing(.Failed) - } else { - statusType = .BubbleOutgoing(.Failed) - } - } else if message.flags.isSending { - if imageMode { - statusType = .ImageOutgoing(.Sending) - } else { - statusType = .BubbleOutgoing(.Sending) - } - } else { - if imageMode { - statusType = .ImageOutgoing(.Sent(read: messageRead)) - } else { - statusType = .BubbleOutgoing(.Sent(read: messageRead)) - } - } - } - - statusSizeAndApply = statusLayout(theme, edited && !sentViaBot, viewCount, dateText, statusType, textConstrainedSize) + return (initialWidth, { constrainedSize, position in + var insets = UIEdgeInsets(top: 0.0, left: horizontalInsets.left, bottom: 5.0, right: horizontalInsets.right) + switch position { + case .linear(.None, _): + insets.top += 8.0 default: break } - let (textLayout, textApply) = textAsyncLayout(textString, nil, 12, .end, textConstrainedSize, .natural, textCutout, UIEdgeInsets()) + var statusInText = false + + var statusSizeAndApply: (CGSize, (Bool) -> Void)? + + let textConstrainedSize = CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height - insets.top - insets.bottom) + + var additionalImageBadgeContent: ChatMessageInteractiveMediaBadgeContent? + + switch position { + case .linear(_, .None): + let imageMode = !((refineContentImageLayout == nil && refineContentFileLayout == nil) || preferMediaBeforeText) + statusInText = !imageMode + + var skipStandardStatus = false + if let count = webpageGalleryMediaCount { + additionalImageBadgeContent = .text(backgroundColor: presentationData.theme.chat.bubble.mediaDateAndStatusFillColor, foregroundColor: presentationData.theme.chat.bubble.mediaDateAndStatusTextColor, shape: .corners(2.0), text: "1 \(presentationData.strings.Common_of) \(count)") + skipStandardStatus = imageMode + } + + if !skipStandardStatus { + let statusType: ChatMessageDateAndStatusType + if message.effectivelyIncoming(account.peerId) { + if imageMode { + statusType = .ImageIncoming + } else { + statusType = .BubbleIncoming + } + } else { + if message.flags.contains(.Failed) { + if imageMode { + statusType = .ImageOutgoing(.Failed) + } else { + statusType = .BubbleOutgoing(.Failed) + } + } else if message.flags.isSending { + if imageMode { + statusType = .ImageOutgoing(.Sending) + } else { + statusType = .BubbleOutgoing(.Sending) + } + } else { + if imageMode { + statusType = .ImageOutgoing(.Sent(read: messageRead)) + } else { + statusType = .BubbleOutgoing(.Sent(read: messageRead)) + } + } + } + + statusSizeAndApply = statusLayout(presentationData.theme, presentationData.strings, edited && !sentViaBot, viewCount, dateText, statusType, textConstrainedSize) + } + default: + break + } + + var updatedAdditionalImageBadge: ChatMessageInteractiveMediaBadge? + if let _ = additionalImageBadgeContent { + updatedAdditionalImageBadge = currentAdditionalImageBadgeNode ?? ChatMessageInteractiveMediaBadge() + } + + let (textLayout, textApply) = textAsyncLayout(TextNodeLayoutArguments(attributedString: textString, backgroundColor: nil, maximumNumberOfLines: 12, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: textCutout, insets: UIEdgeInsets())) var textFrame = CGRect(origin: CGPoint(), size: textLayout.size) @@ -477,7 +532,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { textFrame = textFrame.offsetBy(dx: insets.left, dy: insets.top) - let lineImage = incoming ? PresentationResourcesChat.chatBubbleVerticalLineIncomingImage(theme) : PresentationResourcesChat.chatBubbleVerticalLineOutgoingImage(theme) + let lineImage = incoming ? PresentationResourcesChat.chatBubbleVerticalLineIncomingImage(presentationData.theme) : PresentationResourcesChat.chatBubbleVerticalLineOutgoingImage(presentationData.theme) var boundingSize = textFrame.size var lineHeight = textFrame.size.height @@ -496,9 +551,9 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { } } - var finalizeContentImageLayout: ((CGFloat) -> (CGSize, () -> ChatMessageInteractiveMediaNode))? + var finalizeContentImageLayout: ((CGFloat) -> (CGSize, (ContainedViewLayoutTransition) -> ChatMessageInteractiveMediaNode))? if let refineContentImageLayout = refineContentImageLayout { - let (refinedWidth, finalizeImageLayout) = refineContentImageLayout(textConstrainedSize) + let (refinedWidth, finalizeImageLayout) = refineContentImageLayout(textConstrainedSize, ImageCorners(radius: 4.0)) finalizeContentImageLayout = finalizeImageLayout boundingSize.width = max(boundingSize.width, refinedWidth) @@ -529,23 +584,23 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { let titleColor: UIColor let titleHighlightedColor: UIColor if incoming { - buttonImage = PresentationResourcesChat.chatMessageAttachedContentButtonIncoming(theme)! - buttonHighlightedImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonIncoming(theme)! + buttonImage = PresentationResourcesChat.chatMessageAttachedContentButtonIncoming(presentationData.theme)! + buttonHighlightedImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonIncoming(presentationData.theme)! if let actionIcon = actionIcon, case .instant = actionIcon { - buttonIconImage = PresentationResourcesChat.chatMessageAttachedContentButtonIconInstantIncoming(theme)! - buttonHighlightedIconImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonIconInstantIncoming(theme)! + buttonIconImage = PresentationResourcesChat.chatMessageAttachedContentButtonIconInstantIncoming(presentationData.theme)! + buttonHighlightedIconImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonIconInstantIncoming(presentationData.theme)! } - titleColor = theme.chat.bubble.incomingAccentColor - titleHighlightedColor = theme.chat.bubble.incomingFillColor + titleColor = presentationData.theme.chat.bubble.incomingAccentTextColor + titleHighlightedColor = presentationData.theme.chat.bubble.incomingFillColor } else { - buttonImage = PresentationResourcesChat.chatMessageAttachedContentButtonOutgoing(theme)! - buttonHighlightedImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonOutgoing(theme)! + buttonImage = PresentationResourcesChat.chatMessageAttachedContentButtonOutgoing(presentationData.theme)! + buttonHighlightedImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonOutgoing(presentationData.theme)! if let actionIcon = actionIcon, case .instant = actionIcon { - buttonIconImage = PresentationResourcesChat.chatMessageAttachedContentButtonIconInstantOutgoing(theme)! - buttonHighlightedIconImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonIconInstantOutgoing(theme)! + buttonIconImage = PresentationResourcesChat.chatMessageAttachedContentButtonIconInstantOutgoing(presentationData.theme)! + buttonHighlightedIconImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonIconInstantOutgoing(presentationData.theme)! } - titleColor = theme.chat.bubble.outgoingAccentColor - titleHighlightedColor = theme.chat.bubble.outgoingFillColor + titleColor = presentationData.theme.chat.bubble.outgoingAccentTextColor + titleHighlightedColor = presentationData.theme.chat.bubble.outgoingFillColor } let (buttonWidth, continueLayout) = makeButtonLayout(constrainedSize.width, buttonImage, buttonHighlightedImage, buttonIconImage, buttonHighlightedIconImage, actionTitle, titleColor, titleHighlightedColor) boundingSize.width = max(buttonWidth, boundingSize.width) @@ -564,7 +619,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { imageFrame = CGRect(origin: CGPoint(x: boundingWidth - inlineImageSize.width - insets.right, y: 0.0), size: inlineImageSize) } - var contentImageSizeAndApply: (CGSize, () -> ChatMessageInteractiveMediaNode)? + var contentImageSizeAndApply: (CGSize, (ContainedViewLayoutTransition) -> ChatMessageInteractiveMediaNode)? if let finalizeContentImageLayout = finalizeContentImageLayout { let (size, apply) = finalizeContentImageLayout(boundingWidth - insets.left - insets.right) contentImageSizeAndApply = (size, apply) @@ -587,7 +642,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { let (size, apply) = finalizeContentFileLayout(boundingWidth - insets.left - insets.right) contentFileSizeAndApply = (size, apply) - var imageHeigthAddition = size.height + var imageHeigthAddition = size.height + 6.0 if textFrame.size.height > CGFloat.ulpOfOne { imageHeigthAddition += 2.0 } @@ -610,23 +665,30 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { return (adjustedBoundingSize, { [weak self] animation in if let strongSelf = self { + strongSelf.account = account strongSelf.message = message strongSelf.media = mediaAndFlags?.0 + strongSelf.theme = presentationData.theme var hasAnimation = true - if case .None = animation { - hasAnimation = false + var transition: ContainedViewLayoutTransition = .immediate + switch animation { + case .None: + hasAnimation = false + case let .System(duration): + hasAnimation = true + transition = .animated(duration: duration, curve: .easeInOut) } strongSelf.lineNode.image = lineImage - strongSelf.lineNode.frame = CGRect(origin: CGPoint(x: 13.0, y: 0.0), size: CGSize(width: 2.0, height: adjustedLineHeight - insets.top - insets.bottom - 2.0)) + strongSelf.lineNode.frame = CGRect(origin: CGPoint(x: 13.0, y: insets.top), size: CGSize(width: 2.0, height: adjustedLineHeight - insets.top - insets.bottom - 2.0)) strongSelf.lineNode.isHidden = !displayLine let _ = textApply() if let imageFrame = imageFrame { if let updateImageSignal = updateInlineImageSignal { - strongSelf.inlineImageNode.setSignal(account: account, signal: updateImageSignal) + strongSelf.inlineImageNode.setSignal(updateImageSignal) } strongSelf.inlineImageNode.frame = imageFrame @@ -646,7 +708,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { if let (contentImageSize, contentImageApply) = contentImageSizeAndApply { contentMediaHeight = contentImageSize.height - let contentImageNode = contentImageApply() + let contentImageNode = contentImageApply(transition) if strongSelf.contentImageNode !== contentImageNode { strongSelf.contentImageNode = contentImageNode strongSelf.addSubnode(contentImageNode) @@ -657,7 +719,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { } contentImageNode.visibility = strongSelf.visibility } - let _ = contentImageApply() + let _ = contentImageApply(transition) let contentImageFrame: CGRect if let (_, flags) = mediaAndFlags, flags.contains(.preferMediaBeforeText) { contentImageFrame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: contentImageSize) @@ -676,6 +738,20 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { strongSelf.contentImageNode = nil } + if let updatedAdditionalImageBadge = updatedAdditionalImageBadge, let contentImageNode = strongSelf.contentImageNode, let contentImageSize = contentImageSizeAndApply?.0 { + if strongSelf.additionalImageBadgeNode != updatedAdditionalImageBadge { + strongSelf.additionalImageBadgeNode?.removeFromSupernode() + } + strongSelf.additionalImageBadgeNode = updatedAdditionalImageBadge + contentImageNode.addSubnode(updatedAdditionalImageBadge) + updatedAdditionalImageBadge.contentMode = .topRight + updatedAdditionalImageBadge.content = additionalImageBadgeContent + updatedAdditionalImageBadge.frame = CGRect(origin: CGPoint(x: contentImageSize.width - 2.0, y: contentImageSize.height - 18.0 - 2.0), size: CGSize(width: 0.0, height: 0.0)) + } else if let additionalImageBadgeNode = strongSelf.additionalImageBadgeNode { + strongSelf.additionalImageBadgeNode = nil + additionalImageBadgeNode.removeFromSupernode() + } + if let (contentFileSize, contentFileApply) = contentFileSizeAndApply { contentMediaHeight = contentFileSize.height @@ -781,4 +857,67 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { } return false } + + func tapActionAtPoint(_ point: CGPoint) -> ChatMessageBubbleContentTapAction { + let textNodeFrame = self.textNode.frame + if let (_, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { + if let url = attributes[NSAttributedStringKey(rawValue: TextNode.UrlAttribute)] as? String { + return .url(url) + } else if let peerMention = attributes[NSAttributedStringKey(rawValue: TextNode.TelegramPeerMentionAttribute)] as? TelegramPeerMention { + return .peerMention(peerMention.peerId, peerMention.mention) + } else if let peerName = attributes[NSAttributedStringKey(rawValue: TextNode.TelegramPeerTextMentionAttribute)] as? String { + return .textMention(peerName) + } else if let botCommand = attributes[NSAttributedStringKey(rawValue: TextNode.TelegramBotCommandAttribute)] as? String { + return .botCommand(botCommand) + } else if let hashtag = attributes[NSAttributedStringKey(rawValue: TextNode.TelegramHashtagAttribute)] as? TelegramHashtag { + return .hashtag(hashtag.peerName, hashtag.hashtag) + } else { + return .none + } + } else { + return .none + } + } + + func updateTouchesAtPoint(_ point: CGPoint?) { + if let account = self.account, let message = self.message, let theme = self.theme { + var rects: [CGRect]? + if let point = point { + let textNodeFrame = self.textNode.frame + if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { + let possibleNames: [String] = [ + TextNode.UrlAttribute, + TextNode.TelegramPeerMentionAttribute, + TextNode.TelegramPeerTextMentionAttribute, + TextNode.TelegramBotCommandAttribute, + TextNode.TelegramHashtagAttribute + ] + for name in possibleNames { + if let _ = attributes[NSAttributedStringKey(rawValue: name)] { + rects = self.textNode.attributeRects(name: name, at: index) + break + } + } + } + } + + if let rects = rects { + let linkHighlightingNode: LinkHighlightingNode + if let current = self.linkHighlightingNode { + linkHighlightingNode = current + } else { + linkHighlightingNode = LinkHighlightingNode(color: message.effectivelyIncoming(account.peerId) ? theme.chat.bubble.incomingLinkHighlightColor : theme.chat.bubble.outgoingLinkHighlightColor) + self.linkHighlightingNode = linkHighlightingNode + self.insertSubnode(linkHighlightingNode, belowSubnode: self.textNode) + } + linkHighlightingNode.frame = self.textNode.frame + linkHighlightingNode.updateRects(rects) + } else if let linkHighlightingNode = self.linkHighlightingNode { + self.linkHighlightingNode = nil + linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in + linkHighlightingNode?.removeFromSupernode() + }) + } + } + } } diff --git a/TelegramUI/ChatMessageBackground.swift b/TelegramUI/ChatMessageBackground.swift index 5ca62162cd..ff26074d8c 100644 --- a/TelegramUI/ChatMessageBackground.swift +++ b/TelegramUI/ChatMessageBackground.swift @@ -1,10 +1,11 @@ import Foundation import AsyncDisplayKit +import Display enum ChatMessageBackgroundMergeType { - case None, Top, Bottom, Both + case None, Side, Top, Bottom, Both - init(top: Bool, bottom: Bool) { + init(top: Bool, bottom: Bool, side: Bool) { if top && bottom { self = .Both } else if top { @@ -12,37 +13,48 @@ enum ChatMessageBackgroundMergeType { } else if bottom { self = .Bottom } else { - self = .None + if side { + self = .Side + } else { + self = .None + } } } } enum ChatMessageBackgroundType: Equatable { - case Incoming(ChatMessageBackgroundMergeType), Outgoing(ChatMessageBackgroundMergeType) + case none + case incoming(ChatMessageBackgroundMergeType) + case outgoing(ChatMessageBackgroundMergeType) static func ==(lhs: ChatMessageBackgroundType, rhs: ChatMessageBackgroundType) -> Bool { switch lhs { - case let .Incoming(lhsMergeType): - switch rhs { - case let .Incoming(rhsMergeType): - return lhsMergeType == rhsMergeType - case .Outgoing: - return false - } - case let .Outgoing(lhsMergeType): - switch rhs { - case .Incoming: - return false - case let .Outgoing(rhsMergeType): - return lhsMergeType == rhsMergeType - } + case .none: + if case .none = rhs { + return true + } else { + return false + } + case let .incoming(mergeType): + if case .incoming(mergeType) = rhs { + return true + } else { + return false + } + case let .outgoing(mergeType): + if case .outgoing(mergeType) = rhs { + return true + } else { + return false + } } } } class ChatMessageBackground: ASImageNode { - private var type: ChatMessageBackgroundType? + private(set) var type: ChatMessageBackgroundType? private var currentHighlighted = false + private var graphics: PrincipalThemeEssentialGraphics? override init() { super.init() @@ -52,38 +64,64 @@ class ChatMessageBackground: ASImageNode { self.displayWithoutProcessing = true } - func setType(type: ChatMessageBackgroundType, highlighted: Bool, graphics: PrincipalThemeEssentialGraphics) { - if let currentType = self.type, currentType == type, self.currentHighlighted == highlighted { + func setType(type: ChatMessageBackgroundType, highlighted: Bool, graphics: PrincipalThemeEssentialGraphics, transition: ContainedViewLayoutTransition) { + let previousType = self.type + if let currentType = previousType, currentType == type, self.currentHighlighted == highlighted, self.graphics === graphics { return } self.type = type self.currentHighlighted = highlighted + self.graphics = graphics let image: UIImage? switch type { - case let .Incoming(mergeType): - switch mergeType { - case .None: - image = highlighted ? graphics.chatMessageBackgroundIncomingHighlightedImage : graphics.chatMessageBackgroundIncomingImage - case .Top: - image = highlighted ? graphics.chatMessageBackgroundIncomingMergedTopHighlightedImage : graphics.chatMessageBackgroundIncomingMergedTopImage - case .Bottom: - image = highlighted ? graphics.chatMessageBackgroundIncomingMergedBottomHighlightedImage : graphics.chatMessageBackgroundIncomingMergedBottomImage - case .Both: - image = highlighted ? graphics.chatMessageBackgroundIncomingMergedBothHighlightedImage : graphics.chatMessageBackgroundIncomingMergedBothImage - } - case let .Outgoing(mergeType): - switch mergeType { - case .None: - image = highlighted ? graphics.chatMessageBackgroundOutgoingHighlightedImage : graphics.chatMessageBackgroundOutgoingImage - case .Top: - image = highlighted ? graphics.chatMessageBackgroundOutgoingMergedTopHighlightedImage : graphics.chatMessageBackgroundOutgoingMergedTopImage - case .Bottom: - image = highlighted ? graphics.chatMessageBackgroundOutgoingMergedBottomHighlightedImage : graphics.chatMessageBackgroundOutgoingMergedBottomImage - case .Both: - image = highlighted ? graphics.chatMessageBackgroundOutgoingMergedBothHighlightedImage : graphics.chatMessageBackgroundOutgoingMergedBothImage + case .none: + image = nil + case let .incoming(mergeType): + switch mergeType { + case .None: + image = highlighted ? graphics.chatMessageBackgroundIncomingHighlightedImage : graphics.chatMessageBackgroundIncomingImage + case .Top: + image = highlighted ? graphics.chatMessageBackgroundIncomingMergedTopHighlightedImage : graphics.chatMessageBackgroundIncomingMergedTopImage + case .Bottom: + image = highlighted ? graphics.chatMessageBackgroundIncomingMergedBottomHighlightedImage : graphics.chatMessageBackgroundIncomingMergedBottomImage + case .Both: + image = highlighted ? graphics.chatMessageBackgroundIncomingMergedBothHighlightedImage : graphics.chatMessageBackgroundIncomingMergedBothImage + case .Side: + image = highlighted ? graphics.chatMessageBackgroundIncomingMergedSideHighlightedImage : graphics.chatMessageBackgroundIncomingMergedSideImage + } + case let .outgoing(mergeType): + switch mergeType { + case .None: + image = highlighted ? graphics.chatMessageBackgroundOutgoingHighlightedImage : graphics.chatMessageBackgroundOutgoingImage + case .Top: + image = highlighted ? graphics.chatMessageBackgroundOutgoingMergedTopHighlightedImage : graphics.chatMessageBackgroundOutgoingMergedTopImage + case .Bottom: + image = highlighted ? graphics.chatMessageBackgroundOutgoingMergedBottomHighlightedImage : graphics.chatMessageBackgroundOutgoingMergedBottomImage + case .Both: + image = highlighted ? graphics.chatMessageBackgroundOutgoingMergedBothHighlightedImage : graphics.chatMessageBackgroundOutgoingMergedBothImage + case .Side: + image = highlighted ? graphics.chatMessageBackgroundOutgoingMergedSideHighlightedImage : graphics.chatMessageBackgroundOutgoingMergedSideImage + } + } + + if let previousType = previousType, previousType != .none, type == .none { + if transition.isAnimated { + let tempLayer = CALayer() + tempLayer.contents = self.layer.contents + tempLayer.contentsScale = self.layer.contentsScale + tempLayer.rasterizationScale = self.layer.rasterizationScale + tempLayer.contentsGravity = self.layer.contentsGravity + tempLayer.contentsCenter = self.layer.contentsCenter + + tempLayer.frame = self.bounds + self.layer.addSublayer(tempLayer) + transition.updateAlpha(layer: tempLayer, alpha: 0.0, completion: { [weak tempLayer] _ in + tempLayer?.removeFromSuperlayer() + }) } } + self.image = image } } diff --git a/TelegramUI/ChatMessageBubbleContentCalclulateImageCorners.swift b/TelegramUI/ChatMessageBubbleContentCalclulateImageCorners.swift index e18cc4c776..3f31c822ba 100644 --- a/TelegramUI/ChatMessageBubbleContentCalclulateImageCorners.swift +++ b/TelegramUI/ChatMessageBubbleContentCalclulateImageCorners.swift @@ -4,48 +4,91 @@ func chatMessageBubbleImageContentCorners(relativeContentPosition position: Chat let topLeftCorner: ImageCorner let topRightCorner: ImageCorner - switch position.top { - case .Neighbour: - topLeftCorner = .Corner(mergedWithAnotherContentRadius) - topRightCorner = .Corner(mergedWithAnotherContentRadius) - case let .None(mergeStatus): - switch mergeStatus { - case .Left: - topLeftCorner = .Corner(mergedRadius) - topRightCorner = .Corner(normalRadius) - case .None: + switch position { + case let .linear(top, _): + switch top { + case .Neighbour: + topLeftCorner = .Corner(mergedWithAnotherContentRadius) + topRightCorner = .Corner(mergedWithAnotherContentRadius) + case let .None(mergeStatus): + switch mergeStatus { + case .Left: + topLeftCorner = .Corner(mergedRadius) + topRightCorner = .Corner(normalRadius) + case .None: + topLeftCorner = .Corner(normalRadius) + topRightCorner = .Corner(normalRadius) + case .Right: + topLeftCorner = .Corner(normalRadius) + topRightCorner = .Corner(mergedRadius) + } + } + case let .mosaic(position): + switch position.topLeft { + case .none: topLeftCorner = .Corner(normalRadius) + case .merged: + topLeftCorner = .Corner(mergedWithAnotherContentRadius) + } + switch position.topRight { + case .none: topRightCorner = .Corner(normalRadius) - case .Right: - topLeftCorner = .Corner(normalRadius) - topRightCorner = .Corner(mergedRadius) + case .merged: + topRightCorner = .Corner(mergedWithAnotherContentRadius) } } let bottomLeftCorner: ImageCorner let bottomRightCorner: ImageCorner - switch position.bottom { - case .Neighbour: - bottomLeftCorner = .Corner(mergedWithAnotherContentRadius) - bottomRightCorner = .Corner(mergedWithAnotherContentRadius) - case let .None(mergeStatus): - switch mergeStatus { - case .Left: - bottomLeftCorner = .Corner(mergedRadius) - bottomRightCorner = .Corner(normalRadius) - case let .None(status): - switch status { - case .Incoming: - bottomLeftCorner = .Tail(normalRadius) + switch position { + case let .linear(_, bottom): + switch bottom { + case .Neighbour: + bottomLeftCorner = .Corner(mergedWithAnotherContentRadius) + bottomRightCorner = .Corner(mergedWithAnotherContentRadius) + case let .None(mergeStatus): + switch mergeStatus { + case .Left: + bottomLeftCorner = .Corner(mergedRadius) bottomRightCorner = .Corner(normalRadius) - case .Outgoing: + case let .None(status): + switch status { + case .Incoming: + bottomLeftCorner = .Tail(normalRadius, true) + bottomRightCorner = .Corner(normalRadius) + case .Outgoing: + bottomLeftCorner = .Corner(normalRadius) + bottomRightCorner = .Tail(normalRadius, true) + case .None: + bottomLeftCorner = .Corner(normalRadius) + bottomRightCorner = .Corner(normalRadius) + } + case .Right: bottomLeftCorner = .Corner(normalRadius) - bottomRightCorner = .Tail(normalRadius) + bottomRightCorner = .Corner(mergedRadius) } - case .Right: - bottomLeftCorner = .Corner(normalRadius) - bottomRightCorner = .Corner(mergedRadius) + } + case let .mosaic(position): + switch position.bottomLeft { + case let .none(tail): + if tail { + bottomLeftCorner = .Tail(normalRadius, true) + } else { + bottomLeftCorner = .Corner(normalRadius) + } + case .merged: + bottomLeftCorner = .Corner(mergedWithAnotherContentRadius) + } + switch position.bottomRight { + case let .none(tail): + if tail { + bottomRightCorner = .Tail(normalRadius, true) + } else { + bottomRightCorner = .Corner(normalRadius) + } + case .merged: + bottomRightCorner = .Corner(mergedWithAnotherContentRadius) } } diff --git a/TelegramUI/ChatMessageBubbleContentNode.swift b/TelegramUI/ChatMessageBubbleContentNode.swift index a98dfb2b01..9684790676 100644 --- a/TelegramUI/ChatMessageBubbleContentNode.swift +++ b/TelegramUI/ChatMessageBubbleContentNode.swift @@ -2,15 +2,19 @@ import Foundation import AsyncDisplayKit import Display import Postbox +import TelegramCore struct ChatMessageBubbleContentProperties { let hidesSimpleAuthorHeader: Bool let headerSpacing: CGFloat + let hidesBackgroundForEmptyWallpapers: Bool + let forceFullCorners: Bool } enum ChatMessageBubbleNoneMergeStatus { case Incoming case Outgoing + case None } enum ChatMessageBubbleMergeStatus { @@ -24,9 +28,27 @@ enum ChatMessageBubbleRelativePosition { case Neighbour } -struct ChatMessageBubbleContentPosition { - let top: ChatMessageBubbleRelativePosition - let bottom: ChatMessageBubbleRelativePosition +enum ChatMessageBubbleContentMosaicNeighbor { + case merged + case none(tail: Bool) +} + +struct ChatMessageBubbleContentMosaicPosition { + let topLeft: ChatMessageBubbleContentMosaicNeighbor + let topRight: ChatMessageBubbleContentMosaicNeighbor + let bottomLeft: ChatMessageBubbleContentMosaicNeighbor + let bottomRight: ChatMessageBubbleContentMosaicNeighbor + let mosaicStatusHorizontalOffset: CGFloat? +} + +enum ChatMessageBubbleContentPosition { + case linear(top: ChatMessageBubbleRelativePosition, bottom: ChatMessageBubbleRelativePosition) + case mosaic(position: ChatMessageBubbleContentMosaicPosition) +} + +enum ChatMessageBubblePreparePosition { + case linear(top: ChatMessageBubbleRelativePosition, bottom: ChatMessageBubbleRelativePosition) + case mosaic(top: ChatMessageBubbleRelativePosition, bottom: ChatMessageBubbleRelativePosition) } enum ChatMessageBubbleContentTapAction { @@ -42,21 +64,36 @@ enum ChatMessageBubbleContentTapAction { case ignore } -class ChatMessageBubbleContentNode: ASDisplayNode { - var properties: ChatMessageBubbleContentProperties { - return ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0) - } +final class ChatMessageBubbleContentItem { + let account: Account + let controllerInteraction: ChatControllerInteraction + let message: Message + let read: Bool + let presentationData: ChatPresentationData - var controllerInteraction: ChatControllerInteraction? + init(account: Account, controllerInteraction: ChatControllerInteraction, message: Message, read: Bool, presentationData: ChatPresentationData) { + self.account = account + self.controllerInteraction = controllerInteraction + self.message = message + self.read = read + self.presentationData = presentationData + } +} + +class ChatMessageBubbleContentNode: ASDisplayNode { + var supportsMosaic: Bool { + return false + } var visibility: ListViewItemNodeVisibility = .none + var item: ChatMessageBubbleContentItem? + required override init() { - //super.init(layerBacked: false) super.init() } - func asyncLayoutContent() -> (_ item: ChatMessageItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ position: ChatMessageBubbleContentPosition, _ constrainedSize: CGSize) -> (maxWidth: CGFloat, layout: (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))) { + func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, unboundSize: CGSize?, maxWidth: CGFloat, layout: (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))) { preconditionFailure() } @@ -72,7 +109,13 @@ class ChatMessageBubbleContentNode: ASDisplayNode { func animateInsertionIntoBubble(_ duration: Double) { } - func transitionNode(media: Media) -> ASDisplayNode? { + func animateRemovalFromBubble(_ duration: Double, completion: @escaping () -> Void) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in + completion() + }) + } + + func transitionNode(messageId: MessageId, media: Media) -> ASDisplayNode? { return nil } diff --git a/TelegramUI/ChatMessageBubbleImages.swift b/TelegramUI/ChatMessageBubbleImages.swift index c17cb15572..63866abc6f 100644 --- a/TelegramUI/ChatMessageBubbleImages.swift +++ b/TelegramUI/ChatMessageBubbleImages.swift @@ -6,6 +6,7 @@ enum MessageBubbleImageNeighbors { case top case bottom case both + case side } func messageSingleBubbleLikeImage(fillColor: UIColor, strokeColor: UIColor) -> UIImage { @@ -30,7 +31,7 @@ func messageBubbleImage(incoming: Bool, fillColor: UIColor, strokeColor: UIColor let additionalOffset: CGFloat switch neighbors { - case .none, .bottom: + case .none, .side, .bottom: additionalOffset = 0.0 case .both, .top: additionalOffset = 6.0 @@ -52,6 +53,11 @@ func messageBubbleImage(incoming: Bool, fillColor: UIColor, strokeColor: UIColor context.strokePath() let _ = try? drawSvgPath(context, path: "M6,17.5 C6,7.83289181 13.8350169,0 23.5,0 C33.1671082,0 41,7.83501688 41,17.5 C41,27.1671082 33.1649831,35 23.5,35 C19.2941198,35 15.4354328,33.5169337 12.4179496,31.0453367 C9.05531719,34.9894816 -2.41102995e-08,35 0,35 C5.972003,31.5499861 6,26.8616169 6,26.8616169 L6,17.5 L6,17.5 ") context.fillPath() + case .side: + let _ = try? drawSvgPath(context, path: "M6,17.5 C6,7.83289181 13.8350169,0 23.5,0 C33.1671082,0 41,7.83501688 41,17.5 C41,27.1671082 33.1649831,35 23.5,35 C19.2941198,35 15.4354328,33.5169337 12.4179496,31.0453367 C9.05531719,34.9894816 -2.41102995e-08,35 0,35 C5.972003,31.5499861 6,26.8616169 6,26.8616169 L6,17.5 L6,17.5 ") + context.strokePath() + let _ = try? drawSvgPath(context, path: "M6,17.5 C6,7.83289181 13.8350169,0 23.5,0 C33.1671082,0 41,7.83501688 41,17.5 C41,27.1671082 33.1649831,35 23.5,35 C19.2941198,35 15.4354328,33.5169337 12.4179496,31.0453367 C9.05531719,34.9894816 -2.41102995e-08,35 0,35 C5.972003,31.5499861 6,26.8616169 6,26.8616169 L6,17.5 L6,17.5 ") + context.fillPath() case .top: let _ = try? drawSvgPath(context, path: "M35,17.5 C35,7.83501688 27.1671082,0 17.5,0 L17.5,0 C7.83501688,0 0,7.83289181 0,17.5 L0,29.0031815 C0,32.3151329 2.6882755,35 5.99681848,35 L17.5,35 C27.1649831,35 35,27.1671082 35,17.5 L35,17.5 L35,17.5 ") context.strokePath() @@ -78,7 +84,7 @@ enum MessageBubbleActionButtonPosition { case bottomSingle } -func messageBubbleActionButtonImage(color: UIColor, position: MessageBubbleActionButtonPosition) -> UIImage { +func messageBubbleActionButtonImage(color: UIColor, strokeColor: UIColor, position: MessageBubbleActionButtonPosition) -> UIImage { let largeRadius: CGFloat = 17.0 let smallRadius: CGFloat = 6.0 let size: CGSize @@ -96,11 +102,28 @@ func messageBubbleActionButtonImage(color: UIColor, position: MessageBubbleActio context.scaleBy(x: 1.0, y: -1.0) } context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) - context.setFillColor(color.cgColor) context.setBlendMode(.copy) + var effectiveStrokeColor: UIColor? + var strokeAlpha: CGFloat = 0.0 + strokeColor.getRed(nil, green: nil, blue: nil, alpha: &strokeAlpha) + if !strokeAlpha.isZero { + effectiveStrokeColor = strokeColor + } + context.setFillColor(color.cgColor) + let lineWidth: CGFloat = 1.0 + let halfLineWidth = lineWidth / 2.0 + if let effectiveStrokeColor = effectiveStrokeColor { + context.setStrokeColor(effectiveStrokeColor.cgColor) + context.setLineWidth(lineWidth) + } switch position { case .middle: context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + if effectiveStrokeColor != nil { + context.setBlendMode(.normal) + context.strokeEllipse(in: CGRect(origin: CGPoint(x: halfLineWidth, y: halfLineWidth), size: CGSize(width: size.width - lineWidth, height: size.height - lineWidth))) + context.setBlendMode(.copy) + } case .bottomLeft, .bottomRight: context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: smallRadius + smallRadius, height: smallRadius + smallRadius))) @@ -114,12 +137,45 @@ func messageBubbleActionButtonImage(color: UIColor, position: MessageBubbleActio context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - smallRadius - smallRadius, y: size.height - smallRadius - smallRadius), size: CGSize(width: smallRadius + smallRadius, height: smallRadius + smallRadius))) context.fill(CGRect(origin: CGPoint(x: largeRadius, y: size.height - largeRadius - largeRadius), size: CGSize(width: size.width - smallRadius - largeRadius, height: largeRadius + largeRadius))) context.fill(CGRect(origin: CGPoint(x: size.width - smallRadius, y: size.height - largeRadius), size: CGSize(width: smallRadius, height: largeRadius - smallRadius))) + if effectiveStrokeColor != nil { + context.setBlendMode(.normal) + context.beginPath() + context.move(to: CGPoint(x: halfLineWidth, y: smallRadius + halfLineWidth)) + context.addArc(tangent1End: CGPoint(x: halfLineWidth, y: halfLineWidth), tangent2End: CGPoint(x: halfLineWidth + smallRadius, y: halfLineWidth), radius: smallRadius) + context.addLine(to: CGPoint(x: size.width - smallRadius, y: halfLineWidth)) + context.addArc(tangent1End: CGPoint(x: size.width - halfLineWidth, y: halfLineWidth), tangent2End: CGPoint(x: size.width - halfLineWidth, y: halfLineWidth + smallRadius), radius: smallRadius) + context.addLine(to: CGPoint(x: size.width - halfLineWidth, y: size.height - halfLineWidth - smallRadius)) + context.addArc(tangent1End: CGPoint(x: size.width - halfLineWidth, y: size.height - halfLineWidth), tangent2End: CGPoint(x: size.width - halfLineWidth - smallRadius, y: size.height - halfLineWidth), radius: smallRadius) + context.addLine(to: CGPoint(x: halfLineWidth + largeRadius, y: size.height - halfLineWidth)) + context.addArc(tangent1End: CGPoint(x: halfLineWidth, y: size.height - halfLineWidth), tangent2End: CGPoint(x: halfLineWidth, y: size.height - halfLineWidth - largeRadius), radius: largeRadius) + + context.closePath() + context.strokePath() + context.setBlendMode(.copy) + } case .bottomSingle: context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: smallRadius + smallRadius, height: smallRadius + smallRadius))) context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - smallRadius - smallRadius, y: 0.0), size: CGSize(width: smallRadius + smallRadius, height: smallRadius + smallRadius))) context.fill(CGRect(origin: CGPoint(x: smallRadius, y: 0.0), size: CGSize(width: size.width - smallRadius - smallRadius, height: smallRadius + smallRadius))) context.fill(CGRect(origin: CGPoint(x: 0.0, y: smallRadius), size: CGSize(width: size.width, height: size.height - largeRadius - smallRadius))) + + if effectiveStrokeColor != nil { + context.setBlendMode(.normal) + context.beginPath() + context.move(to: CGPoint(x: halfLineWidth, y: smallRadius + halfLineWidth)) + context.addArc(tangent1End: CGPoint(x: halfLineWidth, y: halfLineWidth), tangent2End: CGPoint(x: halfLineWidth + smallRadius, y: halfLineWidth), radius: smallRadius) + context.addLine(to: CGPoint(x: size.width - smallRadius, y: halfLineWidth)) + context.addArc(tangent1End: CGPoint(x: size.width - halfLineWidth, y: halfLineWidth), tangent2End: CGPoint(x: size.width - halfLineWidth, y: halfLineWidth + smallRadius), radius: smallRadius) + context.addLine(to: CGPoint(x: size.width - halfLineWidth, y: size.height - halfLineWidth - largeRadius)) + context.addArc(tangent1End: CGPoint(x: size.width - halfLineWidth, y: size.height - halfLineWidth), tangent2End: CGPoint(x: size.width - halfLineWidth - largeRadius, y: size.height - halfLineWidth), radius: largeRadius) + context.addLine(to: CGPoint(x: halfLineWidth + largeRadius, y: size.height - halfLineWidth)) + context.addArc(tangent1End: CGPoint(x: halfLineWidth, y: size.height - halfLineWidth), tangent2End: CGPoint(x: halfLineWidth, y: size.height - halfLineWidth - largeRadius), radius: largeRadius) + + context.closePath() + context.strokePath() + context.setBlendMode(.copy) + } } })!.stretchableImage(withLeftCapWidth: Int(size.width / 2.0), topCapHeight: Int(size.height / 2.0)) } diff --git a/TelegramUI/ChatMessageBubbleItemNode.swift b/TelegramUI/ChatMessageBubbleItemNode.swift index f60481e44f..350904a97f 100644 --- a/TelegramUI/ChatMessageBubbleItemNode.swift +++ b/TelegramUI/ChatMessageBubbleItemNode.swift @@ -4,46 +4,65 @@ import Display import Postbox import TelegramCore -private func contentNodeClassesForItem(_ item: ChatMessageItem) -> [AnyClass] { - var result: [AnyClass] = [] +private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> [(Message, AnyClass)] { + var result: [(Message, AnyClass)] = [] var skipText = false - for media in item.message.media { - if let _ = media as? TelegramMediaImage { - result.append(ChatMessageMediaBubbleContentNode.self) - } else if let file = media as? TelegramMediaFile { - if file.isVideo || (file.isAnimated && file.dimensions != nil) { - result.append(ChatMessageMediaBubbleContentNode.self) + var addFinalText = false + + for message in item.content { + inner: for media in message.media { + if let _ = media as? TelegramMediaImage { + result.append((message, ChatMessageMediaBubbleContentNode.self)) + } else if let file = media as? TelegramMediaFile { + if file.isVideo || (file.isAnimated && file.dimensions != nil) { + result.append((message, ChatMessageMediaBubbleContentNode.self)) + } else { + result.append((message, ChatMessageFileBubbleContentNode.self)) + } + } else if let action = media as? TelegramMediaAction, case .phoneCall = action.action { + result.append((message, ChatMessageCallBubbleContentNode.self)) + } else if let _ = media as? TelegramMediaMap { + result.append((message, ChatMessageMapBubbleContentNode.self)) + } else if let _ = media as? TelegramMediaGame { + skipText = true + result.append((message, ChatMessageGameBubbleContentNode.self)) + break inner + } else if let _ = media as? TelegramMediaInvoice { + skipText = true + result.append((message, ChatMessageInvoiceBubbleContentNode.self)) + break inner + } else if let _ = media as? TelegramMediaContact { + result.append((message, ChatMessageContactBubbleContentNode.self)) + } + } + + if !message.text.isEmpty { + if !skipText { + if case .group = item.content { + addFinalText = true + skipText = true + } else { + result.append((message, ChatMessageTextBubbleContentNode.self)) + } } else { - result.append(ChatMessageFileBubbleContentNode.self) + if case .group = item.content { + addFinalText = false + } + } + } + + inner: for media in message.media { + if let webpage = media as? TelegramMediaWebpage { + if case .Loaded = webpage.content { + result.append((message, ChatMessageWebpageBubbleContentNode.self)) + } + break inner } - } else if let action = media as? TelegramMediaAction, case .phoneCall = action.action { - result.append(ChatMessageCallBubbleContentNode.self) - } else if let _ = media as? TelegramMediaMap { - result.append(ChatMessageMapBubbleContentNode.self) - } else if let _ = media as? TelegramMediaGame { - skipText = true - result.append(ChatMessageGameBubbleContentNode.self) - break - } else if let _ = media as? TelegramMediaInvoice { - skipText = true - result.append(ChatMessageInvoiceBubbleContentNode.self) - break - } else if let _ = media as? TelegramMediaContact { - result.append(ChatMessageContactBubbleContentNode.self) } } - if !skipText && !item.message.text.isEmpty { - result.append(ChatMessageTextBubbleContentNode.self) - } - - for media in item.message.media { - if let webpage = media as? TelegramMediaWebpage { - if case .Loaded = webpage.content { - result.append(ChatMessageWebpageBubbleContentNode.self) - } - break - } + if addFinalText && !item.content.firstMessage.text.isEmpty { + result.append((item.content.firstMessage, ChatMessageTextBubbleContentNode.self)) } return result @@ -63,10 +82,11 @@ private let inlineBotNameFont = nameFont private let chatMessagePeerIdColors: [UIColor] = [ UIColor(rgb: 0xfc5c51), UIColor(rgb: 0xfa790f), + UIColor(rgb: 0x895dd5), UIColor(rgb: 0x0fb297), + UIColor(rgb: 0x00c0c2), UIColor(rgb: 0x3ca5ec), - UIColor(rgb: 0x3d72ed), - UIColor(rgb: 0x895dd5) + UIColor(rgb: 0x3d72ed) ] class ChatMessageBubbleItemNode: ChatMessageItemView { @@ -74,6 +94,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { private var transitionClippingNode: ASDisplayNode? private var selectionNode: ChatMessageSelectionNode? + private var swipeToReplyNode: ChatMessageSwipeToReplyNode? + private var swipeToReplyFeedback: HapticFeedback? private var nameNode: TextNode? private var forwardInfoNode: ChatMessageForwardInfoNode? @@ -84,13 +106,13 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { private var shareButtonNode: HighlightableButtonNode? - private var messageId: MessageId? - private var messageStableId: UInt32? private var backgroundType: ChatMessageBackgroundType? private var highlightedState: Bool = false private var backgroundFrameTransition: (CGRect, CGRect)? + private var currentSwipeToReplyTranslation: CGFloat = 0.0 + private var appliedItem: ChatMessageItem? override var visibility: ListViewItemNodeVisibility { @@ -187,6 +209,9 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { return .waitForHold(timeout: 0.12, acceptTap: false) } } + if !strongSelf.backgroundNode.frame.contains(point) { + return .waitForSingleTap + } } return .waitForDoubleTap @@ -203,12 +228,26 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { } } self.view.addGestureRecognizer(recognizer) + + let replyRecognizer = ChatSwipeToReplyRecognizer(target: self, action: #selector(self.swipeToReplyGesture(_:))) + replyRecognizer.shouldBegin = { [weak self] in + if let strongSelf = self, let item = strongSelf.item { + if strongSelf.selectionNode != nil { + return false + } + return item.controllerInteraction.canSetupReply() + } + return false + } + self.view.addGestureRecognizer(replyRecognizer) } - override func asyncLayout() -> (_ item: ChatMessageItem, _ width: CGFloat, _ mergedTop: Bool, _ mergedBottom: Bool, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { - var currentContentClassesPropertiesAndLayouts: [(AnyClass, ChatMessageBubbleContentProperties, (_ item: ChatMessageItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ position: ChatMessageBubbleContentPosition, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))))] = [] + override func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { + var currentContentClassesPropertiesAndLayouts: [(Message, AnyClass, Bool, (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))))] = [] for contentNode in self.contentNodes { - currentContentClassesPropertiesAndLayouts.append((type(of: contentNode) as AnyClass, contentNode.properties, contentNode.asyncLayoutContent())) + if let message = contentNode.item?.message { + currentContentClassesPropertiesAndLayouts.append((message, type(of: contentNode) as AnyClass, contentNode.supportsMosaic, contentNode.asyncLayoutContent())) + } } let authorNameLayout = TextNode.asyncLayout(self.nameNode) @@ -216,29 +255,64 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { let replyInfoLayout = ChatMessageReplyInfoNode.asyncLayout(self.replyInfoNode) let actionButtonsLayout = ChatMessageActionButtonsNode.asyncLayout(self.actionButtonsNode) - var currentShareButtonNode = self.shareButtonNode + let currentShareButtonNode = self.shareButtonNode let layoutConstants = self.layoutConstants let currentItem = self.appliedItem - return { item, width, mergedTop, mergedBottom, dateHeaderAtBottom in - let message = item.message - let incoming = item.message.effectivelyIncoming + return { item, params, mergedTop, mergedBottom, dateHeaderAtBottom in + let baseWidth = params.width - params.leftInset - params.rightInset - let displayAuthorInfo = !mergedTop && incoming && item.peerId.isGroupOrChannel && item.message.author != nil + let content = item.content + let firstMessage = content.firstMessage + let incoming = item.content.effectivelyIncoming(item.account.peerId) + + var effectiveAuthor: Peer? + var ignoreForward = false + let displayAuthorInfo: Bool let avatarInset: CGFloat var hasAvatar = false - if item.peerId.isGroupOrChannel && item.message.author != nil { - var isBroadcastChannel = false - if let peer = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { - isBroadcastChannel = true - } + switch item.chatLocation { + case let .peer(peerId): + if item.message.id.peerId == item.account.peerId { + if let forwardInfo = item.content.firstMessage.forwardInfo { + ignoreForward = true + effectiveAuthor = forwardInfo.author + } + displayAuthorInfo = !mergedTop && incoming && effectiveAuthor != nil + } else { + effectiveAuthor = firstMessage.author + displayAuthorInfo = !mergedTop && incoming && peerId.isGroupOrChannel && effectiveAuthor != nil + } - if !isBroadcastChannel { + if peerId != item.account.peerId { + if peerId.isGroupOrChannel && effectiveAuthor != nil { + var isBroadcastChannel = false + if let peer = firstMessage.peers[firstMessage.id.peerId] as? TelegramChannel, case .broadcast = peer.info { + isBroadcastChannel = true + } + + if !isBroadcastChannel { + hasAvatar = true + } + } + } else if incoming { + hasAvatar = true + } + case .group: hasAvatar = true + displayAuthorInfo = true + } + + if let forwardInfo = item.content.firstMessage.forwardInfo, forwardInfo.source == nil, forwardInfo.author.id.namespace == Namespaces.Peer.CloudUser { + for media in item.content.firstMessage.media { + if let file = media as? TelegramMediaFile, file.isMusic { + ignoreForward = true + break + } } } @@ -248,266 +322,15 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { avatarInset = 0.0 } - let tmpWidth = width * layoutConstants.bubble.maximumWidthFillFactor - let maximumContentWidth = floor(tmpWidth - layoutConstants.bubble.edgeInset - layoutConstants.bubble.edgeInset - layoutConstants.bubble.contentInsets.left - layoutConstants.bubble.contentInsets.right - avatarInset) - - var contentPropertiesAndPrepareLayouts: [(ChatMessageBubbleContentProperties, (_ item: ChatMessageItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ position: ChatMessageBubbleContentPosition, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))))] = [] - var addedContentNodes: [ChatMessageBubbleContentNode]? - - let contentNodeClasses = contentNodeClassesForItem(item) - for contentNodeClass in contentNodeClasses { - var found = false - for (currentClass, currentProperties, currentLayout) in currentContentClassesPropertiesAndLayouts { - if currentClass == contentNodeClass { - contentPropertiesAndPrepareLayouts.append((currentProperties, currentLayout)) - found = true + var needShareButton = false + if item.message.id.peerId == item.account.peerId { + for attribute in item.content.firstMessage.attributes { + if let _ = attribute as? SourceReferenceMessageAttribute { + needShareButton = true break } } - if !found { - let contentNode = (contentNodeClass as! ChatMessageBubbleContentNode.Type).init() - contentPropertiesAndPrepareLayouts.append((contentNode.properties, contentNode.asyncLayoutContent())) - if addedContentNodes == nil { - addedContentNodes = [contentNode] - } else { - addedContentNodes!.append(contentNode) - } - } - } - - var authorNameString: String? - var inlineBotNameString: String? - var replyMessage: Message? - var replyMarkup: ReplyMarkupMessageAttribute? - var authorNameColor: UIColor? - - for attribute in message.attributes { - if let attribute = attribute as? InlineBotMessageAttribute, let bot = message.peers[attribute.peerId] as? TelegramUser { - inlineBotNameString = bot.username - } else if let attribute = attribute as? ReplyMessageAttribute { - replyMessage = message.associatedMessages[attribute.messageId] - } else if let attribute = attribute as? ReplyMarkupMessageAttribute, attribute.flags.contains(.inline), !attribute.rows.isEmpty { - replyMarkup = attribute - } - } - - var initialDisplayHeader = true - if inlineBotNameString == nil && message.forwardInfo == nil && replyMessage == nil { - if let first = contentPropertiesAndPrepareLayouts.first, first.0.hidesSimpleAuthorHeader { - initialDisplayHeader = false - } - } - - if initialDisplayHeader && displayAuthorInfo { - if let peer = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { - authorNameString = peer.displayTitle - authorNameColor = chatMessagePeerIdColors[Int(peer.id.id % 6)] - } else if let author = message.author { - authorNameString = author.displayTitle - authorNameColor = chatMessagePeerIdColors[Int(author.id.id % 6)] - } - } - - var displayHeader = false - if initialDisplayHeader { - if authorNameString != nil { - displayHeader = true - } - if inlineBotNameString != nil { - displayHeader = true - } - if message.forwardInfo != nil { - displayHeader = true - } - if replyMessage != nil { - displayHeader = true - } - } - - var contentPropertiesAndLayouts: [(ChatMessageBubbleContentProperties, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void)))] = [] - - let topNodeMergeStatus: ChatMessageBubbleMergeStatus = mergedTop ? (incoming ? .Left : .Right) : .None(incoming ? .Incoming : .Outgoing) - let bottomNodeMergeStatus: ChatMessageBubbleMergeStatus = mergedBottom ? (incoming ? .Left : .Right) : .None(incoming ? .Incoming : .Outgoing) - - let firstNodeTopPosition: ChatMessageBubbleRelativePosition - if displayHeader { - firstNodeTopPosition = .Neighbour - } else { - firstNodeTopPosition = .None(topNodeMergeStatus) - } - let lastNodeTopPosition: ChatMessageBubbleRelativePosition = .None(bottomNodeMergeStatus) - - var maximumNodeWidth = maximumContentWidth - let contentNodeCount = contentPropertiesAndPrepareLayouts.count - var index = 0 - for (properties, prepareLayout) in contentPropertiesAndPrepareLayouts { - let topPosition: ChatMessageBubbleRelativePosition - let bottomPosition: ChatMessageBubbleRelativePosition - - if index == 0 { - topPosition = firstNodeTopPosition - } else { - topPosition = .Neighbour - } - - if index == contentNodeCount - 1 { - bottomPosition = lastNodeTopPosition - } else { - bottomPosition = .Neighbour - } - - let (maxNodeWidth, nodeLayout) = prepareLayout(item, layoutConstants, ChatMessageBubbleContentPosition(top: topPosition, bottom: bottomPosition), CGSize(width: maximumContentWidth, height: CGFloat.greatestFiniteMagnitude)) - maximumNodeWidth = min(maximumNodeWidth, maxNodeWidth) - - contentPropertiesAndLayouts.append((properties, nodeLayout)) - index += 1 - } - - var headerSize = CGSize() - - var nameNodeOriginY: CGFloat = 0.0 - var nameNodeSizeApply: (CGSize, () -> TextNode?) = (CGSize(), { nil }) - - var replyInfoOriginY: CGFloat = 0.0 - var replyInfoSizeApply: (CGSize, () -> ChatMessageReplyInfoNode?) = (CGSize(), { nil }) - - var forwardInfoOriginY: CGFloat = 0.0 - var forwardInfoSizeApply: (CGSize, () -> ChatMessageForwardInfoNode?) = (CGSize(), { nil }) - - if displayHeader { - if authorNameString != nil || inlineBotNameString != nil { - if headerSize.height < CGFloat.ulpOfOne { - headerSize.height += 4.0 - } - - let inlineBotNameColor = incoming ? item.theme.chat.bubble.incomingAccentColor : item.theme.chat.bubble.outgoingAccentColor - - let attributedString: NSAttributedString - if let authorNameString = authorNameString, let authorNameColor = authorNameColor, let inlineBotNameString = inlineBotNameString { - let botPrefixString: NSString = " via " - let mutableString = NSMutableAttributedString(string: "\(authorNameString)\(botPrefixString)@\(inlineBotNameString)", attributes: [NSAttributedStringKey.font: inlineBotNameFont, NSAttributedStringKey.foregroundColor: inlineBotNameColor]) - mutableString.addAttributes([NSAttributedStringKey.font: nameFont, NSAttributedStringKey.foregroundColor: authorNameColor], range: NSMakeRange(0, (authorNameString as NSString).length)) - mutableString.addAttributes([NSAttributedStringKey.font: inlineBotPrefixFont, NSAttributedStringKey.foregroundColor: inlineBotNameColor], range: NSMakeRange((authorNameString as NSString).length, botPrefixString.length)) - attributedString = mutableString - } else if let authorNameString = authorNameString, let authorNameColor = authorNameColor { - attributedString = NSAttributedString(string: authorNameString, font: nameFont, textColor: authorNameColor) - } else if let inlineBotNameString = inlineBotNameString { - attributedString = NSAttributedString(string: "via @\(inlineBotNameString)", font: inlineBotNameFont, textColor: inlineBotNameColor) - } else { - attributedString = NSAttributedString(string: "", font: nameFont, textColor: inlineBotNameColor) - } - - let sizeAndApply = authorNameLayout(attributedString, nil, 1, .end, CGSize(width: maximumNodeWidth, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) - nameNodeSizeApply = (sizeAndApply.0.size, { - return sizeAndApply.1() - }) - nameNodeOriginY = headerSize.height - headerSize.width = max(headerSize.width, nameNodeSizeApply.0.width + layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right) - headerSize.height += nameNodeSizeApply.0.height - } - - if let forwardInfo = message.forwardInfo { - if headerSize.height < CGFloat.ulpOfOne { - headerSize.height += 4.0 - } - let sizeAndApply = forwardInfoLayout(item.theme, incoming, forwardInfo.source == nil ? forwardInfo.author : forwardInfo.source!, forwardInfo.source == nil ? nil : forwardInfo.author, CGSize(width: maximumNodeWidth, height: CGFloat.greatestFiniteMagnitude)) - forwardInfoSizeApply = (sizeAndApply.0, { sizeAndApply.1() }) - - forwardInfoOriginY = headerSize.height - headerSize.width = max(headerSize.width, forwardInfoSizeApply.0.width + layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right) - headerSize.height += forwardInfoSizeApply.0.height - } - - if let replyMessage = replyMessage { - if headerSize.height < CGFloat.ulpOfOne { - headerSize.height += 6.0 - } else { - headerSize.height += 2.0 - } - let sizeAndApply = replyInfoLayout(item.theme, item.account, .bubble(incoming: incoming), replyMessage, CGSize(width: maximumNodeWidth, height: CGFloat.greatestFiniteMagnitude)) - replyInfoSizeApply = (sizeAndApply.0, { sizeAndApply.1() }) - - replyInfoOriginY = headerSize.height - headerSize.width = max(headerSize.width, replyInfoSizeApply.0.width + layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right) - headerSize.height += replyInfoSizeApply.0.height + 2.0 - } - - if headerSize.height > CGFloat.ulpOfOne { - headerSize.height -= 3.0 - } - } - - var removedContentNodeIndices: [Int]? - findRemoved: for i in 0 ..< currentContentClassesPropertiesAndLayouts.count { - let currentClass: AnyClass = currentContentClassesPropertiesAndLayouts[i].0 - for contentNodeClass in contentNodeClasses { - if currentClass == contentNodeClass { - continue findRemoved - } - } - if removedContentNodeIndices == nil { - removedContentNodeIndices = [i] - } else { - removedContentNodeIndices!.append(i) - } - } - - var contentNodePropertiesAndFinalize: [(ChatMessageBubbleContentProperties, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))] = [] - - var maxContentWidth: CGFloat = headerSize.width - for (contentNodeProperties, contentNodeLayout) in contentPropertiesAndLayouts { - let (contentNodeWidth, contentNodeFinalize) = contentNodeLayout(CGSize(width: maximumNodeWidth, height: CGFloat.greatestFiniteMagnitude)) - maxContentWidth = max(maxContentWidth, contentNodeWidth) - - contentNodePropertiesAndFinalize.append((contentNodeProperties, contentNodeFinalize)) - } - - var actionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animated: Bool) -> ChatMessageActionButtonsNode))? - if let replyMarkup = replyMarkup { - let (minWidth, buttonsLayout) = actionButtonsLayout(item.theme, item.strings, replyMarkup, item.message, maximumNodeWidth) - maxContentWidth = max(maxContentWidth, minWidth) - actionButtonsFinalize = buttonsLayout - } - - var contentSize = CGSize(width: maxContentWidth, height: 0.0) - index = 0 - var contentNodeSizesPropertiesAndApply: [(CGSize, ChatMessageBubbleContentProperties, (ListViewItemUpdateAnimation) -> Void)] = [] - for (properties, finalize) in contentNodePropertiesAndFinalize { - let (size, apply) = finalize(maxContentWidth) - contentNodeSizesPropertiesAndApply.append((size, properties, apply)) - - contentSize.height += size.height - - if index == 0 && headerSize.height > CGFloat.ulpOfOne { - contentSize.height += properties.headerSpacing - } - - index += 1 - } - - var actionButtonsSizeAndApply: (CGSize, (Bool) -> ChatMessageActionButtonsNode)? - if let actionButtonsFinalize = actionButtonsFinalize { - actionButtonsSizeAndApply = actionButtonsFinalize(maxContentWidth) - } - - let layoutBubbleSize = CGSize(width: max(contentSize.width, headerSize.width) + layoutConstants.bubble.contentInsets.left + layoutConstants.bubble.contentInsets.right, height: max(layoutConstants.bubble.minimumSize.height, headerSize.height + contentSize.height + layoutConstants.bubble.contentInsets.top + layoutConstants.bubble.contentInsets.bottom)) - - let backgroundFrame = CGRect(origin: CGPoint(x: incoming ? (layoutConstants.bubble.edgeInset + avatarInset) : (width - layoutBubbleSize.width - layoutConstants.bubble.edgeInset), y: 0.0), size: layoutBubbleSize) - - let contentOrigin = CGPoint(x: backgroundFrame.origin.x + (incoming ? layoutConstants.bubble.contentInsets.left : layoutConstants.bubble.contentInsets.right), y: backgroundFrame.origin.y + layoutConstants.bubble.contentInsets.top + headerSize.height) - - var layoutSize = CGSize(width: width, height: layoutBubbleSize.height) - if let actionButtonsSizeAndApply = actionButtonsSizeAndApply { - layoutSize.height += actionButtonsSizeAndApply.0.height - } - - var layoutInsets = UIEdgeInsets(top: mergedTop ? layoutConstants.bubble.mergedSpacing : layoutConstants.bubble.defaultSpacing, left: 0.0, bottom: mergedBottom ? layoutConstants.bubble.mergedSpacing : layoutConstants.bubble.defaultSpacing, right: 0.0) - if dateHeaderAtBottom { - layoutInsets.top += layoutConstants.timestampHeaderHeight - } - - var needShareButton = false - if item.message.effectivelyIncoming { + } else if item.message.effectivelyIncoming(item.account.peerId) { if let peer = item.message.peers[item.message.id.peerId] { if let channel = peer as? TelegramChannel { if case .broadcast = channel.info { @@ -523,46 +346,605 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { if media is TelegramMediaGame || media is TelegramMediaInvoice { needShareButton = true break loop + } else if let media = media as? TelegramMediaWebpage, case .Loaded = media.content { + needShareButton = true + break loop } } } } + var tmpWidth = layoutConstants.bubble.maximumWidthFill.widthFor(baseWidth) + if needShareButton { + tmpWidth -= 32.0 + } + let maximumContentWidth = floor(tmpWidth - layoutConstants.bubble.edgeInset - layoutConstants.bubble.edgeInset - layoutConstants.bubble.contentInsets.left - layoutConstants.bubble.contentInsets.right - avatarInset) + + var contentPropertiesAndPrepareLayouts: [(Message, Bool, (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))))] = [] + var addedContentNodes: [ChatMessageBubbleContentNode]? + + let contentNodeMessagesAndClasses = contentNodeMessagesAndClassesForItem(item) + for (contentNodeMessage, contentNodeClass) in contentNodeMessagesAndClasses { + var found = false + for (currentMessage, currentClass, supportsMosaic, currentLayout) in currentContentClassesPropertiesAndLayouts { + if currentClass == contentNodeClass && currentMessage.stableId == contentNodeMessage.stableId { + contentPropertiesAndPrepareLayouts.append((contentNodeMessage, supportsMosaic, currentLayout)) + found = true + break + } + } + if !found { + let contentNode = (contentNodeClass as! ChatMessageBubbleContentNode.Type).init() + contentPropertiesAndPrepareLayouts.append((contentNodeMessage, contentNode.supportsMosaic, contentNode.asyncLayoutContent())) + if addedContentNodes == nil { + addedContentNodes = [contentNode] + } else { + addedContentNodes!.append(contentNode) + } + } + } + + var authorNameString: String? + var inlineBotNameString: String? + var replyMessage: Message? + var replyMarkup: ReplyMarkupMessageAttribute? + var authorNameColor: UIColor? + + for attribute in firstMessage.attributes { + if let attribute = attribute as? InlineBotMessageAttribute, let bot = firstMessage.peers[attribute.peerId] as? TelegramUser { + inlineBotNameString = bot.username + } else if let attribute = attribute as? ReplyMessageAttribute { + replyMessage = firstMessage.associatedMessages[attribute.messageId] + } else if let attribute = attribute as? ReplyMarkupMessageAttribute, attribute.flags.contains(.inline), !attribute.rows.isEmpty { + replyMarkup = attribute + } + } + + var contentPropertiesAndLayouts: [(CGSize?, ChatMessageBubbleContentProperties, ChatMessageBubblePreparePosition, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void)))] = [] + + let topNodeMergeStatus: ChatMessageBubbleMergeStatus = mergedTop ? (incoming ? .Left : .Right) : .None(incoming ? .Incoming : .Outgoing) + let bottomNodeMergeStatus: ChatMessageBubbleMergeStatus = mergedBottom ? (incoming ? .Left : .Right) : .None(incoming ? .Incoming : .Outgoing) + + var canPossiblyHideBackground = false + if case .color = item.presentationData.wallpaper { + canPossiblyHideBackground = true + } + + var maximumNodeWidth = maximumContentWidth + + let contentNodeCount = contentPropertiesAndPrepareLayouts.count + + let read: Bool + switch item.content { + case let .message(_, value, _): + read = value + case let .group(messages): + read = messages[0].1 + } + + var mosaicStartIndex: Int? + var mosaicRange: Range? + for i in 0 ..< contentPropertiesAndPrepareLayouts.count { + if contentPropertiesAndPrepareLayouts[i].1 { + if mosaicStartIndex == nil { + mosaicStartIndex = i + } + } else if let mosaicStartIndexValue = mosaicStartIndex { + if mosaicStartIndexValue < i - 1 { + mosaicRange = mosaicStartIndexValue ..< i + } + mosaicStartIndex = nil + } + } + if let mosaicStartIndex = mosaicStartIndex { + if mosaicStartIndex < contentPropertiesAndPrepareLayouts.count - 1 { + mosaicRange = mosaicStartIndex ..< contentPropertiesAndPrepareLayouts.count + } + } + + var index = 0 + for (message, _, prepareLayout) in contentPropertiesAndPrepareLayouts { + let topPosition: ChatMessageBubbleRelativePosition + let bottomPosition: ChatMessageBubbleRelativePosition + + topPosition = .Neighbour + bottomPosition = .Neighbour + + let prepareContentPosition: ChatMessageBubblePreparePosition + if let mosaicRange = mosaicRange, mosaicRange.contains(index) { + prepareContentPosition = .mosaic(top: .None(.None(.Incoming)), bottom: index == (mosaicRange.upperBound - 1) ? bottomPosition : .None(.None(.Incoming))) + } else { + let refinedBottomPosition: ChatMessageBubbleRelativePosition + if index == contentPropertiesAndPrepareLayouts.count - 1 { + refinedBottomPosition = .None(.Left) + } else { + refinedBottomPosition = bottomPosition + } + prepareContentPosition = .linear(top: topPosition, bottom: refinedBottomPosition) + } + + let contentItem = ChatMessageBubbleContentItem(account: item.account, controllerInteraction: item.controllerInteraction, message: message, read: read, presentationData: item.presentationData) + + var itemSelection: Bool? + if case .mosaic = prepareContentPosition { + switch content { + case .message: + break + case let .group(messages): + for (m, _, selection) in messages { + if m.id == message.id { + switch selection { + case .none: + break + case let .selectable(selected): + itemSelection = selected + } + break + } + } + } + } + + let (properties, unboundSize, maxNodeWidth, nodeLayout) = prepareLayout(contentItem, layoutConstants, prepareContentPosition, itemSelection, CGSize(width: maximumContentWidth, height: CGFloat.greatestFiniteMagnitude)) + maximumNodeWidth = min(maximumNodeWidth, maxNodeWidth) + + contentPropertiesAndLayouts.append((unboundSize, properties, prepareContentPosition, nodeLayout)) + + if !properties.hidesBackgroundForEmptyWallpapers { + canPossiblyHideBackground = false + } + + index += 1 + } + + var initialDisplayHeader = true + if inlineBotNameString == nil && (ignoreForward || firstMessage.forwardInfo == nil) && replyMessage == nil { + if let first = contentPropertiesAndLayouts.first, first.1.hidesSimpleAuthorHeader { + initialDisplayHeader = false + } + } + + if initialDisplayHeader && displayAuthorInfo { + if let peer = firstMessage.peers[firstMessage.id.peerId] as? TelegramChannel, case .broadcast = peer.info { + authorNameString = peer.displayTitle + authorNameColor = chatMessagePeerIdColors[Int(peer.id.id % 7)] + } else if let effectiveAuthor = effectiveAuthor { + authorNameString = effectiveAuthor.displayTitle + authorNameColor = chatMessagePeerIdColors[Int(effectiveAuthor.id.id % 7)] + } + if let rawAuthorNameColor = authorNameColor { + var dimColors = false + switch item.presentationData.theme.name { + case .builtin(.nightAccent), .builtin(.nightGrayscale): + dimColors = true + default: + break + } + if dimColors { + var hue: CGFloat = 0.0 + var saturation: CGFloat = 0.0 + var brightness: CGFloat = 0.0 + rawAuthorNameColor.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: nil) + authorNameColor = UIColor(hue: hue, saturation: saturation * 0.7, brightness: min(1.0, brightness * 1.2), alpha: 1.0) + } + } + } + + var displayHeader = false + if initialDisplayHeader { + if authorNameString != nil { + displayHeader = true + } + if inlineBotNameString != nil { + displayHeader = true + } + if firstMessage.forwardInfo != nil { + displayHeader = true + } + if replyMessage != nil { + displayHeader = true + } + } + + let firstNodeTopPosition: ChatMessageBubbleRelativePosition + if displayHeader { + firstNodeTopPosition = .Neighbour + } else { + firstNodeTopPosition = .None(topNodeMergeStatus) + } + let lastNodeTopPosition: ChatMessageBubbleRelativePosition = .None(bottomNodeMergeStatus) + + var calculatedGroupFramesAndSize: ([(CGRect, MosaicItemPosition)], CGSize)? + + if let mosaicRange = mosaicRange { + let maxSize = layoutConstants.image.maxDimensions.fittedToWidthOrSmaller(maximumContentWidth - layoutConstants.image.bubbleInsets.left - layoutConstants.image.bubbleInsets.right) + let (innerFramesAndPositions, innerSize) = chatMessageBubbleMosaicLayout(maxSize: maxSize, itemSizes: contentPropertiesAndLayouts[mosaicRange].map { $0.0 ?? CGSize(width: 256.0, height: 256.0) }) + + let framesAndPositions = innerFramesAndPositions.map { ($0.0.offsetBy(dx: layoutConstants.image.bubbleInsets.left, dy: layoutConstants.image.bubbleInsets.top), $0.1) } + + let size = CGSize(width: innerSize.width + layoutConstants.image.bubbleInsets.left + layoutConstants.image.bubbleInsets.right, height: innerSize.height + layoutConstants.image.bubbleInsets.top + layoutConstants.image.bubbleInsets.bottom) + + calculatedGroupFramesAndSize = (framesAndPositions, size) + + maximumNodeWidth = size.width + } + + var headerSize = CGSize() + + var nameNodeOriginY: CGFloat = 0.0 + var nameNodeSizeApply: (CGSize, () -> TextNode?) = (CGSize(), { nil }) + + var replyInfoOriginY: CGFloat = 0.0 + var replyInfoSizeApply: (CGSize, () -> ChatMessageReplyInfoNode?) = (CGSize(), { nil }) + + var forwardInfoOriginY: CGFloat = 0.0 + var forwardInfoSizeApply: (CGSize, () -> ChatMessageForwardInfoNode?) = (CGSize(), { nil }) + + if displayHeader { + if authorNameString != nil || inlineBotNameString != nil { + if headerSize.height.isZero { + headerSize.height += 5.0 + } + + let inlineBotNameColor = incoming ? item.presentationData.theme.chat.bubble.incomingAccentTextColor : item.presentationData.theme.chat.bubble.outgoingAccentTextColor + + let attributedString: NSAttributedString + if let authorNameString = authorNameString, let authorNameColor = authorNameColor, let inlineBotNameString = inlineBotNameString { + + let mutableString = NSMutableAttributedString(string: "\(authorNameString) ", attributes: [NSAttributedStringKey.font: nameFont, NSAttributedStringKey.foregroundColor: authorNameColor]) + let bodyAttributes = MarkdownAttributeSet(font: nameFont, textColor: inlineBotNameColor) + let boldAttributes = MarkdownAttributeSet(font: inlineBotPrefixFont, textColor: inlineBotNameColor) + let botString = addAttributesToStringWithRanges(item.presentationData.strings.Conversation_MessageViaUser("@\(inlineBotNameString)"), body: bodyAttributes, argumentAttributes: [0: boldAttributes]) + mutableString.append(botString) + attributedString = mutableString + } else if let authorNameString = authorNameString, let authorNameColor = authorNameColor { + attributedString = NSAttributedString(string: authorNameString, font: nameFont, textColor: authorNameColor) + } else if let inlineBotNameString = inlineBotNameString { + let bodyAttributes = MarkdownAttributeSet(font: inlineBotPrefixFont, textColor: inlineBotNameColor) + let boldAttributes = MarkdownAttributeSet(font: nameFont, textColor: inlineBotNameColor) + attributedString = addAttributesToStringWithRanges(item.presentationData.strings.Conversation_MessageViaUser("@\(inlineBotNameString)"), body: bodyAttributes, argumentAttributes: [0: boldAttributes]) + } else { + attributedString = NSAttributedString(string: "", font: nameFont, textColor: inlineBotNameColor) + } + + let sizeAndApply = authorNameLayout(TextNodeLayoutArguments(attributedString: attributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: maximumNodeWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + nameNodeSizeApply = (sizeAndApply.0.size, { + return sizeAndApply.1() + }) + nameNodeOriginY = headerSize.height + headerSize.width = max(headerSize.width, nameNodeSizeApply.0.width + layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right) + headerSize.height += nameNodeSizeApply.0.height + } + + if !ignoreForward, let forwardInfo = firstMessage.forwardInfo { + if headerSize.height.isZero { + headerSize.height += 5.0 + } + let forwardSource: Peer + let forwardAuthorSignature: String? + + if let source = forwardInfo.source { + forwardSource = source + if let authorSignature = forwardInfo.authorSignature { + forwardAuthorSignature = authorSignature + } else if forwardInfo.author.id != source.id { + forwardAuthorSignature = forwardInfo.author.displayTitle + } else { + forwardAuthorSignature = nil + } + } else { + forwardSource = forwardInfo.author + forwardAuthorSignature = nil + } + let sizeAndApply = forwardInfoLayout(item.presentationData.theme, item.presentationData.strings, .bubble(incoming: incoming), forwardSource, forwardAuthorSignature, CGSize(width: maximumNodeWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right, height: CGFloat.greatestFiniteMagnitude)) + forwardInfoSizeApply = (sizeAndApply.0, { sizeAndApply.1() }) + + forwardInfoOriginY = headerSize.height + headerSize.width = max(headerSize.width, forwardInfoSizeApply.0.width + layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right) + headerSize.height += forwardInfoSizeApply.0.height + } + + if let replyMessage = replyMessage { + if headerSize.height.isZero { + headerSize.height += 6.0 + } else { + headerSize.height += 2.0 + } + let sizeAndApply = replyInfoLayout(item.presentationData.theme, item.presentationData.strings, item.account, .bubble(incoming: incoming), replyMessage, CGSize(width: maximumNodeWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right, height: CGFloat.greatestFiniteMagnitude)) + replyInfoSizeApply = (sizeAndApply.0, { sizeAndApply.1() }) + + replyInfoOriginY = headerSize.height + headerSize.width = max(headerSize.width, replyInfoSizeApply.0.width + layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right) + headerSize.height += replyInfoSizeApply.0.height + 2.0 + } + + if !headerSize.height.isZero { + headerSize.height -= 5.0 + } + } + + let hideBackground = canPossiblyHideBackground && !displayHeader + + var removedContentNodeIndices: [Int]? + findRemoved: for i in 0 ..< currentContentClassesPropertiesAndLayouts.count { + let currentMessage = currentContentClassesPropertiesAndLayouts[i].0 + let currentClass: AnyClass = currentContentClassesPropertiesAndLayouts[i].1 + for (contentNodeMessage, contentNodeClass) in contentNodeMessagesAndClasses { + if currentClass == contentNodeClass && currentMessage.stableId == contentNodeMessage.stableId { + continue findRemoved + } + } + if removedContentNodeIndices == nil { + removedContentNodeIndices = [i] + } else { + removedContentNodeIndices!.append(i) + } + } + + var contentNodePropertiesAndFinalize: [(ChatMessageBubbleContentProperties, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))] = [] + + var maxContentWidth: CGFloat = headerSize.width + + var actionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animated: Bool) -> ChatMessageActionButtonsNode))? + if let replyMarkup = replyMarkup { + let (minWidth, buttonsLayout) = actionButtonsLayout(item.account, item.presentationData.theme, item.presentationData.strings, replyMarkup, item.message, maximumNodeWidth) + maxContentWidth = max(maxContentWidth, minWidth) + actionButtonsFinalize = buttonsLayout + } + + for i in 0 ..< contentPropertiesAndLayouts.count { + let (_, contentNodeProperties, preparePosition, contentNodeLayout) = contentPropertiesAndLayouts[i] + + if let mosaicRange = mosaicRange, mosaicRange.contains(i), let (framesAndPositions, size) = calculatedGroupFramesAndSize { + let mosaicIndex = i - mosaicRange.lowerBound + + let position = framesAndPositions[mosaicIndex].1 + + let topLeft: ChatMessageBubbleContentMosaicNeighbor + let topRight: ChatMessageBubbleContentMosaicNeighbor + let bottomLeft: ChatMessageBubbleContentMosaicNeighbor + let bottomRight: ChatMessageBubbleContentMosaicNeighbor + + switch firstNodeTopPosition { + case .Neighbour: + topLeft = .merged + topRight = .merged + case let .None(status): + if position.contains(.top) && position.contains(.left) { + switch status { + case .Left: + topLeft = .merged + case .Right: + topLeft = .none(tail: false) + case .None: + topLeft = .none(tail: false) + } + } else { + topLeft = .merged + } + + if position.contains(.top) && position.contains(.right) { + switch status { + case .Left: + topRight = .none(tail: false) + case .Right: + topRight = .merged + case .None: + topRight = .none(tail: false) + } + } else { + topRight = .merged + } + } + + let lastMosaicBottomPosition: ChatMessageBubbleRelativePosition + if mosaicRange.upperBound - 1 == contentNodeCount - 1 { + lastMosaicBottomPosition = lastNodeTopPosition + } else { + lastMosaicBottomPosition = .Neighbour + } + + if position.contains(.bottom), case .Neighbour = lastMosaicBottomPosition { + bottomLeft = .merged + bottomRight = .merged + } else { + switch lastNodeTopPosition { + case .Neighbour: + bottomLeft = .merged + bottomRight = .merged + case let .None(status): + if position.contains(.bottom) && position.contains(.left) { + switch status { + case .Left: + bottomLeft = .merged + case .Right: + bottomLeft = .none(tail: false) + case let .None(tailStatus): + if case .Incoming = tailStatus { + bottomLeft = .none(tail: true) + } else { + bottomLeft = .none(tail: false) + } + } + } else { + bottomLeft = .merged + } + + if position.contains(.bottom) && position.contains(.right) { + switch status { + case .Left: + bottomRight = .none(tail: false) + case .Right: + bottomRight = .merged + case let .None(tailStatus): + if case .Outgoing = tailStatus { + bottomRight = .none(tail: true) + } else { + bottomRight = .none(tail: false) + } + } + } else { + bottomRight = .merged + } + } + } + + var mosaicStatusHorizontalOffset: CGFloat? + if position.contains(.bottom) { + mosaicStatusHorizontalOffset = size.width - framesAndPositions[mosaicIndex].0.maxX + } + + let (_, contentNodeFinalize) = contentNodeLayout(framesAndPositions[mosaicIndex].0.size, .mosaic(position: ChatMessageBubbleContentMosaicPosition(topLeft: topLeft, topRight: topRight, bottomLeft: bottomLeft, bottomRight: bottomRight, mosaicStatusHorizontalOffset: mosaicStatusHorizontalOffset))) + + contentNodePropertiesAndFinalize.append((contentNodeProperties, contentNodeFinalize)) + + maxContentWidth = max(maxContentWidth, size.width) + } else { + let contentPosition: ChatMessageBubbleContentPosition + switch preparePosition { + case .linear: + let topPosition: ChatMessageBubbleRelativePosition + let bottomPosition: ChatMessageBubbleRelativePosition + + if i == 0 { + topPosition = firstNodeTopPosition + } else { + topPosition = .Neighbour + } + + if i == contentNodeCount - 1 { + bottomPosition = lastNodeTopPosition + } else { + bottomPosition = .Neighbour + } + + contentPosition = .linear(top: topPosition, bottom: bottomPosition) + case .mosaic: + assertionFailure() + contentPosition = .linear(top: .Neighbour, bottom: .Neighbour) + } + let (contentNodeWidth, contentNodeFinalize) = contentNodeLayout(CGSize(width: maximumNodeWidth, height: CGFloat.greatestFiniteMagnitude), contentPosition) + maxContentWidth = max(maxContentWidth, contentNodeWidth) + + contentNodePropertiesAndFinalize.append((contentNodeProperties, contentNodeFinalize)) + } + } + + var contentSize = CGSize(width: maxContentWidth, height: 0.0) + var contentNodeFramesPropertiesAndApply: [(CGRect, ChatMessageBubbleContentProperties, (ListViewItemUpdateAnimation) -> Void)] = [] + var contentNodesHeight: CGFloat = 0.0 + for i in 0 ..< contentNodePropertiesAndFinalize.count { + let (properties, finalize) = contentNodePropertiesAndFinalize[i] + + if let mosaicRange = mosaicRange, mosaicRange.contains(i), let (framesAndPositions, size) = calculatedGroupFramesAndSize { + let mosaicIndex = i - mosaicRange.lowerBound + + if mosaicIndex == 0 { + if !headerSize.height.isZero { + contentNodesHeight += 7.0 + } + } + + let (_, apply) = finalize(maxContentWidth) + contentNodeFramesPropertiesAndApply.append((framesAndPositions[mosaicIndex].0.offsetBy(dx: 0.0, dy: contentNodesHeight), properties, apply)) + + if mosaicIndex == mosaicRange.upperBound - 1 { + contentNodesHeight += size.height + } + } else { + if i == 0 && !headerSize.height.isZero { + contentNodesHeight += properties.headerSpacing + } + + let (size, apply) = finalize(maxContentWidth) + contentNodeFramesPropertiesAndApply.append((CGRect(origin: CGPoint(x: 0.0, y: contentNodesHeight), size: size), properties, apply)) + + contentNodesHeight += size.height + } + } + contentSize.height += contentNodesHeight + + var actionButtonsSizeAndApply: (CGSize, (Bool) -> ChatMessageActionButtonsNode)? + if let actionButtonsFinalize = actionButtonsFinalize { + actionButtonsSizeAndApply = actionButtonsFinalize(maxContentWidth) + } + + let layoutBubbleSize = CGSize(width: max(contentSize.width, headerSize.width) + layoutConstants.bubble.contentInsets.left + layoutConstants.bubble.contentInsets.right, height: max(layoutConstants.bubble.minimumSize.height, headerSize.height + contentSize.height + layoutConstants.bubble.contentInsets.top + layoutConstants.bubble.contentInsets.bottom)) + + let backgroundFrame = CGRect(origin: CGPoint(x: incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + avatarInset) : (params.width - params.rightInset - layoutBubbleSize.width - layoutConstants.bubble.edgeInset), y: 0.0), size: layoutBubbleSize) + + let contentOrigin = CGPoint(x: backgroundFrame.origin.x + (incoming ? layoutConstants.bubble.contentInsets.left : layoutConstants.bubble.contentInsets.right), y: backgroundFrame.origin.y + layoutConstants.bubble.contentInsets.top + headerSize.height) + + var layoutSize = CGSize(width: params.width, height: layoutBubbleSize.height) + if let actionButtonsSizeAndApply = actionButtonsSizeAndApply { + layoutSize.height += actionButtonsSizeAndApply.0.height + } + + var layoutInsets = UIEdgeInsets(top: mergedTop ? layoutConstants.bubble.mergedSpacing : layoutConstants.bubble.defaultSpacing, left: 0.0, bottom: mergedBottom ? layoutConstants.bubble.mergedSpacing : layoutConstants.bubble.defaultSpacing, right: 0.0) + if dateHeaderAtBottom { + layoutInsets.top += layoutConstants.timestampHeaderHeight + } + var updatedShareButtonBackground: UIImage? var updatedShareButtonNode: HighlightableButtonNode? if needShareButton { if currentShareButtonNode != nil { updatedShareButtonNode = currentShareButtonNode - if item.theme !== currentItem?.theme { - updatedShareButtonBackground = PresentationResourcesChat.chatBubbleShareButtonImage(item.theme) + if item.presentationData.theme !== currentItem?.presentationData.theme { + if item.message.id.peerId == item.account.peerId { + updatedShareButtonBackground = PresentationResourcesChat.chatBubbleNavigateButtonImage(item.presentationData.theme) + } else { + updatedShareButtonBackground = PresentationResourcesChat.chatBubbleShareButtonImage(item.presentationData.theme) + } } } else { let buttonNode = HighlightableButtonNode() - buttonNode.setBackgroundImage(PresentationResourcesChat.chatBubbleShareButtonImage(item.theme), for: [.normal]) + let buttonIcon: UIImage? + if item.message.id.peerId == item.account.peerId { + buttonIcon = PresentationResourcesChat.chatBubbleNavigateButtonImage(item.presentationData.theme) + } else { + buttonIcon = PresentationResourcesChat.chatBubbleShareButtonImage(item.presentationData.theme) + } + buttonNode.setBackgroundImage(buttonIcon, for: [.normal]) updatedShareButtonNode = buttonNode } } let layout = ListViewItemNodeLayout(contentSize: layoutSize, insets: layoutInsets) - let graphics = PresentationResourcesChat.principalGraphics(item.theme) + let graphics = PresentationResourcesChat.principalGraphics(item.presentationData.theme) + + var udpdatedMergedTop = mergedBottom + var udpdatedMergedBottom = mergedTop + if mosaicRange == nil { + if headerSize.height.isZero, contentNodePropertiesAndFinalize.first?.0.forceFullCorners ?? false { + udpdatedMergedTop = false + } + if contentNodePropertiesAndFinalize.first?.0.forceFullCorners ?? false { + udpdatedMergedBottom = false + } + } return (layout, { [weak self] animation in if let strongSelf = self { strongSelf.appliedItem = item - strongSelf.messageId = message.id - strongSelf.messageStableId = message.stableId - - let mergeType = ChatMessageBackgroundMergeType(top: mergedBottom, bottom: mergedTop) - let backgroundType: ChatMessageBackgroundType - if !incoming { - backgroundType = .Outgoing(mergeType) - } else { - backgroundType = .Incoming(mergeType) + var transition: ContainedViewLayoutTransition = .immediate + if case let .System(duration) = animation { + transition = .animated(duration: duration, curve: .spring) } - strongSelf.backgroundNode.setType(type: backgroundType, highlighted: strongSelf.highlightedState, graphics: graphics) + + let mergeType = ChatMessageBackgroundMergeType(top: udpdatedMergedTop, bottom: udpdatedMergedBottom, side: actionButtonsSizeAndApply != nil) + let backgroundType: ChatMessageBackgroundType + if hideBackground { + backgroundType = .none + } else if !incoming { + backgroundType = .outgoing(mergeType) + } else { + backgroundType = .incoming(mergeType) + } + strongSelf.backgroundNode.setType(type: backgroundType, highlighted: strongSelf.highlightedState, graphics: graphics, transition: transition) strongSelf.backgroundType = backgroundType @@ -623,7 +1005,14 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { if let removedContentNodeIndices = removedContentNodeIndices { for index in removedContentNodeIndices.reversed() { - updatedContentNodes[index].removeFromSupernode() + let node = updatedContentNodes[index] + if animation.isAnimated { + node.animateRemovalFromBubble(0.2, completion: { [weak node] in + node?.removeFromSupernode() + }) + } else { + node.removeFromSupernode() + } let _ = updatedContentNodes.remove(at: index) } } @@ -632,7 +1021,6 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { for contentNode in addedContentNodes { updatedContentNodes.append(contentNode) strongSelf.addSubnode(contentNode) - contentNode.controllerInteraction = strongSelf.controllerInteraction contentNode.visibility = strongSelf.visibility } @@ -641,15 +1029,12 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { strongSelf.contentNodes = updatedContentNodes } - var contentNodeOrigin = contentOrigin var contentNodeIndex = 0 - for (size, properties, apply) in contentNodeSizesPropertiesAndApply { + for (relativeFrame, _, apply) in contentNodeFramesPropertiesAndApply { apply(animation) - if contentNodeIndex == 0 && headerSize.height > CGFloat.ulpOfOne { - contentNodeOrigin.y += properties.headerSpacing - } + let contentNode = strongSelf.contentNodes[contentNodeIndex] - let contentNodeFrame = CGRect(origin: contentNodeOrigin, size: size) + let contentNodeFrame = relativeFrame.offsetBy(dx: contentOrigin.x, dy: contentOrigin.y) let previousContentNodeFrame = contentNode.frame contentNode.frame = contentNodeFrame @@ -676,7 +1061,6 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { } } contentNodeIndex += 1 - contentNodeOrigin.y += size.height } if let updatedShareButtonNode = updatedShareButtonNode { @@ -716,8 +1100,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { } strongSelf.disableTransitionClippingNode() } - let offset: CGFloat = incoming ? 42.0 : 0.0 - strongSelf.selectionNode?.frame = CGRect(origin: CGPoint(x: -offset, y: 0.0), size: CGSize(width: width, height: layout.size.height)) + let offset: CGFloat = params.leftInset + (incoming ? 42.0 : 0.0) + strongSelf.selectionNode?.frame = CGRect(origin: CGPoint(x: -offset, y: 0.0), size: CGSize(width: params.width, height: layout.size.height)) if let actionButtonsSizeAndApply = actionButtonsSizeAndApply { var animated = false @@ -819,7 +1203,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { if let transitionClippingNode = self.transitionClippingNode { var fixedBackgroundFrame = backgroundFrame - fixedBackgroundFrame = fixedBackgroundFrame.insetBy(dx: 0.0, dy: 1.0) + fixedBackgroundFrame = fixedBackgroundFrame.insetBy(dx: 0.0, dy: self.backgroundNode.type == ChatMessageBackgroundType.none ? 0.0 : 1.0) transitionClippingNode.frame = fixedBackgroundFrame transitionClippingNode.bounds = CGRect(origin: CGPoint(x: fixedBackgroundFrame.origin.x, y: fixedBackgroundFrame.origin.y), size: fixedBackgroundFrame.size) @@ -840,7 +1224,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { case .began: if let (gesture, _) = recognizer.lastRecognizedGestureAndLocation, case .hold = gesture { if let item = self.item, item.message.containsSecretMedia { - self.controllerInteraction?.openSecretMessagePreview(item.message.id) + item.controllerInteraction.openSecretMessagePreview(item.message.id) } } case .ended: @@ -848,8 +1232,9 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { switch gesture { case .tap: if let avatarNode = self.accessoryItemNode as? ChatMessageAvatarAccessoryItemNode, avatarNode.frame.contains(location) { - if let item = self.item, let author = item.message.author { - self.controllerInteraction?.openPeer(author.id, .info, item.message.id) + + if let item = self.item, let author = item.content.firstMessage.author { + item.controllerInteraction.openPeer(item.effectiveAuthorId ?? author.id, .info, item.message.id) } return } @@ -858,7 +1243,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { if let item = self.item { for attribute in item.message.attributes { if let attribute = attribute as? InlineBotMessageAttribute, let botPeer = item.message.peers[attribute.peerId], let addressName = botPeer.addressName { - self.controllerInteraction?.updateInputState { textInputState in + item.controllerInteraction.updateInputState { textInputState in return ChatTextInputState(inputText: "@" + addressName + " ") } return @@ -869,7 +1254,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { if let item = self.item { for attribute in item.message.attributes { if let attribute = attribute as? ReplyMessageAttribute { - self.controllerInteraction?.navigateToMessage(item.message.id, attribute.messageId) + item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId) return } } @@ -878,9 +1263,9 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { if let forwardInfoNode = self.forwardInfoNode, forwardInfoNode.frame.contains(location) { if let item = self.item, let forwardInfo = item.message.forwardInfo { if let sourceMessageId = forwardInfo.sourceMessageId { - self.controllerInteraction?.navigateToMessage(item.message.id, sourceMessageId) + item.controllerInteraction.navigateToMessage(item.message.id, sourceMessageId) } else { - self.controllerInteraction?.openPeer(forwardInfo.source?.id ?? forwardInfo.author.id, .chat(textInputState: nil), nil) + item.controllerInteraction.openPeer(forwardInfo.source?.id ?? forwardInfo.author.id, .chat(textInputState: nil), nil) } return } @@ -893,119 +1278,122 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { break case let .url(url): foundTapAction = true - if let controllerInteraction = self.controllerInteraction { - controllerInteraction.openUrl(url) - } + self.item?.controllerInteraction.openUrl(url) break loop case let .peerMention(peerId, _): foundTapAction = true - if let controllerInteraction = self.controllerInteraction { - controllerInteraction.openPeer(peerId, .chat(textInputState: nil), nil) - } + self.item?.controllerInteraction.openPeer(peerId, .chat(textInputState: nil), nil) break loop case let .textMention(name): foundTapAction = true - if let controllerInteraction = self.controllerInteraction { - controllerInteraction.openPeerMention(name) - } + self.item?.controllerInteraction.openPeerMention(name) break loop case let .botCommand(command): foundTapAction = true - if let item = self.item, let controllerInteraction = self.controllerInteraction { - controllerInteraction.sendBotCommand(item.message.id, command) + if let item = self.item { + item.controllerInteraction.sendBotCommand(item.message.id, command) } break loop case let .hashtag(peerName, hashtag): foundTapAction = true - if let controllerInteraction = self.controllerInteraction { - controllerInteraction.openHashtag(peerName, hashtag) - } + self.item?.controllerInteraction.openHashtag(peerName, hashtag) break loop case .instantPage: foundTapAction = true - if let item = self.item, let controllerInteraction = self.controllerInteraction { - controllerInteraction.openInstantPage(item.message.id) + if let item = self.item { + item.controllerInteraction.openInstantPage(item.message.id) } break loop case .holdToPreviewSecretMedia: foundTapAction = true case let .call(peerId): foundTapAction = true - if let controllerInteraction = self.controllerInteraction { - controllerInteraction.callPeer(peerId) - } + self.item?.controllerInteraction.callPeer(peerId) break loop } } if !foundTapAction { - self.controllerInteraction?.clickThroughMessage() + self.item?.controllerInteraction.clickThroughMessage() } case .longTap, .doubleTap: if let item = self.item, self.backgroundNode.frame.contains(location) { var foundTapAction = false + var tapMessageId: MessageId? = item.content.firstMessage.id loop: for contentNode in self.contentNodes { + if !contentNode.frame.contains(location) { + continue loop + } + tapMessageId = contentNode.item?.message.id let tapAction = contentNode.tapActionAtPoint(CGPoint(x: location.x - contentNode.frame.minX, y: location.y - contentNode.frame.minY)) switch tapAction { case .none, .ignore: break case let .url(url): foundTapAction = true - if let controllerInteraction = self.controllerInteraction { - controllerInteraction.longTap(.url(url)) - } + item.controllerInteraction.longTap(.url(url)) break loop case let .peerMention(peerId, mention): foundTapAction = true - if let controllerInteraction = self.controllerInteraction { - controllerInteraction.longTap(.peerMention(peerId, mention)) - } + item.controllerInteraction.longTap(.peerMention(peerId, mention)) break loop case let .textMention(name): foundTapAction = true - if let controllerInteraction = self.controllerInteraction { - controllerInteraction.longTap(.mention(name)) - } + item.controllerInteraction.longTap(.mention(name)) break loop case let .botCommand(command): foundTapAction = true - if let item = self.item, let controllerInteraction = self.controllerInteraction { - controllerInteraction.longTap(.command(command)) - } + item.controllerInteraction.longTap(.command(command)) break loop case let .hashtag(_, hashtag): foundTapAction = true - if let controllerInteraction = self.controllerInteraction { - controllerInteraction.longTap(.hashtag(hashtag)) - } + item.controllerInteraction.longTap(.hashtag(hashtag)) break loop case .instantPage: break case .holdToPreviewSecretMedia: break - case let .call(peerId): + case .call: break } } - if !foundTapAction { - self.controllerInteraction?.openMessageContextMenu(item.message.id, self, self.backgroundNode.frame) + if !foundTapAction, let tapMessageId = tapMessageId { + item.controllerInteraction.openMessageContextMenu(tapMessageId, self, self.backgroundNode.frame) } } case .hold: if let item = self.item, item.message.containsSecretMedia { - self.controllerInteraction?.closeSecretMessagePreview() + item.controllerInteraction.closeSecretMessagePreview() } } } case .cancelled: if let item = self.item, item.message.containsSecretMedia { - self.controllerInteraction?.closeSecretMessagePreview() + item.controllerInteraction.closeSecretMessagePreview() } default: break } } + private func traceSelectionNodes(parent: ASDisplayNode, point: CGPoint) -> ASDisplayNode? { + if let parent = parent as? GridMessageSelectionNode, parent.bounds.contains(point) { + return parent + } else { + for subnode in parent.subnodes { + let subnodeFrame = subnode.frame + if let result = traceSelectionNodes(parent: subnode, point: point.offsetBy(dx: -subnodeFrame.minX, dy: -subnodeFrame.minY)) { + return result + } + } + return nil + } + } + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if !self.bounds.contains(point) { + return nil + } + if let shareButtonNode = self.shareButtonNode, shareButtonNode.frame.contains(point) { return shareButtonNode.view } @@ -1015,9 +1403,13 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { } if let selectionNode = self.selectionNode { + if let result = self.traceSelectionNodes(parent: self, point: point.offsetBy(dx: -42.0, dy: 0.0)) { + return result.view + } + var selectionNodeFrame = selectionNode.frame - //selectionNodeFrame.origin.x -= 42.0 - selectionNodeFrame.size.width += 42.0 + selectionNodeFrame.origin.x -= 42.0 + selectionNodeFrame.size.width += 42.0 * 2.0 if selectionNodeFrame.contains(point) { return selectionNode.view } else { @@ -1027,7 +1419,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { if !self.backgroundNode.frame.contains(point) { if self.actionButtonsNode == nil || !self.actionButtonsNode!.frame.contains(point) { - return nil + //return nil } } @@ -1035,44 +1427,57 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { } override func transitionNode(id: MessageId, media: Media) -> ASDisplayNode? { - if let item = self.item, item.message.id == id { - for contentNode in self.contentNodes { - if let result = contentNode.transitionNode(media: media) { - return result - } + for contentNode in self.contentNodes { + if let result = contentNode.transitionNode(messageId: id, media: media) { + return result } } return nil } override func updateHiddenMedia() { - if let item = self.item, let controllerInteraction = self.controllerInteraction { + if let item = self.item { for contentNode in self.contentNodes { - contentNode.updateHiddenMedia(controllerInteraction.hiddenMedia[item.message.id]) + if let contentItem = contentNode.item { + contentNode.updateHiddenMedia(item.controllerInteraction.hiddenMedia[contentItem.message.id]) + } } } } override func updateAutomaticMediaDownloadSettings() { - if let item = self.item, let controllerInteraction = self.controllerInteraction { + if let item = self.item { for contentNode in self.contentNodes { - contentNode.updateAutomaticMediaDownloadSettings(controllerInteraction.automaticMediaDownloadSettings) + contentNode.updateAutomaticMediaDownloadSettings(item.controllerInteraction.automaticMediaDownloadSettings) } } } override func updateSelectionState(animated: Bool) { - guard let controllerInteraction = self.controllerInteraction else { + guard let item = self.item else { return } - if let selectionState = controllerInteraction.selectionState { + if let selectionState = item.controllerInteraction.selectionState { var selected = false var incoming = true - if let item = self.item { - selected = selectionState.selectedIds.contains(item.message.id) - incoming = item.message.effectivelyIncoming + + switch item.content { + case let .message(message, _, _): + selected = selectionState.selectedIds.contains(message.id) + case let .group(messages: messages): + var allSelected = !messages.isEmpty + for (message, _, _) in messages { + if !selectionState.selectedIds.contains(message.id) { + allSelected = false + break + } + } + selected = allSelected } + + incoming = item.message.effectivelyIncoming(item.account.peerId) + let offset: CGFloat = incoming ? 42.0 : 0.0 if let selectionNode = self.selectionNode { @@ -1080,9 +1485,14 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { selectionNode.frame = CGRect(origin: CGPoint(x: -offset, y: 0.0), size: CGSize(width: self.contentBounds.size.width, height: self.contentBounds.size.height)) self.subnodeTransform = CATransform3DMakeTranslation(offset, 0.0, 0.0); } else { - let selectionNode = ChatMessageSelectionNode(toggle: { [weak self] in + let selectionNode = ChatMessageSelectionNode(theme: item.presentationData.theme, toggle: { [weak self] value in if let strongSelf = self, let item = strongSelf.item { - strongSelf.controllerInteraction?.toggleMessageSelection(item.message.id) + switch item.content { + case let .message(message, _, _): + item.controllerInteraction.toggleMessagesSelection([message.id], value) + case let .group(messages): + item.controllerInteraction.toggleMessagesSelection(messages.map { $0.0.id }, value) + } } }) @@ -1124,30 +1534,33 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { } override func updateHighlightedState(animated: Bool) { - if let controllerInteraction = self.controllerInteraction, let item = self.item { + if let item = self.item { var highlighted = false - if let messageStableId = self.messageStableId, let highlightedState = controllerInteraction.highlightedState { - if highlightedState.messageStableId == messageStableId { - highlighted = true + if let highlightedState = item.controllerInteraction.highlightedState { + for message in item.content { + if highlightedState.messageStableId == message.stableId { + highlighted = true + break + } } } if self.highlightedState != highlighted { self.highlightedState = highlighted if let backgroundType = self.backgroundType { - let graphics = PresentationResourcesChat.principalGraphics(item.theme) + let graphics = PresentationResourcesChat.principalGraphics(item.presentationData.theme) if highlighted { - self.backgroundNode.setType(type: backgroundType, highlighted: true, graphics: graphics) + self.backgroundNode.setType(type: backgroundType, highlighted: true, graphics: graphics, transition: .immediate) } else { if let previousContents = self.backgroundNode.layer.contents, animated { - self.backgroundNode.setType(type: backgroundType, highlighted: false, graphics: graphics) + self.backgroundNode.setType(type: backgroundType, highlighted: false, graphics: graphics, transition: .immediate) if let updatedContents = self.backgroundNode.layer.contents { self.backgroundNode.layer.animate(from: previousContents as AnyObject, to: updatedContents as AnyObject, keyPath: "contents", timingFunction: kCAMediaTimingFunctionEaseInEaseOut, duration: 0.42) } } else { - self.backgroundNode.setType(type: backgroundType, highlighted: false, graphics: graphics) + self.backgroundNode.setType(type: backgroundType, highlighted: false, graphics: graphics, transition: .immediate) } } } @@ -1156,20 +1569,20 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { } private func performMessageButtonAction(button: ReplyMarkupButton) { - if let item = self.item, let controllerInteraction = self.controllerInteraction { + if let item = self.item { switch button.action { case .text: - controllerInteraction.sendMessage(button.title) + item.controllerInteraction.sendMessage(button.title) case let .url(url): - controllerInteraction.openUrl(url) + item.controllerInteraction.openUrl(url) case .requestMap: - controllerInteraction.shareCurrentLocation() + item.controllerInteraction.shareCurrentLocation() case .requestPhone: - controllerInteraction.shareAccountContact() + item.controllerInteraction.shareAccountContact() case .openWebApp: - controllerInteraction.requestMessageActionCallback(item.message.id, nil, true) + item.controllerInteraction.requestMessageActionCallback(item.message.id, nil, true) case let .callback(data): - controllerInteraction.requestMessageActionCallback(item.message.id, data, false) + item.controllerInteraction.requestMessageActionCallback(item.message.id, data, false) case let .switchInline(samePeer, query): var botPeer: Peer? @@ -1189,17 +1602,86 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { peerId = item.message.id.peerId } if let botPeer = botPeer, let addressName = botPeer.addressName { - controllerInteraction.openPeer(peerId, .chat(textInputState: ChatTextInputState(inputText: "@\(addressName) \(query)")), nil) + item.controllerInteraction.openPeer(peerId, .chat(textInputState: ChatTextInputState(inputText: "@\(addressName) \(query)")), nil) } case .payment: - controllerInteraction.openCheckoutOrReceipt(item.message.id) + item.controllerInteraction.openCheckoutOrReceipt(item.message.id) } } } @objc func shareButtonPressed() { - if let item = self.item, let controllerInteraction = self.controllerInteraction { - controllerInteraction.openMessageShareMenu(item.message.id) + if let item = self.item { + if item.content.firstMessage.id.peerId == item.account.peerId { + for attribute in item.content.firstMessage.attributes { + if let attribute = attribute as? SourceReferenceMessageAttribute { + item.controllerInteraction.navigateToMessage(item.content.firstMessage.id, attribute.messageId) + break + } + } + } else { + item.controllerInteraction.openMessageShareMenu(item.message.id) + } + } + } + + @objc func swipeToReplyGesture(_ recognizer: ChatSwipeToReplyRecognizer) { + switch recognizer.state { + case .began: + self.currentSwipeToReplyTranslation = 0.0 + if self.swipeToReplyFeedback == nil { + self.swipeToReplyFeedback = HapticFeedback() + self.swipeToReplyFeedback?.prepareImpact() + } + (self.view.window as? WindowHost)?.cancelInteractiveKeyboardGestures() + case .changed: + let translation = recognizer.translation(in: self.view) + var animateReplyNodeIn = false + if (translation.x < -45.0) != (self.currentSwipeToReplyTranslation < -45.0) { + if translation.x < -45.0, self.swipeToReplyNode == nil, let item = self.item { + self.swipeToReplyFeedback?.impact() + + let swipeToReplyNode = ChatMessageSwipeToReplyNode(fillColor: item.presentationData.theme.chat.bubble.shareButtonFillColor, strokeColor: item.presentationData.theme.chat.bubble.shareButtonStrokeColor, foregroundColor: item.presentationData.theme.chat.bubble.shareButtonForegroundColor) + self.swipeToReplyNode = swipeToReplyNode + self.addSubnode(swipeToReplyNode) + animateReplyNodeIn = true + } + } + self.currentSwipeToReplyTranslation = translation.x + var bounds = self.bounds + bounds.origin.x = -translation.x + self.bounds = bounds + + if let swipeToReplyNode = self.swipeToReplyNode { + swipeToReplyNode.frame = CGRect(origin: CGPoint(x: bounds.size.width, y: floor((self.contentSize.height - 33.0) / 2.0)), size: CGSize(width: 33.0, height: 33.0)) + if animateReplyNodeIn { + swipeToReplyNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.12) + swipeToReplyNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4) + } + } + case .cancelled, .ended: + self.swipeToReplyFeedback = nil + + let translation = recognizer.translation(in: self.view) + if case .ended = recognizer.state, translation.x < -45.0 { + if let item = self.item { + item.controllerInteraction.setupReply(item.message.id) + } + } + var bounds = self.bounds + let previousBounds = bounds + bounds.origin.x = 0.0 + self.bounds = bounds + self.layer.animateBounds(from: previousBounds, to: bounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + if let swipeToReplyNode = self.swipeToReplyNode { + self.swipeToReplyNode = nil + swipeToReplyNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak swipeToReplyNode] _ in + swipeToReplyNode?.removeFromSupernode() + }) + swipeToReplyNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + } + default: + break } } } diff --git a/TelegramUI/ChatMessageBubbleMosaicLayout.swift b/TelegramUI/ChatMessageBubbleMosaicLayout.swift new file mode 100644 index 0000000000..8694f4a219 --- /dev/null +++ b/TelegramUI/ChatMessageBubbleMosaicLayout.swift @@ -0,0 +1,333 @@ +import Foundation +import UIKit +import Postbox +import TelegramCore +import Display + +struct MosaicItemPosition: OptionSet { + var rawValue: Int32 + + init(rawValue: Int32) { + self.rawValue = rawValue + } + + static let none = MosaicItemPosition(rawValue: 0) + static let top = MosaicItemPosition(rawValue: 1) + static let bottom = MosaicItemPosition(rawValue: 2) + static let left = MosaicItemPosition(rawValue: 4) + static let right = MosaicItemPosition(rawValue: 8) + static let inside = MosaicItemPosition(rawValue: 16) + static let unknown = MosaicItemPosition(rawValue: 65536) +} + +private struct MosaicItemInfo { + let index: Int + let imageSize: CGSize + let aspectRatio: CGFloat + + var layoutFrame: CGRect = CGRect() + var position: MosaicItemPosition = [] +} + +private struct MosaicLayoutAttempt { + let lineCounts: [Int] + let heights: [CGFloat] +} + +func chatMessageBubbleMosaicLayout(maxSize: CGSize, itemSizes: [CGSize]) -> ([(CGRect, MosaicItemPosition)], CGSize) { + var larger: Bool = false + + let spacing: CGFloat = 2.0 + + var proportions = "" + var averageAspectRatio: CGFloat = 1.0 + var forceCalc = false + + var itemInfos = itemSizes.enumerated().map { index, itemSize -> MosaicItemInfo in + let aspectRatio = itemSize.width / itemSize.height + if aspectRatio > 1.2 { + proportions += "w" + } else if aspectRatio < 0.8 { + proportions += "n" + } else { + proportions += "q" + } + + if aspectRatio > 2.0 { + forceCalc = true + } + averageAspectRatio += aspectRatio + + return MosaicItemInfo(index: index, imageSize: itemSize, aspectRatio: aspectRatio , layoutFrame: CGRect(), position: []) + } + + let minWidth: CGFloat = 68.0 + let maxAspectRatio = maxSize.width / maxSize.height + if !itemInfos.isEmpty { + averageAspectRatio = averageAspectRatio / CGFloat(itemInfos.count) + } + + if !forceCalc { + if itemInfos.count == 2 { + if proportions == "ww" && averageAspectRatio > 1.4 * maxAspectRatio && itemInfos[1].aspectRatio - itemInfos[0].aspectRatio < 0.2 { + let width = maxSize.width + let height = floorToScreenPixels(min(width / itemInfos[0].aspectRatio, min(width / itemInfos[1].aspectRatio, (maxSize.height - spacing) / 2.0))) + + itemInfos[0].layoutFrame = CGRect(x: 0.0, y: 0.0, width: width, height: height) + itemInfos[0].position = [.top, .left, .right] + + itemInfos[1].layoutFrame = CGRect(x: 0.0, y: height + spacing, width: width, height: height) + itemInfos[1].position = [.bottom, .left, .right] + } else if proportions == "ww" || proportions == "qq" { + let width = (maxSize.width - spacing) / 2.0 + let height = floorToScreenPixels(min(width / itemInfos[0].aspectRatio, min(width / itemInfos[1].aspectRatio, maxSize.height))) + + itemInfos[0].layoutFrame = CGRect(x: 0.0, y: 0.0, width: width, height: height) + itemInfos[0].position = [.top, .left, .bottom] + + itemInfos[1].layoutFrame = CGRect(x: width + spacing, y: 0.0, width: width, height: height) + itemInfos[1].position = [.top, .right, .bottom] + } else { + let secondWidth = floorToScreenPixels(min(0.5 * (maxSize.width - spacing), round((maxSize.width - spacing) / itemInfos[0].aspectRatio / (1.0 / itemInfos[0].aspectRatio + 1.0 / itemInfos[1].aspectRatio)))) + let firstWidth = maxSize.width - secondWidth - spacing + let height = floorToScreenPixels(min(maxSize.height, round(min(firstWidth / itemInfos[0].aspectRatio, secondWidth / itemInfos[1].aspectRatio)))) + + itemInfos[0].layoutFrame = CGRect(x: 0.0, y: 0.0, width: firstWidth, height: height) + itemInfos[0].position = [.top, .left, .bottom] + + itemInfos[1].layoutFrame = CGRect(x: firstWidth + spacing, y: 0.0, width: secondWidth, height: height) + itemInfos[1].position = [.top, .right, .bottom] + } + } else if (itemInfos.count == 3) { + if proportions.hasPrefix("n") { + let firstHeight = maxSize.height + + let thirdHeight = min((maxSize.height - spacing) * 0.5, round(itemInfos[1].aspectRatio * (maxSize.width - spacing) / (itemInfos[2].aspectRatio + itemInfos[1].aspectRatio))) + let secondHeight = maxSize.height - thirdHeight - spacing + let rightWidth = max(minWidth, min((maxSize.width - spacing) * 0.5, round(min(thirdHeight * itemInfos[2].aspectRatio, secondHeight * itemInfos[1].aspectRatio)))) + + let leftWidth = round(min(firstHeight * itemInfos[0].aspectRatio, (maxSize.width - spacing - rightWidth))) + itemInfos[0].layoutFrame = CGRect(x: 0.0, y: 0.0, width: leftWidth, height: firstHeight) + itemInfos[0].position = [.top, .left, .bottom] + + itemInfos[1].layoutFrame = CGRect(x: leftWidth + spacing, y: 0.0, width: rightWidth, height: secondHeight) + itemInfos[1].position = [.right, .top] + + itemInfos[2].layoutFrame = CGRect(x: leftWidth + spacing, y: secondHeight + spacing, width: rightWidth, height: thirdHeight) + itemInfos[2].position = [.right, .bottom] + } else { + var width = maxSize.width + let firstHeight = floorToScreenPixels(min(width / itemInfos[0].aspectRatio, (maxSize.height - spacing) * 0.66)) + itemInfos[0].layoutFrame = CGRect(x: 0.0, y: 0.0, width: width, height: firstHeight) + itemInfos[0].position = [.top, .left, .right] + + width = (maxSize.width - spacing) / 2.0 + let secondHeight = min(maxSize.height - firstHeight - spacing, round(min(width / itemInfos[1].aspectRatio, width / itemInfos[2].aspectRatio))) + itemInfos[1].layoutFrame = CGRect(x: 0.0, y: firstHeight + spacing, width: width, height: secondHeight) + itemInfos[1].position = [.left, .bottom] + + itemInfos[2].layoutFrame = CGRect(x: width + spacing, y: firstHeight + spacing, width: width, height: secondHeight) + itemInfos[2].position = [.right, .bottom] + } + } else if itemInfos.count == 4 { + if proportions == "wwww" || proportions.hasPrefix("w") { + let w = maxSize.width + let h0 = round(min(w / itemInfos[0].aspectRatio, (maxSize.height - spacing) * 0.66)) + itemInfos[0].layoutFrame = CGRect(x: 0.0, y: 0.0, width: w, height: h0) + itemInfos[0].position = [.top, .left, .right] + + var h = round((maxSize.width - 2 * spacing) / (itemInfos[1].aspectRatio + itemInfos[2].aspectRatio + itemInfos[3].aspectRatio)) + let w0 = max(minWidth, min((maxSize.width - 2 * spacing) * 0.4, h * itemInfos[1].aspectRatio)) + let w2 = max(max(minWidth, (maxSize.width - 2 * spacing) * 0.33), h * itemInfos[3].aspectRatio) + let w1 = w - w0 - w2 - 2 * spacing + h = min(maxSize.height - h0 - spacing, h) + itemInfos[1].layoutFrame = CGRect(x: 0.0, y: h0 + spacing, width: w0, height: h) + itemInfos[1].position = [.left, .bottom] + + itemInfos[2].layoutFrame = CGRect(x: w0 + spacing, y: h0 + spacing, width: w1, height: h) + itemInfos[2].position = [.bottom] + + itemInfos[3].layoutFrame = CGRect(x: w0 + w1 + 2 * spacing, y: h0 + spacing, width: w2, height: h) + itemInfos[3].position = [.right, .bottom] + } else { + let h = maxSize.height + let w0 = round(min(h * itemInfos[0].aspectRatio, (maxSize.width - spacing) * 0.6)) + itemInfos[0].layoutFrame = CGRect(x: 0.0, y: 0.0, width: w0, height: h) + itemInfos[0].position = [.top, .left, .bottom] + + var w = round((maxSize.height - 2 * spacing) / (1.0 / itemInfos[1].aspectRatio + 1.0 / itemInfos[2].aspectRatio + 1.0 / itemInfos[3].aspectRatio)) + let h0 = floorToScreenPixels(w / itemInfos[1].aspectRatio) + let h1 = floorToScreenPixels(w / itemInfos[2].aspectRatio) + let h2 = h - h0 - h1 - 2.0 * spacing + w = max(minWidth, min(maxSize.width - w0 - spacing, w)) + itemInfos[1].layoutFrame = CGRect(x: w0 + spacing, y: 0.0, width: w, height: h0) + itemInfos[1].position = [.right, .top] + + itemInfos[2].layoutFrame = CGRect(x: w0 + spacing, y: h0 + spacing, width: w, height: h1) + itemInfos[2].position = [.right] + + itemInfos[3].layoutFrame = CGRect(x: w0 + spacing, y: h0 + h1 + 2 * spacing, width: w, height: h2) + itemInfos[3].position = [.right, .bottom] + } + } + } + + if forceCalc || itemInfos.count >= 5 { + var croppedRatios: [CGFloat] = [] + for itemInfo in itemInfos { + let aspectRatio = itemInfo.aspectRatio + var croppedRatio = aspectRatio + if averageAspectRatio > 1.1 { + croppedRatio = max(1.0, aspectRatio) + } else { + croppedRatio = min(1.0, aspectRatio) + } + + croppedRatio = max(0.66667, min(1.7, croppedRatio)) + croppedRatios.append(croppedRatio) + } + + func multiHeight(_ ratios: [CGFloat]) -> CGFloat { + var ratioSum: CGFloat = 0.0 + for ratio in ratios { + ratioSum += ratio + } + return (maxSize.width - CGFloat(ratios.count - 1) * spacing) / ratioSum + } + + var attempts: [MosaicLayoutAttempt] = [] + func addAttempt(_ lineCounts: [Int], _ heights: [CGFloat], _ attempts: inout [MosaicLayoutAttempt]) { + attempts.append(MosaicLayoutAttempt(lineCounts: lineCounts, heights: heights)) + } + + for firstLine in 1 ..< croppedRatios.count { + let secondLine = croppedRatios.count - firstLine + if firstLine > 3 || secondLine > 3 { + continue + } + + addAttempt([firstLine, croppedRatios.count - firstLine], [multiHeight(Array(croppedRatios[0.. 3 || secondLine > (averageAspectRatio < 0.85 ? 4 : 3) || thirdLine > 3 { + continue + } + + addAttempt([firstLine, secondLine, thirdLine], [multiHeight(Array(croppedRatios[0 ..< firstLine])), multiHeight(Array(croppedRatios[firstLine ..< croppedRatios.count - thirdLine])), multiHeight(Array(croppedRatios[firstLine + secondLine ..< croppedRatios.count]))], &attempts) + + //addAttempt(@[@(firstLine), @(secondLine), @(thirdLine)], @[multiHeight([croppedRatios subarrayWithRange:NSMakeRange(0, firstLine)]), multiHeight([croppedRatios subarrayWithRange:NSMakeRange(firstLine, croppedRatios.count - firstLine - thirdLine)]), multiHeight([croppedRatios subarrayWithRange:NSMakeRange(firstLine + secondLine, croppedRatios.count - firstLine - secondLine)])]) + } + } + + if croppedRatios.count - 2 >= 1 { + outer: for firstLine in 1 ..< croppedRatios.count - 2 { + if croppedRatios.count - firstLine < 1 { + continue outer + } + for secondLine in 1 ..< croppedRatios.count - firstLine { + for thirdLine in 1 ..< croppedRatios.count - firstLine - secondLine { + let fourthLine = croppedRatios.count - firstLine - secondLine - thirdLine + if firstLine > 3 || secondLine > 3 || thirdLine > 3 || fourthLine > 3 { + continue + } + + addAttempt([firstLine, secondLine, thirdLine, fourthLine], [multiHeight(Array(croppedRatios[0 ..< firstLine])), multiHeight(Array(croppedRatios[firstLine ..< croppedRatios.count - thirdLine - fourthLine])), multiHeight(Array(croppedRatios[firstLine + secondLine ..< croppedRatios.count - fourthLine])), multiHeight(Array(croppedRatios[firstLine + secondLine + thirdLine ..< croppedRatios.count]))], &attempts) + + //addAttempt(@[@(firstLine), @(secondLine), @(thirdLine), @(fourthLine)], @[multiHeight([croppedRatios subarrayWithRange:NSMakeRange(0, firstLine)]), multiHeight([croppedRatios subarrayWithRange:NSMakeRange(firstLine, croppedRatios.count - firstLine - thirdLine - fourthLine)]), multiHeight([croppedRatios subarrayWithRange:NSMakeRange(firstLine + secondLine, croppedRatios.count - firstLine - secondLine - fourthLine)]), multiHeight([croppedRatios subarrayWithRange:NSMakeRange(firstLine + secondLine + thirdLine, croppedRatios.count - firstLine - secondLine - thirdLine)])]) + } + } + } + } + + let maxHeight = maxSize.width / 3.0 * 4.0 + var optimal: MosaicLayoutAttempt? = nil + var optimalDiff: CGFloat = 0.0 + for attempt in attempts { + var totalHeight = spacing * CGFloat(attempt.heights.count - 1) + var minLineHeight: CGFloat = .greatestFiniteMagnitude + var maxLineHeight: CGFloat = 0.0 + for h in attempt.heights { + totalHeight += h + if totalHeight < minLineHeight { + minLineHeight = totalHeight + } + if totalHeight > maxLineHeight { + maxLineHeight = totalHeight + } + } + + var diff = abs(totalHeight - maxHeight) + + if attempt.lineCounts.count > 1 { + if (attempt.lineCounts[0] > attempt.lineCounts[1]) || (attempt.lineCounts.count > 2 && attempt.lineCounts[1] > attempt.lineCounts[2]) || (attempt.lineCounts.count > 3 && attempt.lineCounts[2] > attempt.lineCounts[3]) { + diff *= 1.5 + } + } + + if minLineHeight < minWidth { + diff *= 1.5 + } + + if optimal == nil || diff < optimalDiff { + optimal = attempt + optimalDiff = diff + } + } + + var index = 0 + var y: CGFloat = 0.0 + if let optimal = optimal { + for i in 0 ..< optimal.lineCounts.count { + let count = optimal.lineCounts[i] + let lineHeight = optimal.heights[i] + var x: CGFloat = 0.0 + + var positionFlags: MosaicItemPosition = [] + if i == 0 { + positionFlags.insert(.top) + } + if i == optimal.lineCounts.count - 1 { + positionFlags.insert(.bottom) + } + + for k in 0 ..< count { + var innerPositionFlags = positionFlags + + if k == 0 { + innerPositionFlags.insert(.left) + } + if k == count - 1 { + innerPositionFlags.insert(.right) + } + + if positionFlags == .none { + innerPositionFlags = .inside + } + + let ratio = croppedRatios[index] + let width = ratio * lineHeight + itemInfos[index].layoutFrame = CGRect(x: x, y: y, width: width, height: lineHeight) + itemInfos[index].position = innerPositionFlags + + x += width + spacing + index += 1 + } + + y += lineHeight + spacing + } + } + } + + var dimensions = CGSize() + for itemInfo in itemInfos { + dimensions.width = max(dimensions.width, round(itemInfo.layoutFrame.maxX)) + dimensions.height = max(dimensions.height, round(itemInfo.layoutFrame.maxY)) + } + + return (itemInfos.map { ($0.layoutFrame, $0.position) }, dimensions) +} diff --git a/TelegramUI/ChatMessageCallBubbleContentNode.swift b/TelegramUI/ChatMessageCallBubbleContentNode.swift index be453909e9..8eb8d97001 100644 --- a/TelegramUI/ChatMessageCallBubbleContentNode.swift +++ b/TelegramUI/ChatMessageCallBubbleContentNode.swift @@ -19,8 +19,6 @@ class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode { private let iconNode: ASImageNode private let buttonNode: HighlightableButtonNode - private var item: ChatMessageItem? - required init() { self.titleNode = TextNode() self.labelNode = TextNode() @@ -56,26 +54,27 @@ class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode { fatalError("init(coder:) has not been implemented") } - override func asyncLayoutContent() -> (_ item: ChatMessageItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ position: ChatMessageBubbleContentPosition, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))) { + override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeLabelLayout = TextNode.asyncLayout(self.labelNode) - return { item, layoutConstants, position, _ in - return (CGFloat.greatestFiniteMagnitude, { constrainedSize in + return { item, layoutConstants, _, _, _ in + let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackgroundForEmptyWallpapers: false, forceFullCorners: false) + return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in let message = item.message - let incoming = item.message.effectivelyIncoming + let incoming = item.message.effectivelyIncoming(item.account.peerId) let horizontalInset = layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right let textConstrainedSize = CGSize(width: constrainedSize.width - horizontalInset, height: constrainedSize.height) - let bubbleTheme = item.theme.chat.bubble + let bubbleTheme = item.presentationData.theme.chat.bubble var titleString: String if message.flags.contains(.Incoming) { - titleString = item.strings.Notification_CallIncoming + titleString = item.presentationData.strings.Notification_CallIncoming } else { - titleString = item.strings.Notification_CallOutgoing + titleString = item.presentationData.strings.Notification_CallOutgoing } var callDuration: Int32? @@ -87,10 +86,10 @@ class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode { switch discardReason { case .busy, .disconnect: callSuccessful = false - titleString = item.strings.Notification_CallCanceled + titleString = item.presentationData.strings.Notification_CallCanceled case .missed: callSuccessful = false - titleString = item.strings.Notification_CallMissed + titleString = item.presentationData.strings.Notification_CallMissed case .hangup: break } @@ -123,29 +122,26 @@ class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode { var buttonImage: UIImage? if incoming { - buttonImage = PresentationResourcesChat.chatBubbleIncomingCallButtonImage(item.theme) + buttonImage = PresentationResourcesChat.chatBubbleIncomingCallButtonImage(item.presentationData.theme) } else { - buttonImage = PresentationResourcesChat.chatBubbleOutgoingCallButtonImage(item.theme) + buttonImage = PresentationResourcesChat.chatBubbleOutgoingCallButtonImage(item.presentationData.theme) } - var t = Int(item.message.timestamp) - var timeinfo = tm() - localtime_r(&t, &timeinfo) - var dateText = String(format: "%02d:%02d", arguments: [Int(timeinfo.tm_hour), Int(timeinfo.tm_min)]) + var dateText = stringForMessageTimestamp(timestamp: item.message.timestamp, timeFormat: item.presentationData.timeFormat) if let duration = callDuration, duration != 0 { if duration >= 60 { - dateText += ", " + item.strings.Call_Minutes(duration / 60) + dateText += ", " + item.presentationData.strings.Call_Minutes(duration / 60) } else { - dateText += ", " + item.strings.Call_Seconds(duration) + dateText += ", " + item.presentationData.strings.Call_Seconds(duration) } } var attributedLabel: NSAttributedString? - attributedLabel = NSAttributedString(string: dateText, font: labelFont, textColor: message.effectivelyIncoming ? bubbleTheme.incomingFileDurationColor : bubbleTheme.outgoingFileDurationColor) + attributedLabel = NSAttributedString(string: dateText, font: labelFont, textColor: message.effectivelyIncoming(item.account.peerId) ? bubbleTheme.incomingFileDurationColor : bubbleTheme.outgoingFileDurationColor) - let (titleLayout, titleApply) = makeTitleLayout(attributedTitle, nil, 0, .end, textConstrainedSize, .natural, nil, UIEdgeInsets()) - let (labelLayout, labelApply) = makeLabelLayout(attributedLabel, nil, 0, .end, textConstrainedSize, .natural, nil, UIEdgeInsets()) + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: attributedTitle, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: attributedLabel, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let titleSize = titleLayout.size let labelSize = labelLayout.size diff --git a/TelegramUI/ChatMessageContactBubbleContentNode.swift b/TelegramUI/ChatMessageContactBubbleContentNode.swift index ad05efea74..a2150ce58f 100644 --- a/TelegramUI/ChatMessageContactBubbleContentNode.swift +++ b/TelegramUI/ChatMessageContactBubbleContentNode.swift @@ -16,7 +16,6 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { private let titleNode: TextNode private let textNode: TextNode - private var item: ChatMessageItem? private var contact: TelegramMediaContact? private var contactPhone: String? @@ -44,7 +43,7 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { self.view.addGestureRecognizer(tapRecognizer) } - override func asyncLayoutContent() -> (_ item: ChatMessageItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ position: ChatMessageBubbleContentPosition, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))) { + override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))) { let statusLayout = self.dateAndStatusNode.asyncLayout() let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeTextLayout = TextNode.asyncLayout(self.textNode) @@ -52,7 +51,7 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { let previousContact = self.contact let previousContactPhone = self.contactPhone - return { item, layoutConstants, position, constrainedSize in + return { item, layoutConstants, _, _, constrainedSize in var selectedContact: TelegramMediaContact? for media in item.message.media { if let media = media as? TelegramMediaContact { @@ -73,7 +72,7 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { } else { displayName = selectedContact.lastName } - titleString = NSAttributedString(string: displayName, font: titleFont, textColor: item.message.effectivelyIncoming ? item.theme.chat.bubble.incomingAccentColor : item.theme.chat.bubble.outgoingAccentColor) + titleString = NSAttributedString(string: displayName, font: titleFont, textColor: item.message.effectivelyIncoming(item.account.peerId) ? item.presentationData.theme.chat.bubble.incomingAccentTextColor : item.presentationData.theme.chat.bubble.outgoingAccentTextColor) let phone: String if let previousContact = previousContact, previousContact.isEqual(selectedContact), let contactPhone = previousContactPhone { @@ -82,21 +81,19 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { phone = formatPhoneNumber(selectedContact.phoneNumber) } updatedPhone = phone - textString = NSAttributedString(string: phone, font: textFont, textColor: item.message.effectivelyIncoming ? item.theme.chat.bubble.incomingPrimaryTextColor : item.theme.chat.bubble.outgoingPrimaryTextColor) + textString = NSAttributedString(string: phone, font: textFont, textColor: item.message.effectivelyIncoming(item.account.peerId) ? item.presentationData.theme.chat.bubble.incomingPrimaryTextColor : item.presentationData.theme.chat.bubble.outgoingPrimaryTextColor) } else { updatedPhone = nil } - return (CGFloat.greatestFiniteMagnitude, { constrainedSize in + let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackgroundForEmptyWallpapers: false, forceFullCorners: false) + + return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in let avatarSize = CGSize(width: 40.0, height: 40.0) let maxTextWidth = max(1.0, constrainedSize.width - avatarSize.width - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right) - let (titleLayout, titleApply) = makeTitleLayout(titleString, nil, 1, .end, CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) - let (textLayout, textApply) = makeTextLayout(textString, nil, 2, .end, CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) - - var t = Int(item.message.timestamp) - var timeinfo = tm() - localtime_r(&t, &timeinfo) + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: textString, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) var edited = false var sentViaBot = false @@ -111,7 +108,7 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { } } - var dateText = String(format: "%02d:%02d", arguments: [Int(timeinfo.tm_hour), Int(timeinfo.tm_min)]) + var dateText = stringForMessageTimestamp(timestamp: item.message.timestamp, timeFormat: item.presentationData.timeFormat) if let author = item.message.author as? TelegramUser { if author.botInfo != nil { @@ -123,27 +120,28 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { } let statusType: ChatMessageDateAndStatusType? - if case .None = position.bottom { - if item.message.effectivelyIncoming { - statusType = .BubbleIncoming - } else { - if item.message.flags.contains(.Failed) { - statusType = .BubbleOutgoing(.Failed) - } else if item.message.flags.isSending { - statusType = .BubbleOutgoing(.Sending) + switch position { + case .linear(_, .None): + if item.message.effectivelyIncoming(item.account.peerId) { + statusType = .BubbleIncoming } else { - statusType = .BubbleOutgoing(.Sent(read: item.read)) + if item.message.flags.contains(.Failed) { + statusType = .BubbleOutgoing(.Failed) + } else if item.message.flags.isSending { + statusType = .BubbleOutgoing(.Sending) + } else { + statusType = .BubbleOutgoing(.Sent(read: item.read)) + } } - } - } else { - statusType = nil + default: + statusType = nil } var statusSize = CGSize() var statusApply: ((Bool) -> Void)? if let statusType = statusType { - let (size, apply) = statusLayout(item.theme, edited && !sentViaBot, viewCount, dateText, statusType, CGSize(width: constrainedSize.width, height: CGFloat.greatestFiniteMagnitude)) + let (size, apply) = statusLayout(item.presentationData.theme, item.presentationData.strings, edited && !sentViaBot, viewCount, dateText, statusType, CGSize(width: constrainedSize.width, height: CGFloat.greatestFiniteMagnitude)) statusSize = size statusApply = apply } @@ -248,7 +246,7 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { @objc func contactTap(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { if let item = self.item { - self.controllerInteraction?.openMessage(item.message.id) + item.controllerInteraction.openMessage(item.message.id) } } } diff --git a/TelegramUI/ChatMessageDateAndStatusNode.swift b/TelegramUI/ChatMessageDateAndStatusNode.swift index bf65ae8c1d..ed852de783 100644 --- a/TelegramUI/ChatMessageDateAndStatusNode.swift +++ b/TelegramUI/ChatMessageDateAndStatusNode.swift @@ -22,19 +22,83 @@ private func maybeAddRotationAnimation(_ layer: CALayer, duration: Double) { layer.add(basicAnimation, forKey: "clockFrameAnimation") } -enum ChatMessageDateAndStatusOutgoingType { +enum ChatMessageDateAndStatusOutgoingType: Equatable { case Sent(read: Bool) case Sending case Failed + + static func ==(lhs: ChatMessageDateAndStatusOutgoingType, rhs: ChatMessageDateAndStatusOutgoingType) -> Bool { + switch lhs { + case let .Sent(read): + if case .Sent(read) = rhs { + return true + } else { + return false + } + case .Sending: + if case .Sending = rhs { + return true + } else { + return false + } + case .Failed: + if case .Failed = rhs { + return true + } else { + return false + } + } + } } -enum ChatMessageDateAndStatusType { +enum ChatMessageDateAndStatusType: Equatable { case BubbleIncoming case BubbleOutgoing(ChatMessageDateAndStatusOutgoingType) case ImageIncoming case ImageOutgoing(ChatMessageDateAndStatusOutgoingType) case FreeIncoming case FreeOutgoing(ChatMessageDateAndStatusOutgoingType) + + static func ==(lhs: ChatMessageDateAndStatusType, rhs: ChatMessageDateAndStatusType) -> Bool { + switch lhs { + case .BubbleIncoming: + if case .BubbleIncoming = rhs { + return true + } else { + return false + } + case let .BubbleOutgoing(type): + if case .BubbleOutgoing(type) = rhs { + return true + } else { + return false + } + case .ImageIncoming: + if case .ImageIncoming = rhs { + return true + } else { + return false + } + case let .ImageOutgoing(type): + if case .ImageOutgoing(type) = rhs { + return true + } else { + return false + } + case .FreeIncoming: + if case .FreeIncoming = rhs { + return true + } else { + return false + } + case let .FreeOutgoing(type): + if case .FreeOutgoing(type) = rhs { + return true + } else { + return false + } + } + } } class ChatMessageDateAndStatusNode: ASDisplayNode { @@ -46,6 +110,9 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { private let dateNode: TextNode private var impressionIcon: ASImageNode? + private var type: ChatMessageDateAndStatusType? + private var theme: PresentationTheme? + override init() { self.dateNode = TextNode() self.dateNode.isLayerBacked = true @@ -53,10 +120,12 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { super.init() + self.isUserInteractionEnabled = false + self.addSubnode(self.dateNode) } - func asyncLayout() -> (_ theme: PresentationTheme, _ edited: Bool, _ impressionCount: Int?, _ dateText: String, _ type: ChatMessageDateAndStatusType, _ constrainedSize: CGSize) -> (CGSize, (Bool) -> Void) { + func asyncLayout() -> (_ theme: PresentationTheme, _ strings: PresentationStrings, _ edited: Bool, _ impressionCount: Int?, _ dateText: String, _ type: ChatMessageDateAndStatusType, _ constrainedSize: CGSize) -> (CGSize, (Bool) -> Void) { let dateLayout = TextNode.asyncLayout(self.dateNode) var checkReadNode = self.checkReadNode @@ -67,7 +136,10 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { var currentBackgroundNode = self.backgroundNode var currentImpressionIcon = self.impressionIcon - return { theme, edited, impressionCount, dateText, type, constrainedSize in + let currentType = self.type + let currentTheme = self.theme + + return { theme, strings, edited, impressionCount, dateText, type, constrainedSize in let dateColor: UIColor var backgroundImage: UIImage? var outgoingStatus: ChatMessageDateAndStatusOutgoingType? @@ -79,6 +151,8 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { let clockMinImage: UIImage? var impressionImage: UIImage? + let themeUpdated = theme !== currentTheme || type != currentType + let graphics = PresentationResourcesChat.principalGraphics(theme) switch type { @@ -127,7 +201,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { impressionImage = graphics.mediaImpressionIcon } case .FreeIncoming: - dateColor = theme.chat.bubble.mediaDateAndStatusTextColor + dateColor = theme.chat.serviceMessage.serviceMessagePrimaryTextColor backgroundImage = graphics.dateAndStatusFreeBackground leftInset = 0.0 loadedCheckFullImage = graphics.checkMediaFullImage @@ -138,7 +212,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { impressionImage = graphics.mediaImpressionIcon } case let .FreeOutgoing(status): - dateColor = theme.chat.bubble.mediaDateAndStatusTextColor + dateColor = theme.chat.serviceMessage.serviceMessagePrimaryTextColor outgoingStatus = status backgroundImage = graphics.dateAndStatusFreeBackground leftInset = 0.0 @@ -157,10 +231,10 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { } if edited { - updatedDateText = "edited " + updatedDateText + updatedDateText = "\(strings.Conversation_MessageEditedLabel) \(updatedDateText)" } - let (date, dateApply) = dateLayout(NSAttributedString(string: updatedDateText, font: dateFont, textColor: dateColor), nil, 1, .end, constrainedSize, .natural, nil, UIEdgeInsets()) + let (date, dateApply) = dateLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: updatedDateText, font: dateFont, textColor: dateColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: constrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let statusWidth: CGFloat @@ -193,7 +267,6 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { clockFrameNode?.isLayerBacked = true clockFrameNode?.displaysAsynchronously = false clockFrameNode?.displayWithoutProcessing = true - clockFrameNode?.image = clockFrameImage clockFrameNode?.frame = CGRect(origin: CGPoint(), size: clockFrameImage?.size ?? CGSize()) } @@ -202,7 +275,6 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { clockMinNode?.isLayerBacked = true clockMinNode?.displaysAsynchronously = false clockMinNode?.displayWithoutProcessing = true - clockMinNode?.image = clockMinImage clockMinNode?.frame = CGRect(origin: CGPoint(), size: clockMinImage?.size ?? CGSize()) } clockPosition = CGPoint(x: leftInset + date.size.width + 8.5, y: 7.5) @@ -266,7 +338,6 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { backgroundNode.isLayerBacked = true backgroundNode.displayWithoutProcessing = true backgroundNode.displaysAsynchronously = false - backgroundNode.image = backgroundImage currentBackgroundNode = backgroundNode } backgroundInsets = UIEdgeInsets(top: 2.0, left: 7.0, bottom: 2.0, right: 7.0) @@ -292,11 +363,17 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { return (layoutSize, { [weak self] animated in if let strongSelf = self { + strongSelf.theme = theme + strongSelf.type = type + if backgroundImage != nil { if let currentBackgroundNode = currentBackgroundNode { if currentBackgroundNode.supernode == nil { strongSelf.backgroundNode = currentBackgroundNode + currentBackgroundNode.image = backgroundImage strongSelf.insertSubnode(currentBackgroundNode, at: 0) + } else if themeUpdated { + currentBackgroundNode.image = backgroundImage } } strongSelf.backgroundNode?.frame = CGRect(origin: CGPoint(), size: layoutSize) @@ -323,12 +400,15 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { strongSelf.impressionIcon = nil } - strongSelf.dateNode.frame = CGRect(origin: CGPoint(x: leftInset + backgroundInsets.left + impressionWidth, y: backgroundInsets.top), size: date.size) + strongSelf.dateNode.frame = CGRect(origin: CGPoint(x: leftInset + backgroundInsets.left + impressionWidth, y: backgroundInsets.top + 1.0), size: date.size) if let clockFrameNode = clockFrameNode { if strongSelf.clockFrameNode == nil { strongSelf.clockFrameNode = clockFrameNode + clockFrameNode.image = clockFrameImage strongSelf.addSubnode(clockFrameNode) + } else if themeUpdated { + clockFrameNode.image = clockFrameImage } clockFrameNode.position = CGPoint(x: backgroundInsets.left + clockPosition.x, y: backgroundInsets.top + clockPosition.y) if let clockFrameNode = strongSelf.clockFrameNode { @@ -342,7 +422,10 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { if let clockMinNode = clockMinNode { if strongSelf.clockMinNode == nil { strongSelf.clockMinNode = clockMinNode + clockMinNode.image = clockMinImage strongSelf.addSubnode(clockMinNode) + } else if themeUpdated { + clockMinNode.image = clockMinImage } clockMinNode.position = CGPoint(x: backgroundInsets.left + clockPosition.x, y: backgroundInsets.top + clockPosition.y) if let clockMinNode = strongSelf.clockMinNode { @@ -360,6 +443,8 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { strongSelf.checkSentNode = checkSentNode strongSelf.addSubnode(checkSentNode) animateSentNode = animated + } else if themeUpdated { + checkSentNode.image = loadedCheckFullImage } if let checkSentFrame = checkSentFrame { @@ -378,6 +463,8 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { checkReadNode.image = loadedCheckPartialImage strongSelf.checkReadNode = checkReadNode strongSelf.addSubnode(checkReadNode) + } else if themeUpdated { + checkReadNode.image = loadedCheckPartialImage } if let checkReadFrame = checkReadFrame { diff --git a/TelegramUI/ChatMessageDateHeader.swift b/TelegramUI/ChatMessageDateHeader.swift index cb480af38a..95aae21098 100644 --- a/TelegramUI/ChatMessageDateHeader.swift +++ b/TelegramUI/ChatMessageDateHeader.swift @@ -14,6 +14,7 @@ private let granularity: Int32 = 60 * 60 * 24 final class ChatMessageDateHeader: ListViewItemHeader { private let timestamp: Int32 + private let roundedUtcTimestamp: Int32 private let roundedTimestamp: Int32 let id: Int64 @@ -27,8 +28,10 @@ final class ChatMessageDateHeader: ListViewItemHeader { if timestamp == Int32.max { self.roundedTimestamp = timestamp / (granularity) * (granularity) + self.roundedUtcTimestamp = self.roundedTimestamp } else { self.roundedTimestamp = ((timestamp + timezoneOffset) / (granularity)) * (granularity) + self.roundedUtcTimestamp = ((timestamp) / (granularity)) * (granularity) } self.id = Int64(self.roundedTimestamp) } @@ -38,7 +41,7 @@ final class ChatMessageDateHeader: ListViewItemHeader { let height: CGFloat = 34.0 func node() -> ListViewItemHeaderNode { - return ChatMessageDateHeaderNode(timestamp: self.roundedTimestamp, theme: self.theme, strings: self.strings) + return ChatMessageDateHeaderNode(utcTimestamp: self.roundedUtcTimestamp, theme: self.theme, strings: self.strings) } } @@ -80,15 +83,15 @@ final class ChatMessageDateHeaderNode: ListViewItemHeaderNode { let backgroundNode: ASImageNode let stickBackgroundNode: ASImageNode - private let timestamp: Int32 + private let utcTimestamp: Int32 private var theme: PresentationTheme private var strings: PresentationStrings private var flashingOnScrolling = false private var stickDistanceFactor: CGFloat = 0.0 - init(timestamp: Int32, theme: PresentationTheme, strings: PresentationStrings) { - self.timestamp = timestamp + init(utcTimestamp: Int32, theme: PresentationTheme, strings: PresentationStrings) { + self.utcTimestamp = utcTimestamp self.theme = theme self.strings = strings @@ -126,7 +129,7 @@ final class ChatMessageDateHeaderNode: ListViewItemHeaderNode { let nowTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) - var t: time_t = time_t(timestamp) + var t: time_t = time_t(utcTimestamp) var timeinfo: tm = tm() localtime_r(&t, &timeinfo) @@ -139,16 +142,16 @@ final class ChatMessageDateHeaderNode: ListViewItemHeaderNode { if timeinfo.tm_yday == timeinfoNow.tm_yday { text = strings.Weekday_Today } else { - text = "\(monthAtIndex(Int(timeinfo.tm_mon), strings: strings)) \(timeinfo.tm_mday)" + text = strings.Date_ChatDateHeader(monthAtIndex(Int(timeinfo.tm_mon), strings: strings), "\(timeinfo.tm_mday)").0 } } else { - text = "\(monthAtIndex(Int(timeinfo.tm_mon), strings: strings)) \(timeinfo.tm_mday), \(1900 + timeinfo.tm_year)" + text = strings.Date_ChatDateHeaderYear(monthAtIndex(Int(timeinfo.tm_mon), strings: strings), "\(timeinfo.tm_mday)", "\(1900 + timeinfo.tm_year)").0 } - let attributedString = NSAttributedString(string: text, font: titleFont, textColor: UIColor.white) + let attributedString = NSAttributedString(string: text, font: titleFont, textColor: theme.chat.serviceMessage.dateTextColor) let labelLayout = TextNode.asyncLayout(self.labelNode) - let (size, apply) = labelLayout(attributedString, nil, 1, .end, CGSize(width: 320.0, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let (size, apply) = labelLayout(TextNodeLayoutArguments(attributedString: attributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 320.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let _ = apply() self.labelNode.frame = CGRect(origin: CGPoint(), size: size.size) @@ -178,20 +181,16 @@ final class ChatMessageDateHeaderNode: ListViewItemHeaderNode { self.setNeedsLayout() } - override func layout() { - super.layout() + override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) { + //let labelLayout = TextNode.asyncLayout(self.labelNode) - let bounds = self.bounds + let labelSize = self.labelNode.bounds.size + let backgroundSize = CGSize(width: labelSize.width + 8.0 + 8.0, height: 26.0) - let labelLayout = TextNode.asyncLayout(self.labelNode) - - let size = self.labelNode.bounds.size - let backgroundSize = CGSize(width: size.width + 8.0 + 8.0, height: 26.0) - - let backgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((bounds.size.width - backgroundSize.width) / 2.0), y: (34.0 - 26.0) / 2.0), size: backgroundSize) + let backgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - backgroundSize.width) / 2.0), y: (34.0 - 26.0) / 2.0), size: backgroundSize) self.stickBackgroundNode.frame = CGRect(origin: CGPoint(), size: backgroundFrame.size) self.backgroundNode.frame = backgroundFrame - self.labelNode.frame = CGRect(origin: CGPoint(x: backgroundFrame.origin.x + 8.0, y: backgroundFrame.origin.y + floorToScreenPixels((backgroundSize.height - size.height) / 2.0) - 1.0), size: size) + self.labelNode.frame = CGRect(origin: CGPoint(x: backgroundFrame.origin.x + 8.0, y: backgroundFrame.origin.y + floorToScreenPixels((backgroundSize.height - labelSize.height) / 2.0)), size: labelSize) } override func updateStickDistanceFactor(_ factor: CGFloat, transition: ContainedViewLayoutTransition) { diff --git a/TelegramUI/ChatMessageFileBubbleContentNode.swift b/TelegramUI/ChatMessageFileBubbleContentNode.swift index 6446136600..b54d6cd492 100644 --- a/TelegramUI/ChatMessageFileBubbleContentNode.swift +++ b/TelegramUI/ChatMessageFileBubbleContentNode.swift @@ -8,8 +8,6 @@ import TelegramCore class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode { private let interactiveFileNode: ChatMessageInteractiveFileNode - private var item: ChatMessageItem? - required init() { self.interactiveFileNode = ChatMessageInteractiveFileNode() @@ -19,8 +17,8 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode { self.interactiveFileNode.activateLocalContent = { [weak self] in if let strongSelf = self { - if let item = strongSelf.item, let controllerInteraction = strongSelf.controllerInteraction { - controllerInteraction.openMessage(item.message.id) + if let item = strongSelf.item { + item.controllerInteraction.openMessage(item.message.id) } } } @@ -30,10 +28,10 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode { fatalError("init(coder:) has not been implemented") } - override func asyncLayoutContent() -> (_ item: ChatMessageItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ position: ChatMessageBubbleContentPosition, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))) { + override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))) { let interactiveFileLayout = self.interactiveFileNode.asyncLayout() - return { item, layoutConstants, position, constrainedSize in + return { item, layoutConstants, preparePosition, _, constrainedSize in var selectedFile: TelegramMediaFile? for media in item.message.media { if let telegramFile = media as? TelegramMediaFile { @@ -41,22 +39,23 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode { } } - let incoming = item.message.effectivelyIncoming + let incoming = item.message.effectivelyIncoming(item.account.peerId) let statusType: ChatMessageDateAndStatusType? - if case .None = position.bottom { - if incoming { - statusType = .BubbleIncoming - } else { - if item.message.flags.contains(.Failed) { - statusType = .BubbleOutgoing(.Failed) - } else if item.message.flags.isSending { - statusType = .BubbleOutgoing(.Sending) + switch preparePosition { + case .linear(_, .None): + if incoming { + statusType = .BubbleIncoming } else { - statusType = .BubbleOutgoing(.Sent(read: item.read)) + if item.message.flags.contains(.Failed) { + statusType = .BubbleOutgoing(.Failed) + } else if item.message.flags.isSending { + statusType = .BubbleOutgoing(.Sending) + } else { + statusType = .BubbleOutgoing(.Sent(read: item.read)) + } } - } - } else { - statusType = nil + default: + statusType = nil } var automaticDownload = false @@ -64,9 +63,11 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode { automaticDownload = item.controllerInteraction.automaticMediaDownloadSettings.categories.getVoice(item.message.id.peerId) } - let (initialWidth, refineLayout) = interactiveFileLayout(item.account, item.theme, item.strings, item.message, selectedFile!, automaticDownload, item.message.effectivelyIncoming, statusType, CGSize(width: constrainedSize.width, height: constrainedSize.height)) + let (initialWidth, refineLayout) = interactiveFileLayout(item.account, item.presentationData, item.message, selectedFile!, automaticDownload, item.message.effectivelyIncoming(item.account.peerId), statusType, CGSize(width: constrainedSize.width, height: constrainedSize.height)) - return (initialWidth + layoutConstants.file.bubbleInsets.left + layoutConstants.file.bubbleInsets.right, { constrainedSize in + let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackgroundForEmptyWallpapers: false, forceFullCorners: false) + + return (contentProperties, nil, initialWidth + layoutConstants.file.bubbleInsets.left + layoutConstants.file.bubbleInsets.right, { constrainedSize, position in let (refinedWidth, finishLayout) = refineLayout(constrainedSize) return (refinedWidth + layoutConstants.file.bubbleInsets.left + layoutConstants.file.bubbleInsets.right, { boundingWidth in @@ -86,6 +87,18 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode { } } + override func transitionNode(messageId: MessageId, media: Media) -> ASDisplayNode? { + if self.item?.message.id == messageId { + return self.interactiveFileNode.transitionNode(media: media) + } else { + return nil + } + } + + override func updateHiddenMedia(_ media: [Media]?) { + self.interactiveFileNode.updateHiddenMedia(media) + } + override func animateInsertion(_ currentTimestamp: Double, duration: Double) { self.interactiveFileNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } diff --git a/TelegramUI/ChatMessageForwardInfoNode.swift b/TelegramUI/ChatMessageForwardInfoNode.swift index 1683a04507..10e806489a 100644 --- a/TelegramUI/ChatMessageForwardInfoNode.swift +++ b/TelegramUI/ChatMessageForwardInfoNode.swift @@ -6,6 +6,11 @@ import Postbox private let prefixFont = Font.regular(13.0) private let peerFont = Font.medium(13.0) +enum ChatMessageForwardInfoType { + case bubble(incoming: Bool) + case standalone +} + class ChatMessageForwardInfoNode: ASDisplayNode { private var textNode: TextNode? @@ -13,22 +18,35 @@ class ChatMessageForwardInfoNode: ASDisplayNode { super.init() } - class func asyncLayout(_ maybeNode: ChatMessageForwardInfoNode?) -> (_ theme: PresentationTheme, _ incoming: Bool, _ peer: Peer, _ authorPeer: Peer?, _ constrainedSize: CGSize) -> (CGSize, () -> ChatMessageForwardInfoNode) { + class func asyncLayout(_ maybeNode: ChatMessageForwardInfoNode?) -> (_ theme: PresentationTheme, _ strings: PresentationStrings, _ type: ChatMessageForwardInfoType, _ peer: Peer, _ authorName: String?, _ constrainedSize: CGSize) -> (CGSize, () -> ChatMessageForwardInfoNode) { let textNodeLayout = TextNode.asyncLayout(maybeNode?.textNode) - return { theme, incoming, peer, authorPeer, constrainedSize in - let prefix: NSString = "Forwarded Message\nFrom: " + return { theme, strings, type, peer, authorName, constrainedSize in let peerString: String - if let authorPeer = authorPeer { - peerString = "\(peer.displayTitle) (\(authorPeer.displayTitle))" + if let authorName = authorName { + peerString = "\(peer.displayTitle) (\(authorName))" } else { peerString = peer.displayTitle } - let completeString: NSString = "\(prefix)\(peerString)" as NSString - let color = incoming ? theme.chat.bubble.incomingAccentColor : theme.chat.bubble.outgoingAccentColor - let string = NSMutableAttributedString(string: completeString as String, attributes: [NSAttributedStringKey.foregroundColor: color, NSAttributedStringKey.font: prefixFont]) - string.addAttributes([NSAttributedStringKey.font: peerFont], range: NSMakeRange(prefix.length, completeString.length - prefix.length)) - let (textLayout, textApply) = textNodeLayout(string, nil, 2, .end, constrainedSize, .natural, nil, UIEdgeInsets()) + + let titleColor: UIColor + let completeSourceString: (String, [(Int, NSRange)]) + + switch type { + case let .bubble(incoming): + titleColor = incoming ? theme.chat.bubble.incomingAccentTextColor : theme.chat.bubble.outgoingAccentTextColor + completeSourceString = strings.Message_ForwardedMessage(peerString) + case .standalone: + titleColor = theme.chat.serviceMessage.serviceMessagePrimaryTextColor + completeSourceString = strings.Message_ForwardedMessageShort(peerString) + } + + let completeString: NSString = completeSourceString.0 as NSString + let string = NSMutableAttributedString(string: completeString as String, attributes: [NSAttributedStringKey.foregroundColor: titleColor, NSAttributedStringKey.font: prefixFont]) + if let range = completeSourceString.1.first?.1 { + string.addAttributes([NSAttributedStringKey.font: peerFont], range: range) + } + let (textLayout, textApply) = textNodeLayout(TextNodeLayoutArguments(attributedString: string, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: constrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) return (textLayout.size, { let node: ChatMessageForwardInfoNode diff --git a/TelegramUI/ChatMessageGameBubbleContentNode.swift b/TelegramUI/ChatMessageGameBubbleContentNode.swift index 3d2d803cd9..9a6aef0789 100644 --- a/TelegramUI/ChatMessageGameBubbleContentNode.swift +++ b/TelegramUI/ChatMessageGameBubbleContentNode.swift @@ -6,15 +6,10 @@ import SwiftSignalKit import TelegramCore final class ChatMessageGameBubbleContentNode: ChatMessageBubbleContentNode { - private var item: ChatMessageItem? private var game: TelegramMediaGame? private let contentNode: ChatMessageAttachedContentNode - override var properties: ChatMessageBubbleContentProperties { - return ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0) - } - override var visibility: ListViewItemNodeVisibility { didSet { self.contentNode.visibility = self.visibility @@ -33,10 +28,10 @@ final class ChatMessageGameBubbleContentNode: ChatMessageBubbleContentNode { fatalError("init(coder:) has not been implemented") } - override func asyncLayoutContent() -> (_ item: ChatMessageItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ position: ChatMessageBubbleContentPosition, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))) { + override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))) { let contentNodeLayout = self.contentNode.asyncLayout() - return { item, layoutConstants, position, constrainedSize in + return { item, layoutConstants, _, _, constrainedSize in var game: TelegramMediaGame? var messageEntities: [MessageTextEntity]? @@ -59,7 +54,6 @@ final class ChatMessageGameBubbleContentNode: ChatMessageBubbleContentNode { var text: String? var mediaAndFlags: (Media, ChatMessageAttachedContentNodeMediaFlags)? - if let game = game { title = game.title text = game.description @@ -71,16 +65,19 @@ final class ChatMessageGameBubbleContentNode: ChatMessageBubbleContentNode { } } - let (initialWidth, continueLayout) = contentNodeLayout(item.theme, item.strings, item.controllerInteraction.automaticMediaDownloadSettings, item.account, item.message, item.read, title, subtitle, item.message.text.isEmpty ? text : item.message.text, item.message.text.isEmpty ? nil : messageEntities, mediaAndFlags, nil, nil, true, layoutConstants, position, constrainedSize) + let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.account, item.message, item.read, title, subtitle, item.message.text.isEmpty ? text : item.message.text, item.message.text.isEmpty ? nil : messageEntities, mediaAndFlags, nil, nil, true, layoutConstants, constrainedSize) - return (initialWidth, { constrainedSize in - let (refinedWidth, finalizeLayout) = continueLayout(constrainedSize) + let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackgroundForEmptyWallpapers: false, forceFullCorners: false) + + return (contentProperties, nil, initialWidth, { constrainedSize, position in + let (refinedWidth, finalizeLayout) = continueLayout(constrainedSize, position) return (refinedWidth, { boundingWidth in let (size, apply) = finalizeLayout(boundingWidth) return (size, { [weak self] animation in if let strongSelf = self { + strongSelf.item = item strongSelf.game = game apply(animation) @@ -124,7 +121,10 @@ final class ChatMessageGameBubbleContentNode: ChatMessageBubbleContentNode { self.contentNode.updateHiddenMedia(media) } - override func transitionNode(media: Media) -> ASDisplayNode? { + override func transitionNode(messageId: MessageId, media: Media) -> ASDisplayNode? { + if self.item?.message.id != messageId { + return nil + } return self.contentNode.transitionNode(media: media) } } diff --git a/TelegramUI/ChatMessageInstantVideoItemNode.swift b/TelegramUI/ChatMessageInstantVideoItemNode.swift index e1912d834b..bb0de9f989 100644 --- a/TelegramUI/ChatMessageInstantVideoItemNode.swift +++ b/TelegramUI/ChatMessageInstantVideoItemNode.swift @@ -6,10 +6,13 @@ import Postbox import TelegramCore class ChatMessageInstantVideoItemNode: ChatMessageItemView { - var hostedVideoNode: InstantVideoNode? - var tapRecognizer: UITapGestureRecognizer? + private var videoNode: UniversalVideoNode? + + private var swipeToReplyNode: ChatMessageSwipeToReplyNode? + private var swipeToReplyFeedback: HapticFeedback? private var statusNode: RadialStatusNode? + private var playbackStatusNode: InstantVideoRadialStatusNode? private var videoFrame: CGRect? private var selectionNode: ChatMessageSelectionNode? @@ -19,14 +22,23 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { private let fetchDisposable = MetaDisposable() + private var forwardInfoNode: ChatMessageForwardInfoNode? + private var forwardBackgroundNode: ASImageNode? + private var replyInfoNode: ChatMessageReplyInfoNode? private var replyBackgroundNode: ASImageNode? private let dateAndStatusNode: ChatMessageDateAndStatusNode - private let muteIconNode: ASImageNode + private let infoBackgroundNode: ASImageNode + private let muteIconNode: ASImageNode + private let consumableContentNode: ASImageNode + + private var status: FileMediaResourceStatus? private let playbackStatusDisposable = MetaDisposable() + private var currentSwipeToReplyTranslation: CGFloat = 0.0 + private var shouldAcquireVideoContext: Bool { if case .visible = self.visibility { return true @@ -38,22 +50,37 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { override var visibility: ListViewItemNodeVisibility { didSet { if self.visibility != oldValue { - self.hostedVideoNode?.setShouldAcquireContext(self.shouldAcquireVideoContext) + self.videoNode?.canAttachContent = self.shouldAcquireVideoContext + //self.hostedVideoNode?.setShouldAcquireContext(self.shouldAcquireVideoContext) } } } required init() { + self.infoBackgroundNode = ASImageNode() + self.infoBackgroundNode.isLayerBacked = true + self.infoBackgroundNode.displayWithoutProcessing = true + self.infoBackgroundNode.displaysAsynchronously = false + self.dateAndStatusNode = ChatMessageDateAndStatusNode() + self.muteIconNode = ASImageNode() self.muteIconNode.isLayerBacked = true self.muteIconNode.displayWithoutProcessing = true self.muteIconNode.displaysAsynchronously = false + self.consumableContentNode = ASImageNode() + self.consumableContentNode.isLayerBacked = true + self.consumableContentNode.displayWithoutProcessing = true + self.consumableContentNode.displaysAsynchronously = false + self.consumableContentNode.alpha = 0.0 + super.init(layerBacked: false) self.addSubnode(self.dateAndStatusNode) - self.addSubnode(self.muteIconNode) + self.addSubnode(self.infoBackgroundNode) + self.infoBackgroundNode.addSubnode(self.muteIconNode) + self.infoBackgroundNode.addSubnode(self.consumableContentNode) } required init?(coder aDecoder: NSCoder) { @@ -73,9 +100,21 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { return .waitForSingleTap } self.view.addGestureRecognizer(recognizer) + + let replyRecognizer = ChatSwipeToReplyRecognizer(target: self, action: #selector(self.swipeToReplyGesture(_:))) + replyRecognizer.shouldBegin = { [weak self] in + if let strongSelf = self, let item = strongSelf.item { + if strongSelf.selectionNode != nil { + return false + } + return item.controllerInteraction.canSetupReply() + } + return false + } + self.view.addGestureRecognizer(replyRecognizer) } - override func asyncLayout() -> (_ item: ChatMessageItem, _ width: CGFloat, _ mergedTop: Bool, _ mergedBottom: Bool, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { + override func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { let displaySize = CGSize(width: 212.0, height: 212.0) let previousFile = self.telegramFile let layoutConstants = self.layoutConstants @@ -83,23 +122,32 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { let makeReplyInfoLayout = ChatMessageReplyInfoNode.asyncLayout(self.replyInfoNode) let currentReplyBackgroundNode = self.replyBackgroundNode + let makeForwardInfoLayout = ChatMessageForwardInfoNode.asyncLayout(self.forwardInfoNode) + let currentForwardBackgroundNode = self.forwardBackgroundNode + let currentItem = self.appliedItem let makeDateAndStatusLayout = self.dateAndStatusNode.asyncLayout() - return { item, width, mergedTop, mergedBottom, dateHeaderAtBottom in + return { item, params, mergedTop, mergedBottom, dateHeaderAtBottom in var updatedTheme: PresentationTheme? + var updatedInfoBackgroundImage: UIImage? var updatedMuteIconImage: UIImage? - if item.theme !== currentItem?.theme { - updatedTheme = item.theme - updatedMuteIconImage = PresentationResourcesChat.chatInstantMessageMuteIconImage(item.theme) + var updatedConsumableContentIcon: UIImage? + if item.presentationData.theme !== currentItem?.presentationData.theme { + updatedTheme = item.presentationData.theme + updatedInfoBackgroundImage = PresentationResourcesChat.chatInstantMessageInfoBackgroundImage(item.presentationData.theme) + updatedMuteIconImage = PresentationResourcesChat.chatInstantMessageMuteIconImage(item.presentationData.theme) + updatedConsumableContentIcon = PresentationResourcesChat.chatMediaConsumableContentIcon(item.presentationData.theme) } - let theme = item.theme + let instantVideoBackgroundImage = PresentationResourcesChat.chatInstantVideoBackgroundImage(item.presentationData.theme) + + let theme = item.presentationData.theme let isSecretMedia = item.message.containsSecretMedia - let incoming = item.message.effectivelyIncoming + let incoming = item.message.effectivelyIncoming(item.account.peerId) let imageSize = displaySize var updatedFile: TelegramMediaFile? @@ -115,6 +163,16 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { } } + var notConsumed = false + for attribute in item.message.attributes { + if let attribute = attribute as? ConsumableContentMessageAttribute { + if !attribute.consumed { + notConsumed = true + } + break + } + } + var updatedPlaybackStatus: Signal? if let updatedFile = updatedFile, updatedMedia { updatedPlaybackStatus = combineLatest(messageFileMediaResourceStatus(account: item.account, file: updatedFile, message: item.message), item.account.pendingMessageManager.pendingMessageStatus(item.message.id)) @@ -134,15 +192,20 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { let avatarInset: CGFloat var hasAvatar = false - if item.peerId.isGroupOrChannel && item.message.author != nil { - var isBroadcastChannel = false - if let peer = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { - isBroadcastChannel = true - } - - if !isBroadcastChannel { + switch item.chatLocation { + case let .peer(peerId): + if peerId.isGroupOrChannel && item.message.author != nil { + var isBroadcastChannel = false + if let peer = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { + isBroadcastChannel = true + } + + if !isBroadcastChannel { + hasAvatar = true + } + } + case .group: hasAvatar = true - } } if hasAvatar { @@ -156,7 +219,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { layoutInsets.top += layoutConstants.timestampHeaderHeight } - let videoFrame = CGRect(origin: CGPoint(x: (incoming ? (layoutConstants.bubble.edgeInset + avatarInset + layoutConstants.bubble.contentInsets.left) : (width - imageSize.width - layoutConstants.bubble.edgeInset - layoutConstants.bubble.contentInsets.left)), y: 0.0), size: imageSize) + let videoFrame = CGRect(origin: CGPoint(x: (incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + avatarInset + layoutConstants.bubble.contentInsets.left) : (params.width - params.rightInset - imageSize.width - layoutConstants.bubble.edgeInset - layoutConstants.bubble.contentInsets.left)), y: 0.0), size: imageSize) let arguments = TransformImageArguments(corners: ImageCorners(radius: videoFrame.size.width / 2.0), imageSize: videoFrame.size, boundingSize: videoFrame.size, intrinsicInsets: UIEdgeInsets()) @@ -165,21 +228,53 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { var replyBackgroundImage: UIImage? for attribute in item.message.attributes { if let replyAttribute = attribute as? ReplyMessageAttribute, let replyMessage = item.message.associatedMessages[replyAttribute.messageId] { - let availableWidth = max(60.0, width - imageSize.width - 20.0 - layoutConstants.bubble.edgeInset * 2.0 - avatarInset - layoutConstants.bubble.contentInsets.left) - replyInfoApply = makeReplyInfoLayout(item.theme, item.account, .standalone, replyMessage, CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude)) + let availableWidth = max(60.0, params.width - params.leftInset - params.rightInset - imageSize.width - 20.0 - layoutConstants.bubble.edgeInset * 2.0 - avatarInset - layoutConstants.bubble.contentInsets.left) + replyInfoApply = makeReplyInfoLayout(item.presentationData.theme, item.presentationData.strings, item.account, .standalone, replyMessage, CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude)) if let currentReplyBackgroundNode = currentReplyBackgroundNode { updatedReplyBackgroundNode = currentReplyBackgroundNode } else { updatedReplyBackgroundNode = ASImageNode() } - replyBackgroundImage = PresentationResourcesChat.chatServiceBubbleFillImage(item.theme) + replyBackgroundImage = PresentationResourcesChat.chatServiceBubbleFillImage(item.presentationData.theme) break } } + var forwardInfoSizeApply: (CGSize, () -> ChatMessageForwardInfoNode)? + var updatedForwardBackgroundNode: ASImageNode? + var forwardBackgroundImage: UIImage? + if let forwardInfo = item.message.forwardInfo { + let forwardSource: Peer + let forwardAuthorSignature: String? + + if let source = forwardInfo.source { + forwardSource = source + if let authorSignature = forwardInfo.authorSignature { + forwardAuthorSignature = authorSignature + } else if forwardInfo.author.id != source.id { + forwardAuthorSignature = forwardInfo.author.displayTitle + } else { + forwardAuthorSignature = nil + } + } else { + forwardSource = forwardInfo.author + forwardAuthorSignature = nil + } + let availableWidth = max(60.0, params.width - params.leftInset - params.rightInset - imageSize.width + 30.0 - layoutConstants.bubble.edgeInset * 2.0 - avatarInset - layoutConstants.bubble.contentInsets.left) + forwardInfoSizeApply = makeForwardInfoLayout(item.presentationData.theme, item.presentationData.strings, .standalone, forwardSource, forwardAuthorSignature, CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude)) + + if let currentForwardBackgroundNode = currentForwardBackgroundNode { + updatedForwardBackgroundNode = currentForwardBackgroundNode + } else { + updatedForwardBackgroundNode = ASImageNode() + } + + forwardBackgroundImage = PresentationResourcesChat.chatServiceBubbleFillImage(item.presentationData.theme) + } + let statusType: ChatMessageDateAndStatusType - if item.message.effectivelyIncoming { + if item.message.effectivelyIncoming(item.account.peerId) { statusType = .FreeIncoming } else { if item.message.flags.contains(.Failed) { @@ -191,10 +286,6 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { } } - var t = Int(item.message.timestamp) - var timeinfo = tm() - localtime_r(&t, &timeinfo) - var edited = false var sentViaBot = false var viewCount: Int? @@ -208,7 +299,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { } } - var dateText = String(format: "%02d:%02d", arguments: [Int(timeinfo.tm_hour), Int(timeinfo.tm_min)]) + var dateText = stringForMessageTimestamp(timestamp: item.message.timestamp, timeFormat: item.presentationData.timeFormat) if let author = item.message.author as? TelegramUser { if author.botInfo != nil { @@ -219,26 +310,52 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { } } - let (dateAndStatusSize, dateAndStatusApply) = makeDateAndStatusLayout(item.theme, edited && !sentViaBot, viewCount, dateText, statusType, CGSize(width: width, height: CGFloat.greatestFiniteMagnitude)) + let (dateAndStatusSize, dateAndStatusApply) = makeDateAndStatusLayout(item.presentationData.theme, item.presentationData.strings, edited && !sentViaBot, viewCount, dateText, statusType, CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude)) - return (ListViewItemNodeLayout(contentSize: CGSize(width: width, height: imageSize.height), insets: layoutInsets), { [weak self] animation in + return (ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: imageSize.height), insets: layoutInsets), { [weak self] animation in if let strongSelf = self { strongSelf.appliedItem = item strongSelf.videoFrame = videoFrame + if let updatedInfoBackgroundImage = updatedInfoBackgroundImage { + strongSelf.infoBackgroundNode.image = updatedInfoBackgroundImage + } + if let updatedMuteIconImage = updatedMuteIconImage { strongSelf.muteIconNode.image = updatedMuteIconImage } + if let updatedConsumableContentIcon = updatedConsumableContentIcon { + strongSelf.consumableContentNode.image = updatedConsumableContentIcon + } + strongSelf.telegramFile = updatedFile - if let image = strongSelf.muteIconNode.image { - strongSelf.muteIconNode.frame = CGRect(origin: CGPoint(x: floor(videoFrame.minX + (videoFrame.size.width - image.size.width) / 2.0), y: videoFrame.maxY - image.size.height - 8.0), size: image.size) + if let infoBackgroundImage = strongSelf.infoBackgroundNode.image, let muteImage = strongSelf.muteIconNode.image, let consumableContentImage = strongSelf.consumableContentNode.image { + var infoWidth = muteImage.size.width + if notConsumed { + infoWidth += infoBackgroundImage.size.height - 6.0 + } + let transition: ContainedViewLayoutTransition + if animation.isAnimated { + transition = .animated(duration: 0.2, curve: .spring) + } else { + transition = .immediate + } + let infoBackgroundFrame = CGRect(origin: CGPoint(x: floor(videoFrame.minX + (videoFrame.size.width - infoWidth) / 2.0), y: videoFrame.maxY - infoBackgroundImage.size.height - 8.0), size: CGSize(width: infoWidth, height: infoBackgroundImage.size.height)) + transition.updateFrame(node: strongSelf.infoBackgroundNode, frame: infoBackgroundFrame) + let muteIconFrame = CGRect(origin: CGPoint(x: infoBackgroundFrame.width - muteImage.size.width, y: 0.0), size: muteImage.size) + transition.updateFrame(node: strongSelf.muteIconNode, frame: muteIconFrame) + let consumableContentFrame = CGRect(origin: CGPoint(x: floor((infoBackgroundFrame.height - consumableContentImage.size.width) / 2.0), y: floor((infoBackgroundFrame.height - consumableContentImage.size.width) / 2.0)), size: consumableContentImage.size) + transition.updateFrame(node: strongSelf.consumableContentNode, frame: consumableContentFrame) + transition.updateAlpha(node: strongSelf.consumableContentNode, alpha: notConsumed ? 1.0 : 0.0) } if let updatedPlaybackStatus = updatedPlaybackStatus { strongSelf.playbackStatusDisposable.set((updatedPlaybackStatus |> deliverOnMainQueue).start(next: { status in if let strongSelf = self, let videoFrame = strongSelf.videoFrame { + strongSelf.status = status + let displayMute: Bool switch status { case let .fetchStatus(fetchStatus): @@ -251,15 +368,15 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { case .playbackStatus: displayMute = false } - if displayMute != (!strongSelf.muteIconNode.alpha.isZero) { + if displayMute != (!strongSelf.infoBackgroundNode.alpha.isZero) { if displayMute { - strongSelf.muteIconNode.alpha = 1.0 - strongSelf.muteIconNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) - strongSelf.muteIconNode.layer.animateScale(from: 0.4, to: 1.0, duration: 0.15) + strongSelf.infoBackgroundNode.alpha = 1.0 + strongSelf.infoBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + strongSelf.infoBackgroundNode.layer.animateScale(from: 0.4, to: 1.0, duration: 0.15) } else { - strongSelf.muteIconNode.alpha = 0.0 - strongSelf.muteIconNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15) - strongSelf.muteIconNode.layer.animateScale(from: 1.0, to: 0.4, duration: 0.15) + strongSelf.infoBackgroundNode.alpha = 0.0 + strongSelf.infoBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15) + strongSelf.infoBackgroundNode.layer.animateScale(from: 1.0, to: 0.4, duration: 0.15) } } @@ -279,6 +396,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { if progressRequired { if strongSelf.statusNode == nil { let statusNode = RadialStatusNode(backgroundNodeColor: theme.chat.bubble.mediaOverlayControlBackgroundColor) + statusNode.isUserInteractionEnabled = false statusNode.frame = CGRect(origin: CGPoint(x: videoFrame.origin.x + floor((videoFrame.size.width - 50.0) / 2.0), y: videoFrame.origin.y + floor((videoFrame.size.height - 50.0) / 2.0)), size: CGSize(width: 50.0, height: 50.0)) strongSelf.statusNode = statusNode strongSelf.addSubnode(statusNode) @@ -328,37 +446,56 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { } }) } + + if case .playbackStatus = status { + let playbackStatusNode: InstantVideoRadialStatusNode + if let current = strongSelf.playbackStatusNode { + playbackStatusNode = current + } else { + playbackStatusNode = InstantVideoRadialStatusNode(color: UIColor(white: 1.0, alpha: 0.8)) + strongSelf.addSubnode(playbackStatusNode) + strongSelf.playbackStatusNode = playbackStatusNode + } + playbackStatusNode.frame = videoFrame.insetBy(dx: 1.5, dy: 1.5) + if let updatedFile = updatedFile { + playbackStatusNode.status = messageFileMediaPlaybackStatus(account: item.account, file: updatedFile, message: item.message) + } + } else if let playbackStatusNode = strongSelf.playbackStatusNode { + strongSelf.playbackStatusNode = nil + playbackStatusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak playbackStatusNode] _ in + playbackStatusNode?.removeFromSupernode() + }) + } } })) } dateAndStatusApply(false) - strongSelf.dateAndStatusNode.frame = CGRect(origin: CGPoint(x: min(floor(videoFrame.midX) + 70.0, width - dateAndStatusSize.width - 4.0), y: videoFrame.maxY - dateAndStatusSize.height), size: dateAndStatusSize) + strongSelf.dateAndStatusNode.frame = CGRect(origin: CGPoint(x: min(floor(videoFrame.midX) + 70.0, params.width - params.rightInset - dateAndStatusSize.width - 4.0), y: videoFrame.maxY - dateAndStatusSize.height), size: dateAndStatusSize) - if let telegramFile = updatedFile, updatedMedia, let context = item.account.applicationContext as? TelegramApplicationContext { - if let hostedVideoNode = strongSelf.hostedVideoNode { - hostedVideoNode.removeFromSupernode() + if let telegramFile = updatedFile, updatedMedia { + if let videoNode = strongSelf.videoNode { + videoNode.removeFromSupernode() } - let hostedVideoNode = InstantVideoNode(theme: item.theme, manager: context.mediaManager, account: item.account, source: .messageMedia(stableId: item.message.stableId, file: telegramFile), priority: 1, withSound: false) - hostedVideoNode.tapped = { + let videoNode = UniversalVideoNode(postbox: item.account.postbox, audioSession: item.account.telegramApplicationContext.mediaManager.audioSession, manager: item.account.telegramApplicationContext.mediaManager.universalVideoManager, decoration: ChatBubbleInstantVideoDecoration(diameter: 214.0, backgroundImage: instantVideoBackgroundImage, tapped: { if let strongSelf = self { if let item = strongSelf.item { - if strongSelf.muteIconNode.alpha.isZero { - item.account.telegramApplicationContext.mediaManager.playlistPlayerControl(.stop) + if strongSelf.infoBackgroundNode.alpha.isZero { + item.account.telegramApplicationContext.mediaManager.playlistControl(.playback(.togglePlayPause), type: .voice) } else { - strongSelf.controllerInteraction?.openMessage(item.message.id) + let _ = item.controllerInteraction.openMessage(item.message.id) } } } - } - strongSelf.hostedVideoNode = hostedVideoNode - strongSelf.insertSubnode(hostedVideoNode, belowSubnode: strongSelf.dateAndStatusNode) - hostedVideoNode.setShouldAcquireContext(strongSelf.shouldAcquireVideoContext) + }), content: NativeVideoContent(id: .message(item.message.id, telegramFile.fileId), file: telegramFile, streamVideo: false, enableSound: false), priority: .embedded, autoplay: true) + strongSelf.videoNode = videoNode + strongSelf.insertSubnode(videoNode, belowSubnode: strongSelf.dateAndStatusNode) + videoNode.canAttachContent = strongSelf.shouldAcquireVideoContext } - if let hostedVideoNode = strongSelf.hostedVideoNode { - hostedVideoNode.frame = videoFrame - hostedVideoNode.updateLayout(arguments.boundingSize) + if let videoNode = strongSelf.videoNode { + videoNode.frame = videoFrame + videoNode.updateLayout(size: arguments.boundingSize, transition: .immediate) } if let updatedReplyBackgroundNode = updatedReplyBackgroundNode { @@ -378,30 +515,43 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { strongSelf.replyInfoNode = replyInfoNode strongSelf.addSubnode(replyInfoNode) } - let replyInfoFrame = CGRect(origin: CGPoint(x: (!incoming ? (layoutConstants.bubble.edgeInset + 10.0) : (width - replyInfoSize.width - layoutConstants.bubble.edgeInset - 10.0)), y: imageSize.height - replyInfoSize.height - 8.0), size: replyInfoSize) + let replyInfoFrame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 10.0) : (params.width - params.rightInset - replyInfoSize.width - layoutConstants.bubble.edgeInset - 10.0)), y: imageSize.height - replyInfoSize.height - 8.0), size: replyInfoSize) replyInfoNode.frame = replyInfoFrame strongSelf.replyBackgroundNode?.frame = CGRect(origin: CGPoint(x: replyInfoFrame.minX - 4.0, y: replyInfoFrame.minY - 2.0), size: CGSize(width: replyInfoFrame.size.width + 8.0, height: replyInfoFrame.size.height + 5.0)) } else if let replyInfoNode = strongSelf.replyInfoNode { replyInfoNode.removeFromSupernode() strongSelf.replyInfoNode = nil } + + if let updatedForwardBackgroundNode = updatedForwardBackgroundNode { + if strongSelf.forwardBackgroundNode == nil { + strongSelf.forwardBackgroundNode = updatedForwardBackgroundNode + strongSelf.addSubnode(updatedForwardBackgroundNode) + updatedForwardBackgroundNode.image = forwardBackgroundImage + } + } else if let forwardBackgroundNode = strongSelf.forwardBackgroundNode { + forwardBackgroundNode.removeFromSupernode() + strongSelf.forwardBackgroundNode = nil + } + + if let (forwardInfoSize, forwardInfoApply) = forwardInfoSizeApply { + let forwardInfoNode = forwardInfoApply() + if strongSelf.forwardInfoNode == nil { + strongSelf.forwardInfoNode = forwardInfoNode + strongSelf.addSubnode(forwardInfoNode) + } + let forwardInfoFrame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 12.0) : (params.width - params.rightInset - forwardInfoSize.width - layoutConstants.bubble.edgeInset - 12.0)), y: 8.0), size: forwardInfoSize) + forwardInfoNode.frame = forwardInfoFrame + strongSelf.forwardBackgroundNode?.frame = CGRect(origin: CGPoint(x: forwardInfoFrame.minX - 6.0, y: forwardInfoFrame.minY - 2.0), size: CGSize(width: forwardInfoFrame.size.width + 10.0, height: forwardInfoFrame.size.height + 4.0)) + } else if let forwardInfoNode = strongSelf.forwardInfoNode { + forwardInfoNode.removeFromSupernode() + strongSelf.forwardInfoNode = nil + } } }) } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { - super.animateInsertion(currentTimestamp, duration: duration, short: short) - - self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - } - - override func animateAdded(_ currentTimestamp: Double, duration: Double) { - super.animateAdded(currentTimestamp, duration: duration) - - self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - } - @objc func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { switch recognizer.state { case .ended: @@ -410,7 +560,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { case .tap: if let avatarNode = self.accessoryItemNode as? ChatMessageAvatarAccessoryItemNode, avatarNode.frame.contains(location) { if let item = self.item, let author = item.message.author { - self.controllerInteraction?.openPeer(author.id, .info, item.message.id) + item.controllerInteraction.openPeer(author.id, .info, item.message.id) } return } @@ -419,22 +569,42 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { if let item = self.item { for attribute in item.message.attributes { if let attribute = attribute as? ReplyMessageAttribute { - self.controllerInteraction?.navigateToMessage(item.message.id, attribute.messageId) + item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId) return } } } } - if let item = self.item, let hostedVideoNode = self.hostedVideoNode, hostedVideoNode.frame.contains(location) { - self.controllerInteraction?.openMessage(item.message.id) + if let forwardInfoNode = self.forwardInfoNode, forwardInfoNode.frame.contains(location) { + if let item = self.item, let forwardInfo = item.message.forwardInfo { + if let sourceMessageId = forwardInfo.sourceMessageId { + item.controllerInteraction.navigateToMessage(item.message.id, sourceMessageId) + } else { + item.controllerInteraction.openPeer(forwardInfo.source?.id ?? forwardInfo.author.id, .chat(textInputState: nil), nil) + } + return + } + } + + if let statusNode = self.statusNode, statusNode.supernode != nil, !statusNode.isHidden, statusNode.frame.contains(location) { + self.progressPressed() return } - self.controllerInteraction?.clickThroughMessage() + if let item = self.item, let videoNode = self.videoNode, videoNode.frame.contains(location) { + if self.infoBackgroundNode.alpha.isZero { + item.account.telegramApplicationContext.mediaManager.playlistControl(.playback(.togglePlayPause), type: .voice) + } else { + let _ = item.controllerInteraction.openMessage(item.message.id) + } + return + } + + self.item?.controllerInteraction.clickThroughMessage() case .longTap, .doubleTap: - if let item = self.item, let hostedVideoNode = self.hostedVideoNode, hostedVideoNode.frame.contains(location) { - self.controllerInteraction?.openMessageContextMenu(item.message.id, self, hostedVideoNode.frame) + if let item = self.item, let videoNode = self.videoNode, videoNode.frame.contains(location) { + item.controllerInteraction.openMessageContextMenu(item.message.id, self, videoNode.frame) } case .hold: break @@ -445,22 +615,116 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { } } + @objc func swipeToReplyGesture(_ recognizer: ChatSwipeToReplyRecognizer) { + switch recognizer.state { + case .began: + self.currentSwipeToReplyTranslation = 0.0 + if self.swipeToReplyFeedback == nil { + self.swipeToReplyFeedback = HapticFeedback() + self.swipeToReplyFeedback?.prepareImpact() + } + (self.view.window as? WindowHost)?.cancelInteractiveKeyboardGestures() + case .changed: + let translation = recognizer.translation(in: self.view) + var animateReplyNodeIn = false + if (translation.x < -45.0) != (self.currentSwipeToReplyTranslation < -45.0) { + if translation.x < -45.0, self.swipeToReplyNode == nil, let item = self.item { + self.swipeToReplyFeedback?.impact() + + let swipeToReplyNode = ChatMessageSwipeToReplyNode(fillColor: item.presentationData.theme.chat.bubble.shareButtonFillColor, strokeColor: item.presentationData.theme.chat.bubble.shareButtonStrokeColor, foregroundColor: item.presentationData.theme.chat.bubble.shareButtonForegroundColor) + self.swipeToReplyNode = swipeToReplyNode + self.addSubnode(swipeToReplyNode) + animateReplyNodeIn = true + } + } + self.currentSwipeToReplyTranslation = translation.x + var bounds = self.bounds + bounds.origin.x = -translation.x + self.bounds = bounds + + if let swipeToReplyNode = self.swipeToReplyNode { + swipeToReplyNode.frame = CGRect(origin: CGPoint(x: bounds.size.width, y: floor((self.contentSize.height - 33.0) / 2.0)), size: CGSize(width: 33.0, height: 33.0)) + if animateReplyNodeIn { + swipeToReplyNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.12) + swipeToReplyNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4) + } + } + case .cancelled, .ended: + self.swipeToReplyFeedback = nil + + let translation = recognizer.translation(in: self.view) + if case .ended = recognizer.state, translation.x < -45.0 { + if let item = self.item { + item.controllerInteraction.setupReply(item.message.id) + } + } + var bounds = self.bounds + let previousBounds = bounds + bounds.origin.x = 0.0 + self.bounds = bounds + self.layer.animateBounds(from: previousBounds, to: bounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + if let swipeToReplyNode = self.swipeToReplyNode { + self.swipeToReplyNode = nil + swipeToReplyNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak swipeToReplyNode] _ in + swipeToReplyNode?.removeFromSupernode() + }) + swipeToReplyNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + } + default: + break + } + } + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if !self.bounds.contains(point) { + return nil + } + if let statusNode = self.statusNode, statusNode.supernode != nil, !statusNode.isHidden, statusNode.frame.contains(point) { + return self.view + } return super.hitTest(point, with: event) } + private func progressPressed() { + guard let item = self.item, let file = self.telegramFile else { + return + } + if let status = self.status { + switch status { + case let .fetchStatus(fetchStatus): + switch fetchStatus { + case .Fetching: + if item.message.flags.isSending { + let messageId = item.message.id + let _ = item.account.postbox.modify({ modifier -> Void in + modifier.deleteMessages([messageId]) + }).start() + } else { + self.videoNode?.fetchControl(.cancel) + } + case .Remote: + self.videoNode?.fetchControl(.fetch) + case .Local: + break + } + default: + break + } + } + } + override func updateSelectionState(animated: Bool) { - guard let controllerInteraction = self.controllerInteraction else { + guard let item = self.item else { return } - if let selectionState = controllerInteraction.selectionState { + if let selectionState = item.controllerInteraction.selectionState { var selected = false var incoming = true - if let item = self.item { - selected = selectionState.selectedIds.contains(item.message.id) - incoming = item.message.effectivelyIncoming - } + + selected = selectionState.selectedIds.contains(item.message.id) + incoming = item.message.effectivelyIncoming(item.account.peerId) + let offset: CGFloat = incoming ? 42.0 : 0.0 if let selectionNode = self.selectionNode { @@ -468,9 +732,9 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { selectionNode.frame = CGRect(origin: CGPoint(x: -offset, y: 0.0), size: CGSize(width: self.contentBounds.size.width, height: self.contentBounds.size.height)) self.subnodeTransform = CATransform3DMakeTranslation(offset, 0.0, 0.0); } else { - let selectionNode = ChatMessageSelectionNode(toggle: { [weak self] in + let selectionNode = ChatMessageSelectionNode(theme: item.presentationData.theme, toggle: { [weak self] value in if let strongSelf = self, let item = strongSelf.item { - strongSelf.controllerInteraction?.toggleMessageSelection(item.message.id) + item.controllerInteraction.toggleMessagesSelection([item.message.id], value) } }) @@ -510,4 +774,22 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { } } } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + super.animateInsertion(currentTimestamp, duration: duration, short: short) + + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + super.animateRemoved(currentTimestamp, duration: duration) + + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + } + + override func animateAdded(_ currentTimestamp: Double, duration: Double) { + super.animateAdded(currentTimestamp, duration: duration) + + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } } diff --git a/TelegramUI/ChatMessageInteractiveFileNode.swift b/TelegramUI/ChatMessageInteractiveFileNode.swift index 2dfde1bccb..8d5aa2923d 100644 --- a/TelegramUI/ChatMessageInteractiveFileNode.swift +++ b/TelegramUI/ChatMessageInteractiveFileNode.swift @@ -18,6 +18,8 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { private let titleNode: TextNode private let descriptionNode: TextNode private let waveformNode: AudioWaveformNode + private let waveformForegroundNode: AudioWaveformNode + private var waveformScrubbingNode: MediaPlayerScrubbingNode? private let dateAndStatusNode: ChatMessageDateAndStatusNode private let consumableContentNode: ASImageNode @@ -26,6 +28,8 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { private var tapRecognizer: UITapGestureRecognizer? private let statusDisposable = MetaDisposable() + private let playbackStatusDisposable = MetaDisposable() + private let playbackStatus = Promise() private let fetchControls = Atomic(value: nil) private var resourceStatus: FileMediaResourceStatus? private let fetchDisposable = MetaDisposable() @@ -47,6 +51,9 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { self.descriptionNode.isLayerBacked = true self.waveformNode = AudioWaveformNode() + self.waveformNode.isLayerBacked = true + self.waveformForegroundNode = AudioWaveformNode() + self.waveformForegroundNode.isLayerBacked = true self.dateAndStatusNode = ChatMessageDateAndStatusNode() @@ -60,6 +67,7 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { deinit { self.statusDisposable.dispose() + self.playbackStatusDisposable.dispose() self.fetchDisposable.dispose() } @@ -92,8 +100,8 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { } } case .playbackStatus: - if let account = self.account, let applicationContext = account.applicationContext as? TelegramApplicationContext { - applicationContext.mediaManager.playlistPlayerControl(.playback(.togglePlayPause)) + if let account = self.account, let applicationContext = account.applicationContext as? TelegramApplicationContext, let message = self.message, let type = peerMessageMediaPlayerType(message) { + applicationContext.mediaManager.playlistControl(.playback(.togglePlayPause), type: type) } } } @@ -105,7 +113,7 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { } } - func asyncLayout() -> (_ account: Account, _ theme: PresentationTheme, _ strings: PresentationStrings, _ message: Message, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> Void))) { + func asyncLayout() -> (_ account: Account, _ presentationData: ChatPresentationData, _ message: Message, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> Void))) { let currentFile = self.file let titleAsyncLayout = TextNode.asyncLayout(self.titleNode) @@ -115,16 +123,17 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { let currentMessage = self.message let currentTheme = self.themeAndStrings?.0 - return { account, theme, strings, message, file, automaticDownload, incoming, dateAndStatusType, constrainedSize in + return { account, presentationData, message, file, automaticDownload, incoming, dateAndStatusType, constrainedSize in var updatedTheme: PresentationTheme? - if theme !== currentTheme { - updatedTheme = theme + if presentationData.theme !== currentTheme { + updatedTheme = presentationData.theme } return (CGFloat.greatestFiniteMagnitude, { constrainedSize in - //var updateImageSignal: Signal DrawingContext, NoError>? + var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? var updatedStatusSignal: Signal? + var updatedPlaybackStatusSignal: Signal? var updatedFetchControls: FetchControls? var mediaUpdated = false @@ -139,7 +148,13 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { statusUpdated = true } + let hasThumbnail = !file.previewRepresentations.isEmpty && !file.isMusic && !file.isVoice + if mediaUpdated { + if let _ = largestImageRepresentation(file.previewRepresentations) { + updateImageSignal = chatMessageImageFile(account: account, file: file, thumbnail: true) + } + let messageId = message.id updatedFetchControls = FetchControls(fetch: { [weak self] in if let strongSelf = self { @@ -152,6 +167,7 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { if statusUpdated { updatedStatusSignal = messageFileMediaResourceStatus(account: account, file: file, message: message) + updatedPlaybackStatusSignal = messageFileMediaPlaybackStatus(account: account, file: file, message: message) } var statusSize: CGSize? @@ -162,9 +178,9 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { if let attribute = attribute as? ConsumableContentMessageAttribute { if !attribute.consumed { if incoming { - consumableContentIcon = PresentationResourcesChat.chatBubbleConsumableContentIncomingIcon(theme) + consumableContentIcon = PresentationResourcesChat.chatBubbleConsumableContentIncomingIcon(presentationData.theme) } else { - consumableContentIcon = PresentationResourcesChat.chatBubbleConsumableContentOutgoingIcon(theme) + consumableContentIcon = PresentationResourcesChat.chatBubbleConsumableContentOutgoingIcon(presentationData.theme) } } break @@ -172,10 +188,6 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { } if let statusType = dateAndStatusType { - var t = Int(message.timestamp) - var timeinfo = tm() - localtime_r(&t, &timeinfo) - var edited = false var sentViaBot = false var viewCount: Int? @@ -189,7 +201,7 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { } } - var dateText = String(format: "%02d:%02d", arguments: [Int(timeinfo.tm_hour), Int(timeinfo.tm_min)]) + var dateText = stringForMessageTimestamp(timestamp: message.timestamp, timeFormat: presentationData.timeFormat) if let author = message.author as? TelegramUser { if author.botInfo != nil { @@ -200,7 +212,7 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { } } - let (size, apply) = statusLayout(theme, edited && !sentViaBot, viewCount, dateText, statusType, constrainedSize) + let (size, apply) = statusLayout(presentationData.theme, presentationData.strings, edited && !sentViaBot, viewCount, dateText, statusType, constrainedSize) statusSize = size statusApply = apply } @@ -213,7 +225,7 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { var isVoice = false var audioDuration: Int32 = 0 - let bubbleTheme = theme.chat.bubble + let bubbleTheme = presentationData.theme.chat.bubble for attribute in file.attributes { if case let .Audio(voice, duration, title, performer, waveform) = attribute { @@ -221,8 +233,12 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { if let currentUpdatedStatusSignal = updatedStatusSignal { updatedStatusSignal = currentUpdatedStatusSignal |> map { status in switch status { - case .fetchStatus: - return .fetchStatus(.Local) + case let .fetchStatus(fetchStatus): + if !voice { + return .fetchStatus(.Local) + } else { + return .fetchStatus(fetchStatus) + } case .playbackStatus: return status } @@ -275,10 +291,13 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { descriptionString = NSAttributedString(string: descriptionText, font: descriptionFont, textColor:incoming ? bubbleTheme.incomingFileDescriptionColor : bubbleTheme.outgoingFileDescriptionColor) } - let textConstrainedSize = CGSize(width: constrainedSize.width - 44.0 - 8.0, height: constrainedSize.height) + var textConstrainedSize = CGSize(width: constrainedSize.width - 44.0 - 8.0, height: constrainedSize.height) + if hasThumbnail { + textConstrainedSize.width -= 80.0 + } - let (titleLayout, titleApply) = titleAsyncLayout(titleString, nil, 1, .middle, textConstrainedSize, .natural, nil, UIEdgeInsets()) - let (descriptionLayout, descriptionApply) = descriptionAsyncLayout(descriptionString, nil, 1, .middle, textConstrainedSize, .natural, nil, UIEdgeInsets()) + let (titleLayout, titleApply) = titleAsyncLayout(TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (descriptionLayout, descriptionApply) = descriptionAsyncLayout(TextNodeLayoutArguments(attributedString: descriptionString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let minVoiceWidth: CGFloat = 120.0 let maxVoiceWidth = constrainedSize.width @@ -286,33 +305,48 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { let minVoiceLength: CGFloat = 2.0 let minLayoutWidth: CGFloat - if isVoice { - //y = a exp bx - //b = log (y1/y2) / (x1-x2) - //a = y1 / exp bx1 - - /*let b = log(maxVoiceWidth / minVoiceWidth) / (maxVoiceLength) - let a = minVoiceWidth / exp(CGFloat(0.0)) - - let y = a * exp(b * min(maxVoiceLength, CGFloat(audioDuration))) - - minLayoutWidth = floor(y)*/ - + if hasThumbnail { + minLayoutWidth = max(titleLayout.size.width, descriptionLayout.size.width) + 86.0 + } else if isVoice { let calcDuration = max(minVoiceLength, min(maxVoiceLength, CGFloat(audioDuration))) minLayoutWidth = minVoiceWidth + (maxVoiceWidth - minVoiceWidth) * (calcDuration - minVoiceLength) / (maxVoiceLength - minVoiceLength) } else { minLayoutWidth = max(titleLayout.size.width, descriptionLayout.size.width) + 44.0 + 8.0 } - let fileIconImage = incoming ? PresentationResourcesChat.chatBubbleRadialIndicatorFileIconIncoming(theme) : PresentationResourcesChat.chatBubbleRadialIndicatorFileIconOutgoing(theme) + let fileIconImage: UIImage? + if hasThumbnail { + fileIconImage = nil + } else { + fileIconImage = incoming ? PresentationResourcesChat.chatBubbleRadialIndicatorFileIconIncoming(presentationData.theme) : PresentationResourcesChat.chatBubbleRadialIndicatorFileIconOutgoing(presentationData.theme) + } return (minLayoutWidth, { boundingWidth in - let progressDiameter: CGFloat = isVoice ? 37.0 : 44.0 - let progressFrame = CGRect(origin: CGPoint(x: 0.0, y: isVoice ? -5.0 : 0.0), size: CGSize(width: progressDiameter, height: progressDiameter)) + let progressDiameter: CGFloat = (isVoice && !hasThumbnail) ? 37.0 : 44.0 + + var iconFrame: CGRect? + let progressFrame: CGRect + let controlAreaWidth: CGFloat + + if hasThumbnail { + let currentIconFrame = CGRect(origin: CGPoint(x: -1.0, y: -7.0), size: CGSize(width: 74.0, height: 74.0)) + iconFrame = currentIconFrame + progressFrame = CGRect(origin: CGPoint(x: currentIconFrame.minX + floor((currentIconFrame.size.width - progressDiameter) / 2.0), y: currentIconFrame.minY + floor((currentIconFrame.size.height - progressDiameter) / 2.0)), size: CGSize(width: progressDiameter, height: progressDiameter)) + controlAreaWidth = 86.0 + } else { + progressFrame = CGRect(origin: CGPoint(x: 0.0, y: isVoice ? -5.0 : 0.0), size: CGSize(width: progressDiameter, height: progressDiameter)) + controlAreaWidth = progressFrame.maxX + 8.0 + } let titleAndDescriptionHeight = titleLayout.size.height - 1.0 + descriptionLayout.size.height - let titleFrame = CGRect(origin: CGPoint(x: progressFrame.maxX + 8.0, y: floor((44.0 - titleAndDescriptionHeight) / 2.0)), size: titleLayout.size) + let normHeight: CGFloat + if hasThumbnail { + normHeight = 64.0 + } else { + normHeight = 44.0 + } + let titleFrame = CGRect(origin: CGPoint(x: controlAreaWidth, y: floor((normHeight - titleAndDescriptionHeight) / 2.0)), size: titleLayout.size) let descriptionFrame: CGRect if isVoice { @@ -321,17 +355,31 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { descriptionFrame = CGRect(origin: CGPoint(x: titleFrame.minX, y: titleFrame.maxY - 1.0), size: descriptionLayout.size) } - let fittedLayoutSize: CGSize - if isVoice { + var fittedLayoutSize: CGSize + if hasThumbnail { + let textSizes = titleFrame.union(descriptionFrame).size + fittedLayoutSize = CGSize(width: textSizes.width + controlAreaWidth, height: 59.0) + } else if isVoice { fittedLayoutSize = CGSize(width: minLayoutWidth, height: 27.0) } else { - fittedLayoutSize = titleFrame.union(descriptionFrame).union(progressFrame).size + let unionSize = titleFrame.union(descriptionFrame).union(progressFrame).size + fittedLayoutSize = CGSize(width: unionSize.width, height: unionSize.height + 4.0) + } + + var statusFrame: CGRect? + if let statusSize = statusSize { + statusFrame = CGRect(origin: CGPoint(x: boundingWidth - statusSize.width, y: fittedLayoutSize.height - statusSize.height + 10.0), size: statusSize) + } + + if let statusFrameValue = statusFrame, descriptionFrame.intersects(statusFrameValue) { + fittedLayoutSize.height += 10.0 + statusFrame = statusFrameValue.offsetBy(dx: 0.0, dy: 10.0) } return (fittedLayoutSize, { [weak self] in if let strongSelf = self { strongSelf.account = account - strongSelf.themeAndStrings = (theme, strings) + strongSelf.themeAndStrings = (presentationData.theme, presentationData.strings) strongSelf.message = message strongSelf.file = file @@ -353,30 +401,62 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { strongSelf.consumableContentNode.removeFromSupernode() } - if let statusApply = statusApply, let statusSize = statusSize { + if let statusApply = statusApply, let statusFrame = statusFrame { if strongSelf.dateAndStatusNode.supernode == nil { strongSelf.addSubnode(strongSelf.dateAndStatusNode) } - strongSelf.dateAndStatusNode.frame = CGRect(origin: CGPoint(x: boundingWidth - statusSize.width, y: fittedLayoutSize.height - statusSize.height + 10.0), size: statusSize) + strongSelf.dateAndStatusNode.frame = statusFrame statusApply(false) } else if strongSelf.dateAndStatusNode.supernode != nil { strongSelf.dateAndStatusNode.removeFromSupernode() } if isVoice { - if strongSelf.waveformNode.supernode == nil { - strongSelf.addSubnode(strongSelf.waveformNode) + if strongSelf.waveformScrubbingNode == nil { + let waveformScrubbingNode = MediaPlayerScrubbingNode(content: .custom(backgroundNode: strongSelf.waveformNode, foregroundContentNode: strongSelf.waveformForegroundNode)) + waveformScrubbingNode.status = strongSelf.playbackStatus.get() + strongSelf.waveformScrubbingNode = waveformScrubbingNode + strongSelf.addSubnode(waveformScrubbingNode) } - strongSelf.waveformNode.frame = CGRect(origin: CGPoint(x: 43.0, y: -1.0), size: CGSize(width: boundingWidth - 41.0, height: 12.0)) - strongSelf.waveformNode.setup(color: incoming ? bubbleTheme.incomingAccentColor : bubbleTheme.outgoingAccentColor, waveform: audioWaveform) - } else if strongSelf.waveformNode.supernode != nil { - strongSelf.waveformNode.removeFromSupernode() + strongSelf.waveformScrubbingNode?.frame = CGRect(origin: CGPoint(x: 43.0, y: -1.0), size: CGSize(width: boundingWidth - 41.0, height: 12.0)) + let waveformColor: UIColor + if incoming { + if consumableContentIcon != nil { + waveformColor = bubbleTheme.incomingMediaActiveControlColor + } else { + waveformColor = bubbleTheme.incomingMediaInactiveControlColor + } + } else { + waveformColor = bubbleTheme.outgoingMediaInactiveControlColor + } + strongSelf.waveformNode.setup(color: waveformColor, waveform: audioWaveform) + strongSelf.waveformForegroundNode.setup(color: incoming ? bubbleTheme.incomingMediaActiveControlColor : bubbleTheme.outgoingMediaActiveControlColor, waveform: audioWaveform) + } else if let waveformScrubbingNode = strongSelf.waveformScrubbingNode { + strongSelf.waveformScrubbingNode = nil + waveformScrubbingNode.removeFromSupernode() } - /*if let updateImageSignal = updateImageSignal { - strongSelf.imageNode.setSignal(account, signal: updateImageSignal) - }*/ + if let iconFrame = iconFrame { + let iconNode: TransformImageNode + if let current = strongSelf.iconNode { + iconNode = current + } else { + iconNode = TransformImageNode() + strongSelf.iconNode = iconNode + strongSelf.insertSubnode(iconNode, at: 0) + let arguments = TransformImageArguments(corners: ImageCorners(radius: 8.0), imageSize: CGSize(width: 74.0, height: 74.0), boundingSize: CGSize(width: 74.0, height: 74.0), intrinsicInsets: UIEdgeInsets()) + let apply = iconNode.asyncLayout()(arguments) + apply() + } + if let updateImageSignal = updateImageSignal { + iconNode.setSignal(updateImageSignal) + } + iconNode.frame = iconFrame + } else if let iconNode = strongSelf.iconNode { + iconNode.removeFromSupernode() + strongSelf.iconNode = nil + } if let updatedStatusSignal = updatedStatusSignal { strongSelf.statusDisposable.set((updatedStatusSignal |> deliverOnMainQueue).start(next: { [weak strongSelf] status in @@ -385,7 +465,15 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { strongSelf.resourceStatus = status if strongSelf.statusNode == nil { - let statusNode = RadialStatusNode(backgroundNodeColor: incoming ? bubbleTheme.incomingAccentColor : bubbleTheme.outgoingAccentColor) + let backgroundNodeColor: UIColor + if strongSelf.iconNode != nil { + backgroundNodeColor = bubbleTheme.mediaOverlayControlBackgroundColor + } else if incoming { + backgroundNodeColor = bubbleTheme.incomingMediaActiveControlColor + } else { + backgroundNodeColor = bubbleTheme.outgoingMediaActiveControlColor + } + let statusNode = RadialStatusNode(backgroundNodeColor: backgroundNodeColor) strongSelf.statusNode = statusNode statusNode.frame = progressFrame strongSelf.addSubnode(statusNode) @@ -394,7 +482,14 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { } let state: RadialStatusNodeState - let statusForegroundColor = incoming ? bubbleTheme.incomingFillColor : bubbleTheme.outgoingFillColor + let statusForegroundColor: UIColor + if strongSelf.iconNode != nil { + statusForegroundColor = bubbleTheme.mediaOverlayControlForegroundColor + } else if incoming { + statusForegroundColor = bubbleTheme.incomingFillColor + } else { + statusForegroundColor = bubbleTheme.outgoingFillColor + } switch status { case let .fetchStatus(fetchStatus): switch fetchStatus { @@ -413,7 +508,7 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { state = .none } case .Remote: - if isAudio { + if isAudio && !isVoice { state = .play(statusForegroundColor) } else { state = .download(statusForegroundColor) @@ -443,6 +538,10 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { })) } + if let updatedPlaybackStatusSignal = updatedPlaybackStatusSignal { + strongSelf.playbackStatus.set(updatedPlaybackStatusSignal) + } + strongSelf.statusNode?.frame = progressFrame if let updatedFetchControls = updatedFetchControls { @@ -458,12 +557,12 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { } } - static func asyncLayout(_ node: ChatMessageInteractiveFileNode?) -> (_ account: Account, _ theme: PresentationTheme, _ strings: PresentationStrings, _ message: Message, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> ChatMessageInteractiveFileNode))) { + static func asyncLayout(_ node: ChatMessageInteractiveFileNode?) -> (_ account: Account, _ presentationData: ChatPresentationData, _ message: Message, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> ChatMessageInteractiveFileNode))) { let currentAsyncLayout = node?.asyncLayout() - return { account, theme, strings, message, file, automaticDownload, incoming, dateAndStatusType, constrainedSize in + return { account, presentationData, message, file, automaticDownload, incoming, dateAndStatusType, constrainedSize in var fileNode: ChatMessageInteractiveFileNode - var fileLayout: (_ account: Account, _ theme: PresentationTheme, _ strings: PresentationStrings, _ message: Message, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> Void))) + var fileLayout: (_ account: Account, _ presentationData: ChatPresentationData, _ message: Message, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> Void))) if let node = node, let currentAsyncLayout = currentAsyncLayout { fileNode = node @@ -473,7 +572,7 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { fileLayout = fileNode.asyncLayout() } - let (initialWidth, continueLayout) = fileLayout(account, theme, strings, message, file, automaticDownload, incoming, dateAndStatusType, constrainedSize) + let (initialWidth, continueLayout) = fileLayout(account, presentationData, message, file, automaticDownload, incoming, dateAndStatusType, constrainedSize) return (initialWidth, { constrainedSize in let (finalWidth, finalLayout) = continueLayout(constrainedSize) @@ -489,4 +588,25 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { }) } } + + func transitionNode(media: Media) -> ASDisplayNode? { + if let file = self.file, file.isEqual(media) { + return self.iconNode + } else { + return nil + } + } + + func updateHiddenMedia(_ media: [Media]?) { + var isHidden = false + if let file = self.file, let media = media { + for m in media { + if file.isEqual(m) { + isHidden = true + break + } + } + } + self.iconNode?.isHidden = isHidden + } } diff --git a/TelegramUI/ChatMessageInteractiveMediaBadge.swift b/TelegramUI/ChatMessageInteractiveMediaBadge.swift new file mode 100644 index 0000000000..98959141b6 --- /dev/null +++ b/TelegramUI/ChatMessageInteractiveMediaBadge.swift @@ -0,0 +1,108 @@ +import Foundation +import Display +import AsyncDisplayKit + +enum ChatMessageInteractiveMediaBadgeShape: Equatable { + case round + case corners(CGFloat) + + static func ==(lhs: ChatMessageInteractiveMediaBadgeShape, rhs: ChatMessageInteractiveMediaBadgeShape) -> Bool { + switch lhs { + case .round: + if case .round = rhs { + return true + } else { + return false + } + case let .corners(radius): + if case .corners(radius) = rhs { + return true + } else { + return false + } + } + } +} + +enum ChatMessageInteractiveMediaBadgeContent: Equatable { + case text(backgroundColor: UIColor, foregroundColor: UIColor, shape: ChatMessageInteractiveMediaBadgeShape, text: String) + + static func ==(lhs: ChatMessageInteractiveMediaBadgeContent, rhs: ChatMessageInteractiveMediaBadgeContent) -> Bool { + switch lhs { + case let .text(lhsBackgroundColor, lhsForegroundColor, lhsShape, lhsText): + if case let .text(rhsBackgroundColor, rhsForegroundColor, rhsShape, rhsText) = rhs, lhsBackgroundColor.isEqual(rhsBackgroundColor), lhsForegroundColor.isEqual(rhsForegroundColor), lhsShape == rhsShape, lhsText == rhsText { + return true + } else { + return false + } + } + } +} + +private let font = Font.regular(11.0) + +private final class ChatMessageInteractiveMediaBadgeParams: NSObject { + let content: ChatMessageInteractiveMediaBadgeContent? + + init(content: ChatMessageInteractiveMediaBadgeContent?) { + self.content = content + } +} + +final class ChatMessageInteractiveMediaBadge: ASDisplayNode { + var content: ChatMessageInteractiveMediaBadgeContent? { + didSet { + if oldValue != self.content { + self.setNeedsDisplay() + } + } + } + + override init() { + super.init() + + self.isLayerBacked = true + self.contentMode = .topLeft + self.contentsScale = UIScreenScale + } + + override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { + return ChatMessageInteractiveMediaBadgeParams(content: self.content) + } + + @objc override public class func display(withParameters: Any?, isCancelled: () -> Bool) -> UIImage? { + if let content = (withParameters as? ChatMessageInteractiveMediaBadgeParams)?.content { + switch content { + case let .text(backgroundColor, foregroundColor, shape, text): + let nsText: NSString = text as NSString + let textRect = nsText.boundingRect(with: CGSize(width: 200.0, height: 100.0), options: .usesLineFragmentOrigin, attributes: [.font: font], context: nil) + let imageSize = CGSize(width: ceil(textRect.size.width) + 10.0, height: 18.0) + return generateImage(imageSize, rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size)) + context.setBlendMode(.copy) + context.setFillColor(backgroundColor.cgColor) + switch shape { + case .round: + context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.height, height: size.height))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - size.height, y: 0.0), size: CGSize(width: size.height, height: size.height))) + context.fill(CGRect(origin: CGPoint(x: size.height / 2.0, y: 0.0), size: CGSize(width: size.width - size.height, height: size.height))) + case let .corners(radius): + let diameter = radius * 2.0 + context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: diameter, height: diameter))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: size.height - diameter), size: CGSize(width: diameter, height: diameter))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - diameter, y: 0.0), size: CGSize(width: diameter, height: diameter))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - diameter, y: size.height - diameter), size: CGSize(width: diameter, height: diameter))) + context.fill(CGRect(origin: CGPoint(x: 0.0, y: radius), size: CGSize(width: diameter, height: size.height - diameter))) + context.fill(CGRect(origin: CGPoint(x: radius, y: 0.0), size: CGSize(width: size.width - diameter, height: size.height))) + context.fill(CGRect(origin: CGPoint(x: size.width - diameter, y: radius), size: CGSize(width: diameter, height: size.height - diameter))) + } + context.setBlendMode(.normal) + UIGraphicsPushContext(context) + nsText.draw(at: CGPoint(x: floor((size.width - textRect.size.width) / 2.0) + textRect.origin.x, y: 2.0 + textRect.origin.y), withAttributes: [.font: font, .foregroundColor: foregroundColor]) + UIGraphicsPopContext() + }) + } + } + return nil + } +} diff --git a/TelegramUI/ChatMessageInteractiveMediaNode.swift b/TelegramUI/ChatMessageInteractiveMediaNode.swift index ce5348cc82..7f155dfb91 100644 --- a/TelegramUI/ChatMessageInteractiveMediaNode.swift +++ b/TelegramUI/ChatMessageInteractiveMediaNode.swift @@ -10,10 +10,16 @@ private struct FetchControls { let cancel: () -> Void } +enum InteractiveMediaNodeSizeCalculation { + case constrained(CGSize) + case unconstrained +} + final class ChatMessageInteractiveMediaNode: ASTransformNode { private let imageNode: TransformImageNode - private var videoNode: ManagedVideoNode? + private var videoNode: UniversalVideoNode? private var statusNode: RadialStatusNode? + private var badgeNode: ChatMessageInteractiveMediaBadge? private var timeoutNode: RadialTimeoutNode? private var labelNode: ChatMessageInteractiveMediaLabelNode? private var tapRecognizer: UITapGestureRecognizer? @@ -33,13 +39,12 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { if let videoNode = self.videoNode { switch visibility { case .visible: - if videoNode.supernode == nil { - self.insertSubnode(videoNode, aboveSubnode: self.imageNode) + if !videoNode.canAttachContent { + videoNode.canAttachContent = true + videoNode.play() } case .nearlyVisible, .none: - if videoNode.supernode != nil { - videoNode.removeFromSupernode() - } + videoNode.canAttachContent = false } } } @@ -49,6 +54,7 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { init() { self.imageNode = TransformImageNode() + self.imageNode.contentAnimations = [.subsequentUpdates] super.init(layerBacked: false) @@ -108,7 +114,7 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { } } - func asyncLayout() -> (_ account: Account, _ theme: PresentationTheme, _ strings: PresentationStrings, _ message: Message, _ media: Media, _ corners: ImageCorners, _ automaticDownload: Bool, _ constrainedSize: CGSize, _ layoutConstants: ChatMessageItemLayoutConstants) -> (CGFloat, ImageCorners, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> Void))) { + func asyncLayout() -> (_ account: Account, _ theme: PresentationTheme, _ strings: PresentationStrings, _ message: Message, _ media: Media, _ automaticDownload: Bool, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants) -> (CGSize, CGFloat, (CGSize, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ContainedViewLayoutTransition) -> Void))) { let currentMessageIdAndFlags = self.messageIdAndFlags let currentMedia = self.media let imageLayout = self.imageNode.asyncLayout() @@ -118,7 +124,7 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { let currentTheme = self.themeAndStrings?.0 - return { [weak self] account, theme, strings, message, media, corners, automaticDownload, constrainedSize, layoutConstants in + return { [weak self] account, theme, strings, message, media, automaticDownload, sizeCalculation, layoutConstants in var nativeSize: CGSize var updatedTheme: PresentationTheme? @@ -140,27 +146,35 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { } } + var webpage: TelegramMediaWebpage? + for m in message.media { + if let m = m as? TelegramMediaWebpage { + webpage = m + } + } + var isInlinePlayableVideo = false + var unboundSize: CGSize if let image = media as? TelegramMediaImage, let dimensions = largestImageRepresentation(image.representations)?.dimensions { - nativeSize = CGSize(width: floor(dimensions.width * 0.5), height: floor(dimensions.height * 0.5)).fitted(constrainedSize) + unboundSize = CGSize(width: floor(dimensions.width * 0.5), height: floor(dimensions.height * 0.5)) } else if let file = media as? TelegramMediaFile, let dimensions = file.dimensions { - nativeSize = CGSize(width: floor(dimensions.width * 0.5), height: floor(dimensions.height * 0.5)).fitted(constrainedSize) + unboundSize = CGSize(width: floor(dimensions.width * 0.5), height: floor(dimensions.height * 0.5)) if file.isAnimated { - nativeSize = nativeSize.aspectFilled(CGSize(width: 480.0, height: 480.0)) + unboundSize = unboundSize.aspectFilled(CGSize(width: 480.0, height: 480.0)) } isInlinePlayableVideo = file.isVideo && file.isAnimated } else if let image = media as? TelegramMediaWebFile, let dimensions = image.dimensions { - nativeSize = CGSize(width: floor(dimensions.width * 0.5), height: floor(dimensions.height * 0.5)).fitted(constrainedSize) + unboundSize = CGSize(width: floor(dimensions.width * 0.5), height: floor(dimensions.height * 0.5)) } else { - nativeSize = CGSize(width: 54.0, height: 54.0) + unboundSize = CGSize(width: 54.0, height: 54.0) } - var updatedCorners = corners - if isInlinePlayableVideo { - updatedCorners = updatedCorners.withRemovedTails() - let radius = max(updatedCorners.bottomLeft.radius, updatedCorners.bottomRight.radius) - updatedCorners = ImageCorners(radius: radius) + switch sizeCalculation { + case let .constrained(constrainedSize): + nativeSize = unboundSize.fitted(constrainedSize) + case .unconstrained: + nativeSize = unboundSize } let maxWidth: CGFloat @@ -175,27 +189,42 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { secretProgressIcon = PresentationResourcesChat.chatBubbleSecretMediaIcon(theme) } - return (maxWidth, updatedCorners, { constrainedSize in - let resultWidth: CGFloat + return (nativeSize, maxWidth, { constrainedSize, corners in + var resultWidth: CGFloat - if isSecretMedia { - resultWidth = maxWidth - } else { - //resultWidth = min(maxWidth, nativeSize.width) - resultWidth = min(constrainedSize.width, nativeSize.aspectFitted(layoutConstants.image.maxDimensions).width) + switch sizeCalculation { + case .constrained: + if isSecretMedia { + resultWidth = maxWidth + } else { + let maxFittedSize = nativeSize.aspectFitted (layoutConstants.image.maxDimensions) + resultWidth = min(nativeSize.width, min(maxFittedSize.width, min(constrainedSize.width, layoutConstants.image.maxDimensions.width))) + + resultWidth = max(resultWidth, layoutConstants.image.minDimensions.width) + } + case .unconstrained: + resultWidth = constrainedSize.width } return (resultWidth, { boundingWidth in + var boundingSize: CGSize let drawingSize: CGSize - let boundingSize: CGSize - if isSecretMedia { - boundingSize = CGSize(width: maxWidth, height: maxWidth) - drawingSize = nativeSize.aspectFilled(boundingSize) - } else { - let fittedSize = nativeSize.fittedToWidthOrSmaller(boundingWidth) - boundingSize = CGSize(width: boundingWidth, height: fittedSize.height).cropped(CGSize(width: CGFloat.greatestFiniteMagnitude, height: layoutConstants.image.maxDimensions.height)) - drawingSize = nativeSize.fitted(boundingSize) + switch sizeCalculation { + case .constrained: + if isSecretMedia { + boundingSize = CGSize(width: maxWidth, height: maxWidth) + drawingSize = nativeSize.aspectFilled(boundingSize) + } else { + let fittedSize = nativeSize.fittedToWidthOrSmaller(boundingWidth) + boundingSize = CGSize(width: boundingWidth, height: fittedSize.height).cropped(CGSize(width: CGFloat.greatestFiniteMagnitude, height: layoutConstants.image.maxDimensions.height)) + boundingSize.height = max(boundingSize.height, layoutConstants.image.minDimensions.height) + boundingSize.width = max(boundingSize.width, layoutConstants.image.minDimensions.width) + drawingSize = nativeSize.aspectFitted(boundingSize) + } + case .unconstrained: + boundingSize = constrainedSize + drawingSize = nativeSize.aspectFilled(boundingSize) } var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? @@ -214,7 +243,7 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { statusUpdated = true } - var updatedVideoNode: ManagedVideoNode? + var updatedVideoNode: UniversalVideoNode? var replaceVideoNode = false var updateVideoFile: TelegramMediaFile? @@ -223,7 +252,7 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { if isSecretMedia { updateImageSignal = chatSecretPhoto(account: account, photo: image) } else { - updateImageSignal = chatMessagePhoto(account: account, photo: image) + updateImageSignal = chatMessagePhoto(postbox: account.postbox, photo: image) } updatedFetchControls = FetchControls(fetch: { @@ -247,14 +276,14 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { if isSecretMedia { updateImageSignal = chatSecretMessageVideo(account: account, video: file) } else { - updateImageSignal = chatMessageVideo(account: account, video: file) + updateImageSignal = chatMessageVideo(postbox: account.postbox, video: file) } if isInlinePlayableVideo { updateVideoFile = file if hasCurrentVideoNode { } else { - let videoNode = ManagedVideoNode() + let videoNode = UniversalVideoNode(postbox: account.postbox, audioSession: account.telegramApplicationContext.mediaManager.audioSession, manager: account.telegramApplicationContext.mediaManager.universalVideoManager, decoration: ChatBubbleVideoDecoration(cornerRadius: 17.0), content: NativeVideoContent(id: .message(message.id, file.fileId), file: file, enableSound: false), priority: .embedded) videoNode.isUserInteractionEnabled = false updatedVideoNode = videoNode replaceVideoNode = true @@ -268,10 +297,18 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { let messageId = message.id updatedFetchControls = FetchControls(fetch: { if let strongSelf = self { + if file.isAnimated { + strongSelf.fetchDisposable.set(account.postbox.mediaBox.fetchedResource(file.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .image)).start()) + } else { strongSelf.fetchDisposable.set(messageMediaFileInteractiveFetched(account: account, messageId: messageId, file: file).start()) + } } }, cancel: { - messageMediaFileCancelInteractiveFetch(account: account, messageId: messageId, file: file) + if file.isAnimated { + account.postbox.mediaBox.cancelInteractiveResourceFetch(file.resource) + } else { + messageMediaFileCancelInteractiveFetch(account: account, messageId: messageId, file: file) + } }) } } @@ -310,56 +347,64 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { } } - let arguments = TransformImageArguments(corners: updatedCorners, imageSize: drawingSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets()) + let arguments = TransformImageArguments(corners: corners, imageSize: drawingSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets()) let imageFrame = CGRect(origin: CGPoint(x: -arguments.insets.left, y: -arguments.insets.top), size: arguments.drawingSize) let imageApply = imageLayout(arguments) - return (boundingSize, { + let radialStatusSize: CGFloat + if case .unconstrained = sizeCalculation { + radialStatusSize = 32.0 + } else { + radialStatusSize = 50.0 + } + + return (boundingSize, { transition in if let strongSelf = self { strongSelf.account = account strongSelf.messageIdAndFlags = (message.id, message.flags) strongSelf.media = media strongSelf.themeAndStrings = (theme, strings) - strongSelf.imageNode.frame = imageFrame + transition.updateFrame(node: strongSelf.imageNode, frame: imageFrame) strongSelf.statusNode?.position = CGPoint(x: imageFrame.midX, y: imageFrame.midY) strongSelf.timeoutNode?.position = CGPoint(x: imageFrame.midX, y: imageFrame.midY) if replaceVideoNode { if let videoNode = strongSelf.videoNode { - videoNode.clearContext() + videoNode.canAttachContent = false videoNode.removeFromSupernode() strongSelf.videoNode = nil } if let updatedVideoNode = updatedVideoNode { strongSelf.videoNode = updatedVideoNode - if strongSelf.visibility == .visible { - strongSelf.insertSubnode(updatedVideoNode, aboveSubnode: strongSelf.imageNode) - } + strongSelf.insertSubnode(updatedVideoNode, aboveSubnode: strongSelf.imageNode) } } if let videoNode = strongSelf.videoNode { - if let updateVideoFile = updateVideoFile { - if let applicationContext = account.applicationContext as? TelegramApplicationContext { - videoNode.acquireContext(account: account, mediaManager: applicationContext.mediaManager, id: PeerMessageManagedMediaId(messageId: message.id), resource: updateVideoFile.resource, priority: 1) - } - } - - videoNode.transformArguments = arguments + videoNode.updateLayout(size: arguments.drawingSize, transition: .immediate) videoNode.frame = imageFrame + + if strongSelf.visibility == .visible { + if !videoNode.canAttachContent { + videoNode.canAttachContent = true + videoNode.play() + } + } else { + videoNode.canAttachContent = false + } } if let updateImageSignal = updateImageSignal { - strongSelf.imageNode.setSignal(account: account, signal: updateImageSignal) + strongSelf.imageNode.setSignal(updateImageSignal) } if let secretBeginTimeAndTimeout = secretBeginTimeAndTimeout { if strongSelf.timeoutNode == nil { let timeoutNode = RadialTimeoutNode(backgroundColor: theme.chat.bubble.mediaOverlayControlBackgroundColor, foregroundColor: theme.chat.bubble.mediaOverlayControlForegroundColor) - timeoutNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 50.0, height: 50.0)) + timeoutNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: radialStatusSize, height: radialStatusSize)) timeoutNode.position = strongSelf.imageNode.position strongSelf.timeoutNode = timeoutNode strongSelf.addSubnode(timeoutNode) @@ -392,6 +437,8 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { progressRequired = true } else if isSecretMedia { progressRequired = true + } else if let webpage = webpage, case let .Loaded(content) = webpage.content, content.embedUrl != nil { + progressRequired = true } } else { progressRequired = true @@ -401,13 +448,10 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { if progressRequired { if strongSelf.statusNode == nil { let statusNode = RadialStatusNode(backgroundNodeColor: theme.chat.bubble.mediaOverlayControlBackgroundColor) - statusNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 50.0, height: 50.0)) + statusNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: radialStatusSize, height: radialStatusSize)) statusNode.position = strongSelf.imageNode.position strongSelf.statusNode = statusNode strongSelf.addSubnode(statusNode) - } else if let _ = updatedTheme { - - //strongSelf.progressNode?.updateTheme(RadialProgressTheme(backgroundColor: theme.chat.bubble.mediaOverlayControlBackgroundColor, foregroundColor: theme.chat.bubble.mediaOverlayControlForegroundColor, icon: nil)) } } else { if let statusNode = strongSelf.statusNode { @@ -419,6 +463,7 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { } var state: RadialStatusNodeState + var badgeContent: ChatMessageInteractiveMediaBadgeContent? let bubbleTheme = theme.chat.bubble switch status { case let .Fetching(isActive, progress): @@ -426,7 +471,20 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { if isActive { adjustedProgress = max(adjustedProgress, 0.027) } - state = .progress(color: bubbleTheme.mediaOverlayControlForegroundColor, value: CGFloat(adjustedProgress), cancelEnabled: true) + if let (_, flags) = strongSelf.messageIdAndFlags, flags.isSending && adjustedProgress.isEqual(to: 1.0), case .unconstrained = sizeCalculation { + state = .check(bubbleTheme.mediaOverlayControlForegroundColor) + } else { + state = .progress(color: bubbleTheme.mediaOverlayControlForegroundColor, value: CGFloat(adjustedProgress), cancelEnabled: true) + } + if case .constrained = sizeCalculation { + if let file = media as? TelegramMediaFile, !file.isAnimated { + if let size = file.size { + badgeContent = .text(backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, shape: .round, text: "\(dataSizeString(Int(Float(size) * progress))) / \(dataSizeString(size))") + } else if let _ = file.duration { + badgeContent = .text(backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, shape: .round, text: strings.Conversation_Processing) + } + } + } case .Local: state = .none if isSecretMedia && secretProgressIcon != nil { @@ -437,9 +495,23 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { } else { state = .none } + } else if let webpage = webpage, case let .Loaded(content) = webpage.content, content.embedUrl != nil { + state = .play(bubbleTheme.mediaOverlayControlForegroundColor) + } + if case .constrained = sizeCalculation { + if let file = media as? TelegramMediaFile, let duration = file.duration, !file.isAnimated { + let durationString = String(format: "%d:%02d", duration / 60, duration % 60) + badgeContent = .text(backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, shape: .round, text: durationString) + } } case .Remote: state = .download(bubbleTheme.mediaOverlayControlForegroundColor) + if case .constrained = sizeCalculation { + if let file = media as? TelegramMediaFile, let duration = file.duration, !file.isAnimated { + let durationString = String(format: "%d:%02d", duration / 60, duration % 60) + badgeContent = .text(backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, shape: .round, text: durationString) + } + } } if let statusNode = strongSelf.statusNode { if state == .none { @@ -451,6 +523,18 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { } }) } + if let badgeContent = badgeContent { + if strongSelf.badgeNode == nil { + let badgeNode = ChatMessageInteractiveMediaBadge() + badgeNode.frame = CGRect(origin: CGPoint(x: 6.0, y: 6.0), size: CGSize(width: radialStatusSize, height: radialStatusSize)) + strongSelf.badgeNode = badgeNode + strongSelf.addSubnode(badgeNode) + } + strongSelf.badgeNode?.content = badgeContent + } else if let badgeNode = strongSelf.badgeNode { + strongSelf.badgeNode = nil + badgeNode.removeFromSupernode() + } } } })) @@ -475,12 +559,12 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { } } - static func asyncLayout(_ node: ChatMessageInteractiveMediaNode?) -> (_ account: Account, _ theme: PresentationTheme, _ strings: PresentationStrings, _ message: Message, _ media: Media, _ corners: ImageCorners, _ automaticDownload: Bool, _ constrainedSize: CGSize, _ layoutConstants: ChatMessageItemLayoutConstants) -> (CGFloat, ImageCorners, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> ChatMessageInteractiveMediaNode))) { + static func asyncLayout(_ node: ChatMessageInteractiveMediaNode?) -> (_ account: Account, _ theme: PresentationTheme, _ strings: PresentationStrings, _ message: Message, _ media: Media, _ automaticDownload: Bool, _ sizeCalcilation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants) -> (CGSize, CGFloat, (CGSize, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ContainedViewLayoutTransition) -> ChatMessageInteractiveMediaNode))) { let currentAsyncLayout = node?.asyncLayout() - return { account, theme, strings, message, media, corners, automaticDownload, constrainedSize, layoutConstants in + return { account, theme, strings, message, media, automaticDownload, sizeCalculation, layoutConstants in var imageNode: ChatMessageInteractiveMediaNode - var imageLayout: (_ account: Account, _ theme: PresentationTheme, _ strings: PresentationStrings, _ message: Message, _ media: Media, _ corners: ImageCorners, _ automaticDownload: Bool, _ constrainedSize: CGSize, _ layoutConstants: ChatMessageItemLayoutConstants) -> (CGFloat, ImageCorners, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> Void))) + var imageLayout: (_ account: Account, _ theme: PresentationTheme, _ strings: PresentationStrings, _ message: Message, _ media: Media, _ automaticDownload: Bool, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants) -> (CGSize, CGFloat, (CGSize, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ContainedViewLayoutTransition) -> Void))) if let node = node, let currentAsyncLayout = currentAsyncLayout { imageNode = node @@ -490,16 +574,16 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { imageLayout = imageNode.asyncLayout() } - let (initialWidth, corners, continueLayout) = imageLayout(account, theme, strings, message, media, corners, automaticDownload, constrainedSize, layoutConstants) + let (unboundSize, initialWidth, continueLayout) = imageLayout(account, theme, strings, message, media, automaticDownload, sizeCalculation, layoutConstants) - return (initialWidth, corners, { constrainedSize in - let (finalWidth, finalLayout) = continueLayout(constrainedSize) + return (unboundSize, initialWidth, { constrainedSize, corners in + let (finalWidth, finalLayout) = continueLayout(constrainedSize, corners) return (finalWidth, { boundingWidth in let (finalSize, apply) = finalLayout(boundingWidth) - return (finalSize, { - apply() + return (finalSize, { transition in + apply(transition) return imageNode }) }) diff --git a/TelegramUI/ChatMessageInvoiceBubbleContentNode.swift b/TelegramUI/ChatMessageInvoiceBubbleContentNode.swift index 0b8f648933..8b4577117b 100644 --- a/TelegramUI/ChatMessageInvoiceBubbleContentNode.swift +++ b/TelegramUI/ChatMessageInvoiceBubbleContentNode.swift @@ -6,15 +6,10 @@ import SwiftSignalKit import TelegramCore final class ChatMessageInvoiceBubbleContentNode: ChatMessageBubbleContentNode { - private var item: ChatMessageItem? private var invoice: TelegramMediaInvoice? private let contentNode: ChatMessageAttachedContentNode - override var properties: ChatMessageBubbleContentProperties { - return ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0) - } - override var visibility: ListViewItemNodeVisibility { didSet { self.contentNode.visibility = self.visibility @@ -33,10 +28,10 @@ final class ChatMessageInvoiceBubbleContentNode: ChatMessageBubbleContentNode { fatalError("init(coder:) has not been implemented") } - override func asyncLayoutContent() -> (_ item: ChatMessageItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ position: ChatMessageBubbleContentPosition, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))) { + override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))) { let contentNodeLayout = self.contentNode.asyncLayout() - return { item, layoutConstants, position, constrainedSize in + return { item, layoutConstants, _, _, constrainedSize in var invoice: TelegramMediaInvoice? for media in item.message.media { if let media = media as? TelegramMediaInvoice { @@ -59,16 +54,19 @@ final class ChatMessageInvoiceBubbleContentNode: ChatMessageBubbleContentNode { } } - let (initialWidth, continueLayout) = contentNodeLayout(item.theme, item.strings, item.controllerInteraction.automaticMediaDownloadSettings, item.account, item.message, item.read, title, subtitle, text, nil, mediaAndFlags, nil, nil, false, layoutConstants, position, constrainedSize) + let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.account, item.message, item.read, title, subtitle, text, nil, mediaAndFlags, nil, nil, false, layoutConstants, constrainedSize) - return (initialWidth, { constrainedSize in - let (refinedWidth, finalizeLayout) = continueLayout(constrainedSize) + let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackgroundForEmptyWallpapers: false, forceFullCorners: false) + + return (contentProperties, nil, initialWidth, { constrainedSize, position in + let (refinedWidth, finalizeLayout) = continueLayout(constrainedSize, position) return (refinedWidth, { boundingWidth in let (size, apply) = finalizeLayout(boundingWidth) return (size, { [weak self] animation in if let strongSelf = self { + strongSelf.item = item strongSelf.invoice = invoice apply(animation) @@ -112,7 +110,10 @@ final class ChatMessageInvoiceBubbleContentNode: ChatMessageBubbleContentNode { self.contentNode.updateHiddenMedia(media) } - override func transitionNode(media: Media) -> ASDisplayNode? { + override func transitionNode(messageId: MessageId, media: Media) -> ASDisplayNode? { + if self.item?.message.id != messageId { + return nil + } return self.contentNode.transitionNode(media: media) } } diff --git a/TelegramUI/ChatMessageItem.swift b/TelegramUI/ChatMessageItem.swift index e6dc20c6da..8565f7307c 100644 --- a/TelegramUI/ChatMessageItem.swift +++ b/TelegramUI/ChatMessageItem.swift @@ -6,6 +6,62 @@ import Display import SwiftSignalKit import TelegramCore +public enum ChatMessageItemContent: Sequence { + case message(message: Message, read: Bool, selection: ChatHistoryMessageSelection) + case group(messages: [(Message, Bool, ChatHistoryMessageSelection)]) + + func effectivelyIncoming(_ accountPeerId: PeerId) -> Bool { + switch self { + case let .message(message, _, _): + return message.effectivelyIncoming(accountPeerId) + case let .group(messages): + return messages[0].0.effectivelyIncoming(accountPeerId) + } + } + + var index: MessageIndex { + switch self { + case let .message(message, _, _): + return MessageIndex(message) + case let .group(messages): + return MessageIndex(messages[0].0) + } + } + + var firstMessage: Message { + switch self { + case let .message(message, _, _): + return message + case let .group(messages): + return messages[0].0 + } + } + + public func makeIterator() -> AnyIterator { + var index = 0 + return AnyIterator { () -> Message? in + switch self { + case let .message(message, _, _): + if index == 0 { + index += 1 + return message + } else { + index += 1 + return nil + } + case let .group(messages): + if index < messages.count { + let currentIndex = index + index += 1 + return messages[currentIndex].0 + } else { + return nil + } + } + } + } +} + private func mediaIsNotMergeable(_ media: Media) -> Bool { if let file = media as? TelegramMediaFile, file.isSticker { return true @@ -20,8 +76,21 @@ private func mediaIsNotMergeable(_ media: Media) -> Bool { return false } -private func messagesShouldBeMerged(_ lhs: Message, _ rhs: Message) -> Bool { - if abs(lhs.timestamp - rhs.timestamp) < Int32(5 * 60) && lhs.author?.id == rhs.author?.id { +private func messagesShouldBeMerged(accountPeerId: PeerId, _ lhs: Message, _ rhs: Message) -> Bool { + var lhsEffectiveAuthor: Peer? = lhs.author + var rhsEffectiveAuthor: Peer? = rhs.author + if lhs.id.peerId == accountPeerId { + if let forwardInfo = lhs.forwardInfo { + lhsEffectiveAuthor = forwardInfo.author + } + } + if rhs.id.peerId == accountPeerId { + if let forwardInfo = rhs.forwardInfo { + rhsEffectiveAuthor = forwardInfo.author + } + } + + if abs(lhs.timestamp - rhs.timestamp) < Int32(5 * 60) && lhsEffectiveAuthor?.id == rhsEffectiveAuthor?.id { for media in lhs.media { if mediaIsNotMergeable(media) { return false @@ -52,8 +121,9 @@ func chatItemsHaveCommonDateHeader(_ lhs: ListViewItem, _ rhs: ListViewItem?) - let rhsHeader: ChatMessageDateHeader? if let lhs = lhs as? ChatMessageItem { lhsHeader = lhs.header - } else if let lhs = lhs as? ChatHoleItem { - lhsHeader = lhs.header + } else if let _ = lhs as? ChatHoleItem { + //lhsHeader = lhs.header + lhsHeader = nil } else if let lhs = lhs as? ChatUnreadItem { lhsHeader = lhs.header } else { @@ -62,8 +132,9 @@ func chatItemsHaveCommonDateHeader(_ lhs: ListViewItem, _ rhs: ListViewItem?) - if let rhs = rhs { if let rhs = rhs as? ChatMessageItem { rhsHeader = rhs.header - } else if let rhs = rhs as? ChatHoleItem { - rhsHeader = rhs.header + } else if let _ = rhs as? ChatHoleItem { + //rhsHeader = rhs.header + rhsHeader = nil } else if let rhs = rhs as? ChatUnreadItem { rhsHeader = rhs.header } else { @@ -80,33 +151,71 @@ func chatItemsHaveCommonDateHeader(_ lhs: ListViewItem, _ rhs: ListViewItem?) - } public final class ChatMessageItem: ListViewItem, CustomStringConvertible { - let theme: PresentationTheme - let strings: PresentationStrings + let presentationData: ChatPresentationData let account: Account - let peerId: PeerId + let chatLocation: ChatLocation let controllerInteraction: ChatControllerInteraction - let message: Message - let read: Bool + let content: ChatMessageItemContent + let disableDate: Bool + let effectiveAuthorId: PeerId? public let accessoryItem: ListViewAccessoryItem? let header: ChatMessageDateHeader - public init(theme: PresentationTheme, strings: PresentationStrings, account: Account, peerId: PeerId, controllerInteraction: ChatControllerInteraction, message: Message, read: Bool) { - self.theme = theme - self.strings = strings + var message: Message { + switch self.content { + case let .message(message, _, _): + return message + case let .group(messages): + return messages[0].0 + } + } + + var read: Bool { + switch self.content { + case let .message(_, read, _): + return read + case let .group(messages): + return messages[0].1 + } + } + + public init(presentationData: ChatPresentationData, account: Account, chatLocation: ChatLocation, controllerInteraction: ChatControllerInteraction, content: ChatMessageItemContent, disableDate: Bool = false) { + self.presentationData = presentationData self.account = account - self.peerId = peerId + self.chatLocation = chatLocation self.controllerInteraction = controllerInteraction - self.message = message - self.read = read + self.content = content + self.disableDate = disableDate var accessoryItem: ListViewAccessoryItem? - let incoming = message.effectivelyIncoming - let displayAuthorInfo = incoming && message.author != nil && peerId.isGroupOrChannel + let incoming = content.effectivelyIncoming(self.account.peerId) - self.header = ChatMessageDateHeader(timestamp: message.timestamp, theme: theme, strings: strings) + var effectiveAuthor: Peer? + let displayAuthorInfo: Bool + + switch chatLocation { + case let .peer(peerId): + if peerId == account.peerId { + if let forwardInfo = content.firstMessage.forwardInfo { + effectiveAuthor = forwardInfo.author + } + displayAuthorInfo = incoming && effectiveAuthor != nil + } else { + effectiveAuthor = content.firstMessage.author + displayAuthorInfo = incoming && peerId.isGroupOrChannel && effectiveAuthor != nil + } + case .group: + effectiveAuthor = content.firstMessage.author + displayAuthorInfo = incoming && effectiveAuthor != nil + } + + self.effectiveAuthorId = effectiveAuthor?.id + + self.header = ChatMessageDateHeader(timestamp: content.index.timestamp, theme: presentationData.theme, strings: presentationData.strings) if displayAuthorInfo { + let message = content.firstMessage var hasActionMedia = false for media in message.media { if media is TelegramMediaAction { @@ -115,19 +224,21 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible { } } var isBroadcastChannel = false - if let peer = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { - isBroadcastChannel = true + if case .peer = chatLocation { + if let peer = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { + isBroadcastChannel = true + } } if !hasActionMedia && !isBroadcastChannel { - if let author = message.author { - accessoryItem = ChatMessageAvatarAccessoryItem(account: account, peerId: author.id, peer: author, messageTimestamp: message.timestamp) + if let effectiveAuthor = effectiveAuthor { + accessoryItem = ChatMessageAvatarAccessoryItem(account: account, peerId: effectiveAuthor.id, peer: effectiveAuthor, messageTimestamp: content.index.timestamp) } } } self.accessoryItem = accessoryItem } - public func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { var viewClassName: AnyClass = ChatMessageBubbleItemNode.self loop: for media in message.media { @@ -159,12 +270,11 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible { let configure = { let node = (viewClassName as! ChatMessageItemView.Type).init() - node.controllerInteraction = self.controllerInteraction node.setupItem(self) let nodeLayout = node.asyncLayout() let (top, bottom, dateAtBottom) = self.mergedWithItems(top: previousItem, bottom: nextItem) - let (layout, apply) = nodeLayout(self, width, top, bottom, dateAtBottom) + let (layout, apply) = nodeLayout(self, params, top, bottom, dateAtBottom && !self.disableDate) node.updateSelectionState(animated: false) node.updateHighlightedState(animated: false) @@ -193,7 +303,7 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible { if top.header.id != self.header.id { mergedBottom = false } else { - mergedBottom = messagesShouldBeMerged(message, top.message) + mergedBottom = messagesShouldBeMerged(accountPeerId: self.account.peerId, message, top.message) } } if let bottom = bottom as? ChatMessageItem { @@ -201,16 +311,16 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible { mergedTop = false dateAtBottom = true } else { - mergedTop = messagesShouldBeMerged(bottom.message, message) + mergedTop = messagesShouldBeMerged(accountPeerId: self.account.peerId, bottom.message, message) } } else if let bottom = bottom as? ChatUnreadItem { if bottom.header.id != self.header.id { dateAtBottom = true } } else if let bottom = bottom as? ChatHoleItem { - if bottom.header.id != self.header.id { + //if bottom.header.id != self.header.id { dateAtBottom = true - } + //} } else { dateAtBottom = true } @@ -218,7 +328,7 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible { return (mergedTop, mergedBottom, dateAtBottom) } - public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { if let node = node as? ChatMessageItemView { Queue.mainQueue().async { node.setupItem(self) @@ -228,7 +338,7 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible { async { let (top, bottom, dateAtBottom) = self.mergedWithItems(top: previousItem, bottom: nextItem) - let (layout, apply) = nodeLayout(self, width, top, bottom, dateAtBottom) + let (layout, apply) = nodeLayout(self, params, top, bottom, dateAtBottom && !self.disableDate) Queue.mainQueue().async { completion(layout, { apply(animation) diff --git a/TelegramUI/ChatMessageItemContent.swift b/TelegramUI/ChatMessageItemContent.swift deleted file mode 100644 index 0ac184db5b..0000000000 --- a/TelegramUI/ChatMessageItemContent.swift +++ /dev/null @@ -1,16 +0,0 @@ -import Foundation -import Display - -class ChatMessageItemContent { - func attach(node: ASDisplayNode) { - preconditionFailure() - } - - func detach() { - preconditionFailure() - } - - func asyncLayout() -> (_ item: ChatMessageItem, _ width: CGFloat, _ mergedTop: Bool, _ mergedBottom: Bool, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { - preconditionFailure() - } -} diff --git a/TelegramUI/ChatMessageItemView.swift b/TelegramUI/ChatMessageItemView.swift index aa18261113..942a6ee330 100644 --- a/TelegramUI/ChatMessageItemView.swift +++ b/TelegramUI/ChatMessageItemView.swift @@ -3,11 +3,25 @@ import AsyncDisplayKit import Display import Postbox +struct ChatMessageItemWidthFill { + let compactInset: CGFloat + let compactWidthBoundary: CGFloat + let freeMaximumFillFactor: CGFloat + + func widthFor(_ width: CGFloat) -> CGFloat { + if width <= self.compactWidthBoundary { + return max(1.0, width - self.compactInset) + } else { + return max(1.0, floor(width * self.freeMaximumFillFactor)) + } + } +} + struct ChatMessageItemBubbleLayoutConstants { let edgeInset: CGFloat let defaultSpacing: CGFloat let mergedSpacing: CGFloat - let maximumWidthFillFactor: CGFloat + let maximumWidthFill: ChatMessageItemWidthFill let minimumSize: CGSize let contentInsets: UIEdgeInsets } @@ -23,6 +37,7 @@ struct ChatMessageItemImageLayoutConstants { let mergedCornerRadius: CGFloat let contentMergedCornerRadius: CGFloat let maxDimensions: CGSize + let minDimensions: CGSize } struct ChatMessageItemInstantVideoConstants { @@ -48,9 +63,9 @@ struct ChatMessageItemLayoutConstants { self.avatarDiameter = 37.0 self.timestampHeaderHeight = 34.0 - self.bubble = ChatMessageItemBubbleLayoutConstants(edgeInset: 4.0, defaultSpacing: 2.0 + UIScreenPixel, mergedSpacing: 1.0, maximumWidthFillFactor: 0.85, minimumSize: CGSize(width: 40.0, height: 35.0), contentInsets: UIEdgeInsets(top: 1.0, left: 7.0, bottom: 1.0, right: 1.0)) + self.bubble = ChatMessageItemBubbleLayoutConstants(edgeInset: 4.0, defaultSpacing: 2.0 + UIScreenPixel, mergedSpacing: 1.0, maximumWidthFill: ChatMessageItemWidthFill(compactInset: 40.0, compactWidthBoundary: 500.0, freeMaximumFillFactor: 0.85), minimumSize: CGSize(width: 40.0, height: 35.0), contentInsets: UIEdgeInsets(top: 0.0, left: 6.0, bottom: 0.0, right: 0.0)) self.text = ChatMessageItemTextLayoutConstants(bubbleInsets: UIEdgeInsets(top: 6.0 + UIScreenPixel, left: 12.0, bottom: 6.0 - UIScreenPixel, right: 12.0)) - self.image = ChatMessageItemImageLayoutConstants(bubbleInsets: UIEdgeInsets(top: 0.5, left: 0.5, bottom: 0.5, right: 0.5), statusInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 6.0, right: 6.0), defaultCornerRadius: 17.0, mergedCornerRadius: 5.0, contentMergedCornerRadius: 5.0, maxDimensions: CGSize(width: 300.0, height: 300.0)) + self.image = ChatMessageItemImageLayoutConstants(bubbleInsets: UIEdgeInsets(top: 1.5, left: 1.5, bottom: 1.5, right: 1.5), statusInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 6.0, right: 6.0), defaultCornerRadius: 17.0, mergedCornerRadius: 5.0, contentMergedCornerRadius: 5.0, maxDimensions: CGSize(width: 300.0, height: 300.0), minDimensions: CGSize(width: 64.0, height: 64.0)) self.file = ChatMessageItemFileLayoutConstants(bubbleInsets: UIEdgeInsets(top: 15.0, left: 9.0, bottom: 15.0, right: 12.0)) self.instantVideo = ChatMessageItemInstantVideoConstants(insets: UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0), dimensions: CGSize(width: 212.0, height: 212.0)) } @@ -62,9 +77,6 @@ public class ChatMessageItemView: ListViewItemNode { let layoutConstants = defaultChatMessageItemLayoutConstants var item: ChatMessageItem? - var controllerInteraction: ChatControllerInteraction? - - private var content: ChatMessageItemContent? public required convenience init() { self.init(layerBacked: true) @@ -90,20 +102,20 @@ public class ChatMessageItemView: ListViewItemNode { self.item = item } - override public func layoutForWidth(_ width: CGFloat, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { + override public func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { if let item = item as? ChatMessageItem { let doLayout = self.asyncLayout() let merged = item.mergedWithItems(top: previousItem, bottom: nextItem) - let (layout, apply) = doLayout(item, width, merged.top, merged.bottom, merged.dateAtBottom) + let (layout, apply) = doLayout(item, params, merged.top, merged.bottom, merged.dateAtBottom) self.contentSize = layout.contentSize self.insets = layout.insets apply(.None) } } - override public func layoutAccessoryItemNode(_ accessoryItemNode: ListViewAccessoryItemNode) { + override public func layoutAccessoryItemNode(_ accessoryItemNode: ListViewAccessoryItemNode, leftInset: CGFloat, rightInset: CGFloat) { if let avatarNode = accessoryItemNode as? ChatMessageAvatarAccessoryItemNode { - avatarNode.frame = CGRect(origin: CGPoint(x: 3.0, y: self.bounds.height - 38.0 - self.insets.top + 1.0), size: CGSize(width: 38.0, height: 38.0)) + avatarNode.frame = CGRect(origin: CGPoint(x: leftInset + 3.0, y: self.apparentFrame.height - 38.0 - self.insets.top + 1.0), size: CGSize(width: 38.0, height: 38.0)) } } @@ -116,7 +128,7 @@ public class ChatMessageItemView: ListViewItemNode { } } - func asyncLayout() -> (_ item: ChatMessageItem, _ width: CGFloat, _ mergedTop: Bool, _ mergedBottom: Bool, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { + func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { return { _, _, _, _, _ in return (ListViewItemNodeLayout(contentSize: CGSize(width: 32.0, height: 32.0), insets: UIEdgeInsets()), { _ in diff --git a/TelegramUI/ChatMessageLiveLocationPositionNode.swift b/TelegramUI/ChatMessageLiveLocationPositionNode.swift new file mode 100644 index 0000000000..9943703251 --- /dev/null +++ b/TelegramUI/ChatMessageLiveLocationPositionNode.swift @@ -0,0 +1,137 @@ +import Foundation +import Display +import TelegramCore +import Postbox + +private let avatarFont: UIFont = UIFont(name: "ArialRoundedMTBold", size: 24.0)! +private let avatarBackgroundImage = UIImage(bundleImageName: "Chat/Message/LocationPin")?.precomposed() + +private func addPulseAnimations(layer: CALayer) { + let scaleAnimation = CAKeyframeAnimation(keyPath: "transform.scale") + scaleAnimation.values = [0.0 as NSNumber, 0.72 as NSNumber, 1.0 as NSNumber, 1.0 as NSNumber] + scaleAnimation.keyTimes = [0.0 as NSNumber, 0.49 as NSNumber, 0.88 as NSNumber, 1.0 as NSNumber] + scaleAnimation.duration = 3.0 + scaleAnimation.repeatCount = Float.infinity + scaleAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut) + layer.add(scaleAnimation, forKey: "pulse-scale") + + let opacityAnimation = CAKeyframeAnimation(keyPath: "opacity") + opacityAnimation.values = [1.0 as NSNumber, 0.2 as NSNumber, 0.0 as NSNumber, 0.0 as NSNumber] + opacityAnimation.keyTimes = [0.0 as NSNumber, 0.4 as NSNumber, 0.62 as NSNumber, 1.0 as NSNumber] + opacityAnimation.duration = 3.0 + opacityAnimation.repeatCount = Float.infinity + layer.add(opacityAnimation, forKey: "pulse-opacity") +} + +private func removePulseAnimations(layer: CALayer) { + layer.removeAnimation(forKey: "pulse-scale") + layer.removeAnimation(forKey: "pulse-opacity") +} + +final class ChatMessageLiveLocationPositionNode: ASDisplayNode { + private let backgroundNode: ASImageNode + private let avatarNode: AvatarNode + private let pulseNode: ASImageNode + + private var pulseImage: UIImage? + + override init() { + self.backgroundNode = ASImageNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.displaysAsynchronously = false + self.backgroundNode.displayWithoutProcessing = true + + self.avatarNode = AvatarNode(font: avatarFont) + self.avatarNode.isLayerBacked = true + + self.pulseNode = ASImageNode() + self.pulseNode.isLayerBacked = true + self.pulseNode.displaysAsynchronously = false + self.pulseNode.displayWithoutProcessing = true + self.pulseNode.isHidden = true + + super.init() + + self.isLayerBacked = true + + self.addSubnode(self.pulseNode) + self.addSubnode(self.backgroundNode) + self.addSubnode(self.avatarNode) + } + + func asyncLayout() -> (_ account: Account, _ theme: PresentationTheme, _ peer: Peer?, _ liveActive: Bool?) -> (CGSize, () -> Void) { + let currentPulseImage = self.pulseImage + + return { [weak self] account, theme, peer, liveActive in + let backgroundImage: UIImage? + var hasPulse = false + if let peer = peer { + backgroundImage = avatarBackgroundImage + + if let liveActive = liveActive { + hasPulse = liveActive + } + } else { + backgroundImage = PresentationResourcesChat.chatBubbleMapPinImage(theme) + } + + let pulseImage: UIImage? + if hasPulse { + pulseImage = currentPulseImage ?? generateFilledCircleImage(diameter: 120.0, color: UIColor(rgb: 0x007aff, alpha: 0.27)) + } else { + pulseImage = nil + } + + return (CGSize(width: 62.0, height: 74.0), { + if let strongSelf = self { + if strongSelf.backgroundNode.image !== backgroundImage { + strongSelf.backgroundNode.image = backgroundImage + } + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 62.0, height: 74.0)) + strongSelf.avatarNode.frame = CGRect(origin: CGPoint(x: 10.0, y: 9.0), size: CGSize(width: 42.0, height: 42.0)) + if let peer = peer { + strongSelf.avatarNode.setPeer(account: account, peer: peer) + strongSelf.avatarNode.isHidden = false + + if let liveActive = liveActive { + strongSelf.avatarNode.alpha = liveActive ? 1.0 : 0.6 + } else { + strongSelf.avatarNode.alpha = 1.0 + } + } else { + strongSelf.avatarNode.isHidden = true + } + + strongSelf.pulseImage = pulseImage + strongSelf.pulseNode.image = pulseImage + strongSelf.pulseNode.frame = CGRect(origin: CGPoint(x: floor((62.0 - 60.0) / 2.0), y: 34.0), size: CGSize(width: 60.0, height: 60.0)) + if hasPulse { + if strongSelf.pulseNode.isHidden { + strongSelf.pulseNode.isHidden = false + if strongSelf.isInHierarchy { + addPulseAnimations(layer: strongSelf.pulseNode.layer) + } + } + } else if !strongSelf.pulseNode.isHidden { + strongSelf.pulseNode.isHidden = true + removePulseAnimations(layer: strongSelf.pulseNode.layer) + } + } + }) + } + } + + override func willEnterHierarchy() { + super.willEnterHierarchy() + if !self.pulseNode.isHidden { + addPulseAnimations(layer: self.pulseNode.layer) + } + } + + override func didExitHierarchy() { + super.didExitHierarchy() + if !self.pulseNode.isHidden { + removePulseAnimations(layer: self.pulseNode.layer) + } + } +} diff --git a/TelegramUI/ChatMessageLiveLocationTextNode.swift b/TelegramUI/ChatMessageLiveLocationTextNode.swift new file mode 100644 index 0000000000..79636edb26 --- /dev/null +++ b/TelegramUI/ChatMessageLiveLocationTextNode.swift @@ -0,0 +1,86 @@ +import Foundation +import AsyncDisplayKit +import Display + +private let textFont: UIFont = Font.regular(14.0) + +private class ChatMessageLiveLocationTextNodeParams: NSObject { + let color: UIColor + let string: String + + init(color: UIColor, string: String) { + self.color = color + self.string = string + + super.init() + } +} + +private final class RadialTimeoutNodeTimer: NSObject { + let action: () -> Void + init(_ action: @escaping () -> Void) { + self.action = action + + super.init() + } + + @objc func event() { + self.action() + } +} + +final class ChatMessageLiveLocationTextNode: ASDisplayNode { + private var timeoutAndColors: (UIColor, Double, PresentationStrings, PresentationTimeFormat)? + private var updateTimer: Timer? + + override init() { + super.init() + + self.isOpaque = false + } + + deinit { + self.updateTimer?.invalidate() + } + + public func update(color: UIColor, timestamp: Double, strings: PresentationStrings, timeFormat: PresentationTimeFormat) { + if self.timeoutAndColors?.1 != timestamp { + self.updateTimer?.invalidate() + self.timeoutAndColors = (color, timestamp, strings, timeFormat) + + let updateTimer = Timer(timeInterval: 30.0, target: RadialTimeoutNodeTimer({ [weak self] in + self?.setNeedsDisplay() + }), selector: #selector(RadialTimeoutNodeTimer.event), userInfo: nil, repeats: true) + self.updateTimer = updateTimer + RunLoop.main.add(updateTimer, forMode: RunLoopMode.commonModes) + } + } + + public override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { + if let (color, updateTimestamp, strings, timeFormat) = self.timeoutAndColors { + let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 + + let string = stringForRelativeLiveLocationTimestamp(strings: strings, relativeTimestamp: Int32(updateTimestamp), relativeTo: Int32(timestamp), timeFormat: timeFormat) + + return ChatMessageLiveLocationTextNodeParams(color: color, string: string) + } else { + return nil + } + } + + @objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) { + let context = UIGraphicsGetCurrentContext()! + + if !isRasterizing { + context.setBlendMode(.copy) + context.setFillColor(UIColor.clear.cgColor) + context.fill(bounds) + } + + if let parameters = parameters as? ChatMessageLiveLocationTextNodeParams { + let attributes: [NSAttributedStringKey: Any] = [.font: textFont, .foregroundColor: parameters.color] + let nsString = parameters.string as NSString + nsString.draw(at: CGPoint(x: 0.0, y: 0.0), withAttributes: attributes) + } + } +} diff --git a/TelegramUI/ChatMessageLiveLocationTimerNode.swift b/TelegramUI/ChatMessageLiveLocationTimerNode.swift new file mode 100644 index 0000000000..890b99aa3c --- /dev/null +++ b/TelegramUI/ChatMessageLiveLocationTimerNode.swift @@ -0,0 +1,127 @@ +import Foundation +import AsyncDisplayKit +import Display + +private let textFont: UIFont = UIFont(name: "ArialRoundedMTBold", size: 13.0)! + +private class ChatMessageLiveLocationTimerNodeParams: NSObject { + let backgroundColor: UIColor + let foregroundColor: UIColor + let textColor: UIColor + let value: CGFloat + let string: String + + init(backgroundColor: UIColor, foregroundColor: UIColor, textColor: UIColor, value: CGFloat, string: String) { + self.backgroundColor = backgroundColor + self.foregroundColor = foregroundColor + self.textColor = textColor + self.value = value + self.string = string + + super.init() + } +} + +private final class RadialTimeoutNodeTimer: NSObject { + let action: () -> Void + init(_ action: @escaping () -> Void) { + self.action = action + + super.init() + } + + @objc func event() { + self.action() + } +} + +final class ChatMessageLiveLocationTimerNode: ASDisplayNode { + private var timeoutAndColors: (UIColor, UIColor, UIColor, Double, Double, PresentationStrings)? + private var animationTimer: Timer? + + override init() { + super.init() + + self.isOpaque = false + } + + deinit { + self.animationTimer?.invalidate() + } + + public func update(backgroundColor: UIColor, foregroundColor: UIColor, textColor: UIColor, beginTimestamp: Double, timeout: Double, strings: PresentationStrings) { + if self.timeoutAndColors?.3 != beginTimestamp || self.timeoutAndColors?.4 != timeout { + self.animationTimer?.invalidate() + self.timeoutAndColors = (backgroundColor, foregroundColor, textColor, beginTimestamp, timeout, strings) + + let animationTimer = Timer(timeInterval: 10.0, target: RadialTimeoutNodeTimer({ [weak self] in + self?.setNeedsDisplay() + }), selector: #selector(RadialTimeoutNodeTimer.event), userInfo: nil, repeats: true) + self.animationTimer = animationTimer + RunLoop.main.add(animationTimer, forMode: RunLoopMode.commonModes) + } + } + + public override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { + var value: CGFloat = 0.0 + if let (backgroundColor, foregroundColor, textColor, beginTimestamp, timeout, strings) = self.timeoutAndColors { + let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 + let remaining = beginTimestamp + timeout - timestamp + value = CGFloat(max(0.0, 1.0 - min(1.0, remaining / timeout))) + + let intRemaining = Int32(remaining) + let string: String + if intRemaining > 60 * 60 { + let hours = Int32(round(remaining / (60.0 * 60.0))) + string = strings.Map_LiveLocationShortHour("\(hours)").0 + } else { + let minutes = Int32(round(remaining / (60.0))) + string = "\(minutes)" + } + + return ChatMessageLiveLocationTimerNodeParams(backgroundColor: backgroundColor, foregroundColor: foregroundColor, textColor: textColor, value: value, string: string) + } else { + return nil + } + } + + @objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) { + let context = UIGraphicsGetCurrentContext()! + + if !isRasterizing { + context.setBlendMode(.copy) + context.setFillColor(UIColor.clear.cgColor) + context.fill(bounds) + } + + if let parameters = parameters as? ChatMessageLiveLocationTimerNodeParams { + let lineWidth: CGFloat = 1.5 + + context.setBlendMode(.copy) + context.setFillColor(parameters.backgroundColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: bounds.size.width, height: bounds.size.height))) + context.setFillColor(UIColor.clear.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: lineWidth, y: lineWidth), size: CGSize(width: bounds.size.width - lineWidth * 2.0, height: bounds.size.height - lineWidth * 2.0))) + context.setBlendMode(.normal) + + context.setStrokeColor(parameters.foregroundColor.cgColor) + + let progress = 1.0 - parameters.value + let startAngle = -CGFloat(progress) * 2.0 * CGFloat.pi - CGFloat.pi / 2.0 + let endAngle = CGFloat(progress) * 2.0 * CGFloat.pi + startAngle + + let pathDiameter = bounds.size.width - lineWidth + + let path = UIBezierPath(arcCenter: CGPoint(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0), radius: pathDiameter / 2.0, startAngle: startAngle, endAngle: endAngle, clockwise:true) + path.lineWidth = lineWidth + path.lineCapStyle = .round + path.stroke() + + let attributes: [NSAttributedStringKey: Any] = [.font: textFont, .foregroundColor: parameters.foregroundColor] + let nsString = parameters.string as NSString + let size = nsString.size(withAttributes: attributes) + nsString.draw(at: CGPoint(x: floor((bounds.size.width - size.width) / 2.0), y: floor((bounds.size.height - size.height) / 2.0)), withAttributes: attributes) + } + } +} + diff --git a/TelegramUI/ChatMessageMapBubbleContentNode.swift b/TelegramUI/ChatMessageMapBubbleContentNode.swift index a37a81873a..12031d4db9 100644 --- a/TelegramUI/ChatMessageMapBubbleContentNode.swift +++ b/TelegramUI/ChatMessageMapBubbleContentNode.swift @@ -6,23 +6,24 @@ import Postbox import TelegramCore private let titleFont = Font.medium(14.0) +private let liveTitleFont = Font.medium(16.0) private let textFont = Font.regular(14.0) class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { - override var properties: ChatMessageBubbleContentProperties { - return ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 5.0) - } - private let imageNode: TransformImageNode + private let pinNode: ChatMessageLiveLocationPositionNode private let dateAndStatusNode: ChatMessageDateAndStatusNode private let titleNode: TextNode private let textNode: TextNode + private var liveTimerNode: ChatMessageLiveLocationTimerNode? + private var liveTextNode: ChatMessageLiveLocationTextNode? - private var item: ChatMessageItem? private var media: TelegramMediaMap? required init() { self.imageNode = TransformImageNode() + self.imageNode.contentAnimations = [.subsequentUpdates] + self.pinNode = ChatMessageLiveLocationPositionNode() self.dateAndStatusNode = ChatMessageDateAndStatusNode() self.titleNode = TextNode() self.textNode = TextNode() @@ -30,6 +31,7 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { super.init() self.addSubnode(self.imageNode) + self.addSubnode(self.pinNode) } required init?(coder aDecoder: NSCoder) { @@ -43,59 +45,121 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { self.view.addGestureRecognizer(tapRecognizer) } - override func asyncLayoutContent() -> (_ item: ChatMessageItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ position: ChatMessageBubbleContentPosition, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))) { + override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))) { let makeImageLayout = self.imageNode.asyncLayout() + let makePinLayout = self.pinNode.asyncLayout() let statusLayout = self.dateAndStatusNode.asyncLayout() let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeTextLayout = TextNode.asyncLayout(self.textNode) let previousMedia = self.media - return { item, layoutConstants, position, constrainedSize in + return { item, layoutConstants, preparePosition, _, constrainedSize in var selectedMedia: TelegramMediaMap? + var activeLiveBroadcastingTimeout: Int32? for media in item.message.media { - if let telegramImage = media as? TelegramMediaMap { - selectedMedia = telegramImage + if let telegramMap = media as? TelegramMediaMap { + selectedMedia = telegramMap + if let liveBroadcastingTimeout = telegramMap.liveBroadcastingTimeout { + let timestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + if item.message.timestamp + liveBroadcastingTimeout > timestamp { + activeLiveBroadcastingTimeout = liveBroadcastingTimeout + } + } } } - let imageCorners: ImageCorners + let bubbleInsets: UIEdgeInsets + if case .color = item.presentationData.wallpaper { + bubbleInsets = UIEdgeInsets() + } else { + bubbleInsets = layoutConstants.image.bubbleInsets + } var titleString: NSAttributedString? var textString: NSAttributedString? let imageSize: CGSize - if let venue = selectedMedia?.venue { - imageCorners = ImageCorners(radius: 14.0) - imageSize = CGSize(width: 75.0, height: 75.0) - titleString = NSAttributedString(string: venue.title, font: titleFont, textColor: item.message.effectivelyIncoming ? item.theme.chat.bubble.incomingPrimaryTextColor : item.theme.chat.bubble.outgoingPrimaryTextColor) - if let address = venue.address, !address.isEmpty { - textString = NSAttributedString(string: address, font: textFont, textColor: item.message.effectivelyIncoming ? item.theme.chat.bubble.incomingAccentColor : item.theme.chat.bubble.outgoingAccentColor) + if let selectedMedia = selectedMedia { + if activeLiveBroadcastingTimeout != nil { + let fitWidth: CGFloat = min(constrainedSize.width, layoutConstants.image.maxDimensions.width) + + imageSize = CGSize(width: fitWidth, height: floor(fitWidth * 0.5)) + + textString = NSAttributedString(string: " ", font: textFont, textColor: item.message.effectivelyIncoming(item.account.peerId) ? item.presentationData.theme.chat.bubble.incomingSecondaryTextColor : item.presentationData.theme.chat.bubble.outgoingSecondaryTextColor) + } else if let venue = selectedMedia.venue { + imageSize = CGSize(width: 75.0, height: 75.0) + titleString = NSAttributedString(string: venue.title, font: titleFont, textColor: item.message.effectivelyIncoming(item.account.peerId) ? item.presentationData.theme.chat.bubble.incomingPrimaryTextColor : item.presentationData.theme.chat.bubble.outgoingPrimaryTextColor) + if let address = venue.address, !address.isEmpty { + textString = NSAttributedString(string: address, font: textFont, textColor: item.message.effectivelyIncoming(item.account.peerId) ? item.presentationData.theme.chat.bubble.incomingSecondaryTextColor : item.presentationData.theme.chat.bubble.outgoingSecondaryTextColor) + } + } else { + let fitWidth: CGFloat = min(constrainedSize.width, layoutConstants.image.maxDimensions.width) + + imageSize = CGSize(width: fitWidth, height: floor(fitWidth * 0.5)) + } + + if selectedMedia.liveBroadcastingTimeout != nil { + titleString = NSAttributedString(string: item.presentationData.strings.Message_LiveLocation, font: liveTitleFont, textColor: item.message.effectivelyIncoming(item.account.peerId) ? item.presentationData.theme.chat.bubble.incomingPrimaryTextColor : item.presentationData.theme.chat.bubble.outgoingPrimaryTextColor) } } else { - imageCorners = chatMessageBubbleImageContentCorners(relativeContentPosition: position, normalRadius: layoutConstants.image.defaultCornerRadius, mergedRadius: layoutConstants.image.mergedCornerRadius, mergedWithAnotherContentRadius: layoutConstants.image.contentMergedCornerRadius) - imageSize = CGSize(width: 160.0, height: 100.0) + imageSize = CGSize(width: 75.0, height: 75.0) } var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? if let selectedMedia = selectedMedia, previousMedia == nil || !previousMedia!.isEqual(selectedMedia) { - updateImageSignal = chatMapSnapshotImage(account: item.account, resource: MapSnapshotMediaResource(latitude: selectedMedia.latitude, longitude: selectedMedia.longitude, width: 160, height: 100)) + var updated = true + if let previousMedia = previousMedia { + if previousMedia.latitude.isEqual(to: selectedMedia.latitude) && previousMedia.longitude.isEqual(to: selectedMedia.longitude) { + updated = false + } + } + if updated { + updateImageSignal = chatMapSnapshotImage(account: item.account, resource: MapSnapshotMediaResource(latitude: selectedMedia.latitude, longitude: selectedMedia.longitude, width: Int32(imageSize.width), height: Int32(imageSize.height))) + } } let maximumWidth: CGFloat - if let _ = titleString { + if activeLiveBroadcastingTimeout != nil { + maximumWidth = imageSize.width + bubbleInsets.left + bubbleInsets.right + } else if selectedMedia?.venue != nil { maximumWidth = CGFloat.greatestFiniteMagnitude } else { - maximumWidth = imageSize.width + layoutConstants.image.bubbleInsets.left + layoutConstants.image.bubbleInsets.right + maximumWidth = imageSize.width + bubbleInsets.left + bubbleInsets.right } - return (maximumWidth, { constrainedSize in - let (titleLayout, titleApply) = makeTitleLayout(titleString, nil, 1, .end, CGSize(width: max(1.0, constrainedSize.width - imageSize.width - layoutConstants.image.bubbleInsets.left + layoutConstants.image.bubbleInsets.right - layoutConstants.text.bubbleInsets.right), height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) - let (textLayout, textApply) = makeTextLayout(textString, nil, 2, .end, CGSize(width: max(1.0, constrainedSize.width - imageSize.width - layoutConstants.image.bubbleInsets.left + layoutConstants.image.bubbleInsets.right - layoutConstants.text.bubbleInsets.right), height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 5.0, hidesBackgroundForEmptyWallpapers: activeLiveBroadcastingTimeout == nil && selectedMedia?.venue == nil, forceFullCorners: false) + + var pinPeer: Peer? + var pinLiveLocationActive: Bool? + if let selectedMedia = selectedMedia { + if selectedMedia.liveBroadcastingTimeout != nil { + pinPeer = item.message.author + pinLiveLocationActive = activeLiveBroadcastingTimeout != nil + } + } + let (pinSize, pinApply) = makePinLayout(item.account, item.presentationData.theme, pinPeer, pinLiveLocationActive) + + return (contentProperties, nil, maximumWidth, { constrainedSize, position in + let imageCorners: ImageCorners + let maxTextWidth: CGFloat - var t = Int(item.message.timestamp) - var timeinfo = tm() - localtime_r(&t, &timeinfo) + if activeLiveBroadcastingTimeout != nil { + imageCorners = chatMessageBubbleImageContentCorners(relativeContentPosition: position, normalRadius: layoutConstants.image.defaultCornerRadius, mergedRadius: layoutConstants.image.mergedCornerRadius, mergedWithAnotherContentRadius: layoutConstants.image.contentMergedCornerRadius) + + maxTextWidth = constrainedSize.width - bubbleInsets.left + bubbleInsets.right - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right - 40.0 + } else { + maxTextWidth = constrainedSize.width - imageSize.width - bubbleInsets.left + bubbleInsets.right - layoutConstants.text.bubbleInsets.right + + if let _ = selectedMedia?.venue { + imageCorners = ImageCorners(radius: 14.0) + } else { + imageCorners = chatMessageBubbleImageContentCorners(relativeContentPosition: position, normalRadius: layoutConstants.image.defaultCornerRadius, mergedRadius: layoutConstants.image.mergedCornerRadius, mergedWithAnotherContentRadius: layoutConstants.image.contentMergedCornerRadius) + } + } + + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(1.0, maxTextWidth), height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: textString, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: max(1.0, maxTextWidth), height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) var edited = false var sentViaBot = false @@ -110,7 +174,13 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { } } - var dateText = String(format: "%02d:%02d", arguments: [Int(timeinfo.tm_hour), Int(timeinfo.tm_min)]) + if let selectedMedia = selectedMedia { + if selectedMedia.liveBroadcastingTimeout != nil { + edited = false + } + } + + var dateText = stringForMessageTimestamp(timestamp: item.message.timestamp, timeFormat: item.presentationData.timeFormat) if let author = item.message.author as? TelegramUser { if author.botInfo != nil { @@ -122,57 +192,60 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { } let statusType: ChatMessageDateAndStatusType? - if case .None = position.bottom { - if let _ = titleString { - if item.message.effectivelyIncoming { - statusType = .BubbleIncoming - } else { - if item.message.flags.contains(.Failed) { - statusType = .BubbleOutgoing(.Failed) - } else if item.message.flags.isSending { - statusType = .BubbleOutgoing(.Sending) + switch position { + case .linear(_, .None): + if selectedMedia?.venue != nil || activeLiveBroadcastingTimeout != nil { + if item.message.effectivelyIncoming(item.account.peerId) { + statusType = .BubbleIncoming } else { - statusType = .BubbleOutgoing(.Sent(read: item.read)) + if item.message.flags.contains(.Failed) { + statusType = .BubbleOutgoing(.Failed) + } else if item.message.flags.isSending { + statusType = .BubbleOutgoing(.Sending) + } else { + statusType = .BubbleOutgoing(.Sent(read: item.read)) + } + } + } else { + if item.message.effectivelyIncoming(item.account.peerId) { + statusType = .ImageIncoming + } else { + if item.message.flags.contains(.Failed) { + statusType = .ImageOutgoing(.Failed) + } else if item.message.flags.isSending { + statusType = .ImageOutgoing(.Sending) + } else { + statusType = .ImageOutgoing(.Sent(read: item.read)) + } } } - } else { - if item.message.effectivelyIncoming { - statusType = .ImageIncoming - } else { - if item.message.flags.contains(.Failed) { - statusType = .ImageOutgoing(.Failed) - } else if item.message.flags.isSending { - statusType = .ImageOutgoing(.Sending) - } else { - statusType = .ImageOutgoing(.Sent(read: item.read)) - } - } - } - } else { - statusType = nil + default: + statusType = nil } var statusSize = CGSize() var statusApply: ((Bool) -> Void)? if let statusType = statusType { - let (size, apply) = statusLayout(item.theme, edited && !sentViaBot, viewCount, dateText, statusType, CGSize(width: constrainedSize.width, height: CGFloat.greatestFiniteMagnitude)) + let (size, apply) = statusLayout(item.presentationData.theme, item.presentationData.strings, edited && !sentViaBot, viewCount, dateText, statusType, CGSize(width: constrainedSize.width, height: CGFloat.greatestFiniteMagnitude)) statusSize = size statusApply = apply } let contentWidth: CGFloat - if let _ = titleString { + if let selectedMedia = selectedMedia, selectedMedia.liveBroadcastingTimeout != nil { + contentWidth = imageSize.width + bubbleInsets.left + bubbleInsets.right + } else if selectedMedia?.venue != nil { contentWidth = imageSize.width + max(statusSize.width, max(titleLayout.size.width, textLayout.size.width)) + layoutConstants.text.bubbleInsets.right + 8.0 } else { - contentWidth = imageSize.width + layoutConstants.image.bubbleInsets.left + layoutConstants.image.bubbleInsets.right + contentWidth = imageSize.width + bubbleInsets.left + bubbleInsets.right } return (contentWidth, { boundingWidth in let arguments = TransformImageArguments(corners: imageCorners, imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()) - let imageLayoutSize = CGSize(width: imageSize.width + layoutConstants.image.bubbleInsets.left + layoutConstants.image.bubbleInsets.right, height: imageSize.height + layoutConstants.image.bubbleInsets.top + layoutConstants.image.bubbleInsets.bottom) + let imageLayoutSize = CGSize(width: imageSize.width + bubbleInsets.left + bubbleInsets.right, height: imageSize.height + bubbleInsets.top + bubbleInsets.bottom) let layoutSize: CGSize let statusFrame: CGRect @@ -181,14 +254,22 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { let imageFrame: CGRect - if let _ = titleString { - layoutSize = CGSize(width: contentWidth, height: imageLayoutSize.height + 10.0) + if activeLiveBroadcastingTimeout != nil { + layoutSize = CGSize(width: imageLayoutSize.width + bubbleInsets.left, height: imageLayoutSize.height + 1.0 + titleLayout.size.height + 1.0 + textLayout.size.height + 10.0) + + imageFrame = baseImageFrame.offsetBy(dx: bubbleInsets.left, dy: bubbleInsets.top) + statusFrame = CGRect(origin: CGPoint(x: boundingWidth - statusSize.width - layoutConstants.text.bubbleInsets.right, y: layoutSize.height - statusSize.height - 5.0 - 4.0), size: statusSize) - imageFrame = baseImageFrame.offsetBy(dx: 5.0, dy: 5.0) } else { - layoutSize = CGSize(width: max(imageLayoutSize.width, statusSize.width + layoutConstants.image.bubbleInsets.left + layoutConstants.image.bubbleInsets.right + layoutConstants.image.statusInsets.left + layoutConstants.image.statusInsets.right), height: imageLayoutSize.height) - statusFrame = CGRect(origin: CGPoint(x: layoutSize.width - layoutConstants.image.bubbleInsets.right - layoutConstants.image.statusInsets.right - statusSize.width, y: layoutSize.height - layoutConstants.image.bubbleInsets.bottom - layoutConstants.image.statusInsets.bottom - statusSize.height), size: statusSize) - imageFrame = baseImageFrame.offsetBy(dx: layoutConstants.image.bubbleInsets.left, dy: layoutConstants.image.bubbleInsets.top) + if selectedMedia?.venue != nil { + layoutSize = CGSize(width: contentWidth, height: imageLayoutSize.height + 10.0) + statusFrame = CGRect(origin: CGPoint(x: boundingWidth - statusSize.width - layoutConstants.text.bubbleInsets.right, y: layoutSize.height - statusSize.height - 5.0 - 4.0), size: statusSize) + imageFrame = baseImageFrame.offsetBy(dx: 5.0, dy: 5.0) + } else { + layoutSize = CGSize(width: max(imageLayoutSize.width, statusSize.width + bubbleInsets.left + bubbleInsets.right + layoutConstants.image.statusInsets.left + layoutConstants.image.statusInsets.right), height: imageLayoutSize.height) + statusFrame = CGRect(origin: CGPoint(x: layoutSize.width - bubbleInsets.right - layoutConstants.image.statusInsets.right - statusSize.width, y: layoutSize.height - bubbleInsets.bottom - layoutConstants.image.statusInsets.bottom - statusSize.height), size: statusSize) + imageFrame = baseImageFrame.offsetBy(dx: bubbleInsets.left, dy: bubbleInsets.top) + } } let imageApply = makeImageLayout(arguments) @@ -200,11 +281,25 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { strongSelf.imageNode.frame = imageFrame + var transition: ContainedViewLayoutTransition = .immediate + if case let .System(duration) = animation { + transition = .animated(duration: duration, curve: .spring) + } + let _ = titleApply() let _ = textApply() - strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: imageFrame.maxX + 7.0, y: imageFrame.minY + 1.0), size: titleLayout.size) - strongSelf.textNode.frame = CGRect(origin: CGPoint(x: imageFrame.maxX + 7.0, y: imageFrame.minY + 19.0), size: textLayout.size) + transition.updateAlpha(node: strongSelf.dateAndStatusNode, alpha: activeLiveBroadcastingTimeout != nil ? 0.0 : 1.0) + + if let selectedMedia = selectedMedia, selectedMedia.liveBroadcastingTimeout != nil { + strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: imageFrame.minX + 7.0, y: imageFrame.maxY + 6.0), size: titleLayout.size) + strongSelf.textNode.frame = CGRect(origin: CGPoint(x: imageFrame.minX + 7.0, y: imageFrame.maxY + 6.0 + titleLayout.size.height), size: textLayout.size) + transition.updateAlpha(node: strongSelf.titleNode, alpha: activeLiveBroadcastingTimeout != nil ? 1.0 : 0.0) + transition.updateAlpha(node: strongSelf.textNode, alpha: activeLiveBroadcastingTimeout != nil ? 1.0 : 0.0) + } else { + strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: imageFrame.maxX + 7.0, y: imageFrame.minY + 1.0), size: titleLayout.size) + strongSelf.textNode.frame = CGRect(origin: CGPoint(x: imageFrame.maxX + 7.0, y: imageFrame.minY + 19.0), size: textLayout.size) + } if let statusApply = statusApply { if strongSelf.dateAndStatusNode.supernode == nil { @@ -237,10 +332,59 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { } if let updateImageSignal = updateImageSignal { - strongSelf.imageNode.setSignal(account: item.account, signal: updateImageSignal) + strongSelf.imageNode.setSignal(updateImageSignal) + } + + if let activeLiveBroadcastingTimeout = activeLiveBroadcastingTimeout { + if strongSelf.liveTimerNode == nil { + let liveTimerNode = ChatMessageLiveLocationTimerNode() + strongSelf.liveTimerNode = liveTimerNode + strongSelf.addSubnode(liveTimerNode) + } + let timerSize = CGSize(width: 28.0, height: 28.0) + strongSelf.liveTimerNode?.frame = CGRect(origin: CGPoint(x: imageFrame.maxX - 10.0 - timerSize.width, y: imageFrame.maxY + 11.0), size: timerSize) + + let timerForegroundColor: UIColor = item.message.effectivelyIncoming(item.account.peerId) ? item.presentationData.theme.chat.bubble.incomingAccentControlColor : item.presentationData.theme.chat.bubble.outgoingAccentControlColor + let timerTextColor: UIColor = item.message.effectivelyIncoming(item.account.peerId) ? item.presentationData.theme.chat.bubble.incomingAccentTextColor : item.presentationData.theme.chat.bubble.outgoingAccentTextColor + strongSelf.liveTimerNode?.update(backgroundColor: timerForegroundColor.withAlphaComponent(0.4), foregroundColor: timerForegroundColor, textColor: timerTextColor, beginTimestamp: Double(item.message.timestamp), timeout: Double(activeLiveBroadcastingTimeout), strings: item.presentationData.strings) + + if strongSelf.liveTextNode == nil { + let liveTextNode = ChatMessageLiveLocationTextNode() + strongSelf.liveTextNode = liveTextNode + strongSelf.addSubnode(liveTextNode) + } + strongSelf.liveTextNode?.frame = CGRect(origin: CGPoint(x: imageFrame.minX + 7.0, y: imageFrame.maxY + 6.0 + titleLayout.size.height), size: CGSize(width: imageFrame.size.width - 14.0 - 40.0, height: 18.0)) + + var updateTimestamp = item.message.timestamp + for attribute in item.message.attributes { + if let attribute = attribute as? EditedMessageAttribute { + updateTimestamp = attribute.date + break + } + } + + strongSelf.liveTextNode?.update(color: timerTextColor, timestamp: Double(updateTimestamp), strings: item.presentationData.strings, timeFormat: item.presentationData.timeFormat) + } else { + if let liveTimerNode = strongSelf.liveTimerNode { + strongSelf.liveTimerNode = nil + transition.updateAlpha(node: liveTimerNode, alpha: 0.0, completion: { [weak liveTimerNode] _ in + liveTimerNode?.removeFromSupernode() + }) + } + + if let liveTextNode = strongSelf.liveTextNode { + strongSelf.liveTextNode = nil + transition.updateAlpha(node: liveTextNode, alpha: 0.0, completion: { [weak liveTextNode] _ in + liveTextNode?.removeFromSupernode() + }) + } } imageApply() + + strongSelf.pinNode.frame = CGRect(origin: CGPoint(x: imageFrame.minX + floor((imageFrame.size.width - pinSize.width) / 2.0), y: imageFrame.minY + floor(imageFrame.size.height * 0.5 - 10.0 - pinSize.height / 2.0)), size: pinSize) + + pinApply() } }) }) @@ -260,8 +404,8 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) } - override func transitionNode(media: Media) -> ASDisplayNode? { - if let currentMedia = self.media, currentMedia.isEqual(media) { + override func transitionNode(messageId: MessageId, media: Media) -> ASDisplayNode? { + if self.item?.message.id == messageId, let currentMedia = self.media, currentMedia.isEqual(media) { return self.imageNode } return nil @@ -288,7 +432,7 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { @objc func imageTap(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { if let item = self.item { - self.controllerInteraction?.openMessage(item.message.id) + item.controllerInteraction.openMessage(item.message.id) } } } diff --git a/TelegramUI/ChatMessageMediaBubbleContentNode.swift b/TelegramUI/ChatMessageMediaBubbleContentNode.swift index 55cd6e0528..f69f181ae2 100644 --- a/TelegramUI/ChatMessageMediaBubbleContentNode.swift +++ b/TelegramUI/ChatMessageMediaBubbleContentNode.swift @@ -6,14 +6,14 @@ import Postbox import TelegramCore class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { - override var properties: ChatMessageBubbleContentProperties { - return ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 5.0) + override var supportsMosaic: Bool { + return true } private let interactiveImageNode: ChatMessageInteractiveMediaNode private let dateAndStatusNode: ChatMessageDateAndStatusNode + private var selectionNode: GridMessageSelectionNode? - private var item: ChatMessageItem? private var media: Media? override var visibility: ListViewItemNodeVisibility { @@ -32,8 +32,8 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { self.interactiveImageNode.activateLocalContent = { [weak self] in if let strongSelf = self { - if let item = strongSelf.item, let controllerInteraction = strongSelf.controllerInteraction, !item.message.containsSecretMedia { - controllerInteraction.openMessage(item.message.id) + if let item = strongSelf.item, !item.message.containsSecretMedia { + item.controllerInteraction.openMessage(item.message.id) } } } @@ -43,11 +43,11 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { fatalError("init(coder:) has not been implemented") } - override func asyncLayoutContent() -> (_ item: ChatMessageItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ position: ChatMessageBubbleContentPosition, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))) { + override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))) { let interactiveImageLayout = self.interactiveImageNode.asyncLayout() let statusLayout = self.dateAndStatusNode.asyncLayout() - return { item, layoutConstants, position, constrainedSize in + return { item, layoutConstants, preparePosition, selection, constrainedSize in var selectedMedia: Media? var automaticDownload: Bool = false for media in item.message.media { @@ -62,26 +62,54 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { } } - let initialImageCorners = chatMessageBubbleImageContentCorners(relativeContentPosition: position, normalRadius: layoutConstants.image.defaultCornerRadius, mergedRadius: layoutConstants.image.mergedCornerRadius, mergedWithAnotherContentRadius: layoutConstants.image.contentMergedCornerRadius) + let bubbleInsets: UIEdgeInsets + let sizeCalculation: InteractiveMediaNodeSizeCalculation - let (initialWidth, _, refineLayout) = interactiveImageLayout(item.account, item.theme, item.strings, item.message, selectedMedia!, initialImageCorners, automaticDownload, CGSize(width: constrainedSize.width, height: constrainedSize.height), layoutConstants) - - return (initialWidth + layoutConstants.image.bubbleInsets.left + layoutConstants.image.bubbleInsets.right, { constrainedSize in - let (refinedWidth, finishLayout) = refineLayout(constrainedSize) - - return (refinedWidth + layoutConstants.image.bubbleInsets.left + layoutConstants.image.bubbleInsets.right, { boundingWidth in - let (imageSize, imageApply) = finishLayout(boundingWidth - layoutConstants.image.bubbleInsets.left - layoutConstants.image.bubbleInsets.right) + switch preparePosition { + case .linear: + if case .color = item.presentationData.wallpaper { + bubbleInsets = UIEdgeInsets() + } else { + bubbleInsets = layoutConstants.image.bubbleInsets + } - var t = Int(item.message.timestamp) - var timeinfo = tm() - localtime_r(&t, &timeinfo) + sizeCalculation = .constrained(CGSize(width: constrainedSize.width, height: constrainedSize.height)) + case .mosaic: + bubbleInsets = UIEdgeInsets() + sizeCalculation = .unconstrained + } + + let (unboundSize, initialWidth, refineLayout) = interactiveImageLayout(item.account, item.presentationData.theme, item.presentationData.strings, item.message, selectedMedia!, automaticDownload, sizeCalculation, layoutConstants) + + var forceFullCorners = false + if let media = selectedMedia as? TelegramMediaFile, media.isAnimated { + forceFullCorners = true + } + + let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 7.0, hidesBackgroundForEmptyWallpapers: true, forceFullCorners: forceFullCorners) + + return (contentProperties, unboundSize, initialWidth + bubbleInsets.left + bubbleInsets.right, { constrainedSize, position in + var updatedPosition: ChatMessageBubbleContentPosition = position + if forceFullCorners, case .linear = updatedPosition { + updatedPosition = .linear(top: .None(.None(.None)), bottom: .None(.None(.None))) + } + + let imageCorners = chatMessageBubbleImageContentCorners(relativeContentPosition: updatedPosition, normalRadius: layoutConstants.image.defaultCornerRadius, mergedRadius: layoutConstants.image.mergedCornerRadius, mergedWithAnotherContentRadius: layoutConstants.image.contentMergedCornerRadius) + + let (refinedWidth, finishLayout) = refineLayout(constrainedSize, imageCorners) + + return (refinedWidth + bubbleInsets.left + bubbleInsets.right, { boundingWidth in + let (imageSize, imageApply) = finishLayout(boundingWidth - bubbleInsets.left - bubbleInsets.right) var edited = false var sentViaBot = false var viewCount: Int? for attribute in item.message.attributes { if let _ = attribute as? EditedMessageAttribute { - edited = true + if case .mosaic = preparePosition { + } else { + edited = true + } } else if let attribute = attribute as? ViewCountMessageAttribute { viewCount = attribute.count } else if let _ = attribute as? InlineBotMessageAttribute { @@ -89,53 +117,97 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { } } - var dateText = String(format: "%02d:%02d", arguments: [Int(timeinfo.tm_hour), Int(timeinfo.tm_min)]) + var dateText = stringForMessageTimestamp(timestamp: item.message.timestamp, timeFormat: item.presentationData.timeFormat) + var authorTitle: String? if let author = item.message.author as? TelegramUser { if author.botInfo != nil { sentViaBot = true } if let peer = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { - dateText = "\(author.displayTitle), \(dateText)" + authorTitle = author.displayTitle } + } else { + if let peer = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { + for attribute in item.message.attributes { + if let attribute = attribute as? AuthorSignatureMessageAttribute { + authorTitle = attribute.signature + break + } + } + } + } + + if let authorTitle = authorTitle, !authorTitle.isEmpty { + dateText = "\(authorTitle), \(dateText)" } let statusType: ChatMessageDateAndStatusType? - if case .None = position.bottom { - if item.message.effectivelyIncoming { - statusType = .ImageIncoming - } else { - if item.message.flags.contains(.Failed) { - statusType = .ImageOutgoing(.Failed) - } else if item.message.flags.isSending { - statusType = .ImageOutgoing(.Sending) + var statusHorizontalOffset: CGFloat = 0.0 + switch position { + case .linear(_, .None): + if item.message.effectivelyIncoming(item.account.peerId) { + statusType = .ImageIncoming } else { - statusType = .ImageOutgoing(.Sent(read: item.read)) + if item.message.flags.contains(.Failed) { + statusType = .ImageOutgoing(.Failed) + } else if item.message.flags.isSending { + statusType = .ImageOutgoing(.Sending) + } else { + statusType = .ImageOutgoing(.Sent(read: item.read)) + } } - } - } else { - statusType = nil + case let .mosaic(position): + if let mosaicStatusHorizontalOffset = position.mosaicStatusHorizontalOffset { + statusHorizontalOffset = mosaicStatusHorizontalOffset + if item.message.effectivelyIncoming(item.account.peerId) { + statusType = .ImageIncoming + } else { + if item.message.flags.contains(.Failed) { + statusType = .ImageOutgoing(.Failed) + } else if item.message.flags.isSending { + statusType = .ImageOutgoing(.Sending) + } else { + statusType = .ImageOutgoing(.Sent(read: item.read)) + } + } + } else { + statusType = nil + } + default: + statusType = nil } - let imageLayoutSize = CGSize(width: imageSize.width + layoutConstants.image.bubbleInsets.left + layoutConstants.image.bubbleInsets.right, height: imageSize.height + layoutConstants.image.bubbleInsets.top + layoutConstants.image.bubbleInsets.bottom) + let imageLayoutSize = CGSize(width: imageSize.width + bubbleInsets.left + bubbleInsets.right, height: imageSize.height + bubbleInsets.top + bubbleInsets.bottom) var statusSize = CGSize() var statusApply: ((Bool) -> Void)? if let statusType = statusType { - let (size, apply) = statusLayout(item.theme, edited && !sentViaBot, viewCount, dateText, statusType, CGSize(width: imageLayoutSize.width, height: CGFloat.greatestFiniteMagnitude)) + let (size, apply) = statusLayout(item.presentationData.theme, item.presentationData.strings, edited && !sentViaBot, viewCount, dateText, statusType, CGSize(width: 200.0, height: CGFloat.greatestFiniteMagnitude)) statusSize = size statusApply = apply } - let layoutSize = CGSize(width: max(imageLayoutSize.width, statusSize.width + layoutConstants.image.bubbleInsets.left + layoutConstants.image.bubbleInsets.right + layoutConstants.image.statusInsets.left + layoutConstants.image.statusInsets.right), height: imageLayoutSize.height) + var layoutWidth = imageLayoutSize.width + if case .constrained = sizeCalculation { + layoutWidth = max(layoutWidth, statusSize.width + bubbleInsets.left + bubbleInsets.right + layoutConstants.image.statusInsets.left + layoutConstants.image.statusInsets.right) + } + + let layoutSize = CGSize(width: layoutWidth, height: imageLayoutSize.height) return (layoutSize, { [weak self] animation in if let strongSelf = self { strongSelf.item = item strongSelf.media = selectedMedia - strongSelf.interactiveImageNode.frame = CGRect(origin: CGPoint(x: layoutConstants.image.bubbleInsets.left, y: layoutConstants.image.bubbleInsets.top), size: imageSize) + let imageFrame = CGRect(origin: CGPoint(x: bubbleInsets.left, y: bubbleInsets.top), size: imageSize) + var transition: ContainedViewLayoutTransition = .immediate + if case let .System(duration) = animation { + transition = .animated(duration: duration, curve: .spring) + } + + transition.updateFrame(node: strongSelf.interactiveImageNode, frame: imageFrame) if let statusApply = statusApply { if strongSelf.dateAndStatusNode.supernode == nil { @@ -146,12 +218,53 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { hasAnimation = false } statusApply(hasAnimation) - strongSelf.dateAndStatusNode.frame = CGRect(origin: CGPoint(x: layoutSize.width - layoutConstants.image.bubbleInsets.right - layoutConstants.image.statusInsets.right - statusSize.width, y: layoutSize.height - layoutConstants.image.bubbleInsets.bottom - layoutConstants.image.statusInsets.bottom - statusSize.height), size: statusSize) + + let dateAndStatusFrame = CGRect(origin: CGPoint(x: layoutSize.width - bubbleInsets.right - layoutConstants.image.statusInsets.right - statusSize.width + statusHorizontalOffset, y: layoutSize.height - bubbleInsets.bottom - layoutConstants.image.statusInsets.bottom - statusSize.height), size: statusSize) + + if case .unconstrained = sizeCalculation { + strongSelf.dateAndStatusNode.clipsToBounds = true + + let deltaWidth = dateAndStatusFrame.size.width - layoutSize.width + layoutConstants.image.statusInsets.right - statusHorizontalOffset + let adjustedFrame = CGRect(origin: CGPoint(x: 0.0, y: dateAndStatusFrame.minY), size: CGSize(width: layoutSize.width, height: dateAndStatusFrame.height)) + strongSelf.dateAndStatusNode.frame = adjustedFrame + strongSelf.dateAndStatusNode.bounds = CGRect(origin: CGPoint(x: deltaWidth, y: 0.0), size: adjustedFrame.size) + } else { + strongSelf.dateAndStatusNode.clipsToBounds = false + strongSelf.dateAndStatusNode.frame = dateAndStatusFrame + strongSelf.dateAndStatusNode.bounds = CGRect(origin: CGPoint(), size: dateAndStatusFrame.size) + } } else if strongSelf.dateAndStatusNode.supernode != nil { strongSelf.dateAndStatusNode.removeFromSupernode() } - imageApply() + imageApply(transition) + + if let selection = selection { + if let selectionNode = strongSelf.selectionNode { + selectionNode.frame = imageFrame + selectionNode.updateSelected(selection, animated: animation.isAnimated) + } else { + let selectionNode = GridMessageSelectionNode(theme: item.presentationData.theme, toggle: { value in + item.controllerInteraction.toggleMessagesSelection([item.message.id], value) + }) + strongSelf.selectionNode = selectionNode + strongSelf.addSubnode(selectionNode) + selectionNode.frame = imageFrame + selectionNode.updateSelected(selection, animated: false) + if animation.isAnimated { + selectionNode.animateIn() + } + } + } else if let selectionNode = strongSelf.selectionNode { + strongSelf.selectionNode = nil + if animation.isAnimated { + selectionNode.animateOut(completion: { [weak selectionNode] in + selectionNode?.removeFromSupernode() + }) + } else { + selectionNode.removeFromSupernode() + } + } } }) }) @@ -159,20 +272,8 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double) { - self.interactiveImageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - } - - override func animateAdded(_ currentTimestamp: Double, duration: Double) { - self.interactiveImageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - } - - override func animateRemoved(_ currentTimestamp: Double, duration: Double) { - self.interactiveImageNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) - } - - override func transitionNode(media: Media) -> ASDisplayNode? { - if let currentMedia = self.media, currentMedia.isEqual(media) { + override func transitionNode(messageId: MessageId, media: Media) -> ASDisplayNode? { + if self.item?.message.id == messageId, let currentMedia = self.media, currentMedia.isEqual(media) { return self.interactiveImageNode } return nil @@ -200,4 +301,20 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { } return .none } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } + + override func animateAdded(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) + } + + override func animateInsertionIntoBubble(_ duration: Double) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } } diff --git a/TelegramUI/ChatMessageNotificationItem.swift b/TelegramUI/ChatMessageNotificationItem.swift index 7be0ce47d6..3ba8d6e552 100644 --- a/TelegramUI/ChatMessageNotificationItem.swift +++ b/TelegramUI/ChatMessageNotificationItem.swift @@ -7,17 +7,21 @@ import SwiftSignalKit public final class ChatMessageNotificationItem: NotificationItem { let account: Account + let strings: PresentationStrings let message: Message - let tapAction: () -> Void + let tapAction: () -> Bool + let expandAction: (@escaping () -> (ASDisplayNode?, () -> Void)) -> Void public var groupingKey: AnyHashable? { return message.id.peerId } - public init(account: Account, message: Message, tapAction: @escaping () -> Void) { + public init(account: Account, strings: PresentationStrings, message: Message, tapAction: @escaping () -> Bool, expandAction: @escaping (() -> (ASDisplayNode?, () -> Void)) -> Void) { self.account = account + self.strings = strings self.message = message self.tapAction = tapAction + self.expandAction = expandAction } public func node() -> NotificationItemNode { @@ -26,8 +30,18 @@ public final class ChatMessageNotificationItem: NotificationItem { return node } - public func tapped() { - self.tapAction() + public func tapped(_ take: @escaping () -> (ASDisplayNode?, () -> Void)) { + if self.tapAction() { + self.expandAction(take) + } + } + + public func canBeExpanded() -> Bool { + return true + } + + public func expand(_ take: @escaping () -> (ASDisplayNode?, () -> Void)) { + self.expandAction(take) } } @@ -37,20 +51,23 @@ final class ChatMessageNotificationItemNode: NotificationItemNode { private var item: ChatMessageNotificationItem? private let avatarNode: AvatarNode - private let titleNode: ASTextNode - private let textNode: ASTextNode + private let titleNode: TextNode + private let textNode: TextNode private let imageNode: TransformImageNode + private var titleAttributedText: NSAttributedString? + private var textAttributedText: NSAttributedString? + + private var validLayout: CGFloat? + override init() { self.avatarNode = AvatarNode(font: avatarFont) - self.titleNode = ASTextNode() + self.titleNode = TextNode() self.titleNode.isLayerBacked = true - self.titleNode.maximumNumberOfLines = 1 - self.textNode = ASTextNode() + self.textNode = TextNode() self.textNode.isLayerBacked = true - self.textNode.maximumNumberOfLines = 2 self.imageNode = TransformImageNode() @@ -64,16 +81,17 @@ final class ChatMessageNotificationItemNode: NotificationItemNode { func setupItem(_ item: ChatMessageNotificationItem) { self.item = item + let presentationData = item.account.telegramApplicationContext.currentPresentationData.with { $0 } if let peer = messageMainPeer(item.message) { self.avatarNode.setPeer(account: item.account, peer: peer) if let channel = peer as? TelegramChannel, case .broadcast = channel.info { - self.titleNode.attributedText = NSAttributedString(string: peer.displayTitle, font: Font.semibold(16.0), textColor: .black) + self.titleAttributedText = NSAttributedString(string: peer.displayTitle, font: Font.semibold(16.0), textColor: presentationData.theme.inAppNotification.primaryTextColor) } else if let author = item.message.author, author.id != peer.id { - self.titleNode.attributedText = NSAttributedString(string: author.displayTitle + "@" + peer.displayTitle, font: Font.semibold(16.0), textColor: .black) + self.titleAttributedText = NSAttributedString(string: author.displayTitle + "@" + peer.displayTitle, font: Font.semibold(16.0), textColor: presentationData.theme.inAppNotification.primaryTextColor) } else { - self.titleNode.attributedText = NSAttributedString(string: peer.displayTitle, font: Font.semibold(16.0), textColor: .black) + self.titleAttributedText = NSAttributedString(string: peer.displayTitle, font: Font.semibold(16.0), textColor: presentationData.theme.inAppNotification.primaryTextColor) } } @@ -110,59 +128,12 @@ final class ChatMessageNotificationItemNode: NotificationItemNode { if file.isSticker { updateImageSignal = chatMessageSticker(account: item.account, file: file, small: true, fetched: true) } else if file.isVideo { - updateImageSignal = mediaGridMessageVideo(account: item.account, video: file) + updateImageSignal = mediaGridMessageVideo(postbox: item.account.postbox, video: file) } } } - var messageText = item.message.text - for media in item.message.media { - switch media { - case _ as TelegramMediaImage: - if messageText.isEmpty { - messageText = "Photo" - } - case let file as TelegramMediaFile: - var selectedText = false - loop: for attribute in file.attributes { - switch attribute { - case let .Audio(isVoice, _, title, performer, _): - if isVoice { - messageText = "Voice Message" - } else { - if let title = title, let performer = performer, !title.isEmpty, !performer.isEmpty { - messageText = title + " — " + performer - } else if let title = title, !title.isEmpty { - messageText = title - } else if let performer = performer, !performer.isEmpty { - messageText = performer - } else { - messageText = "Audio" - } - } - selectedText = true - break loop - case let .Sticker(displayText, _, _): - messageText = "\(displayText) Sticker" - selectedText = true - break loop - case .Video: - if messageText.isEmpty { - messageText = "Video" - } - selectedText = true - break loop - default: - break - } - } - if !selectedText { - messageText = file.fileName ?? "File" - } - default: - break - } - } + let messageText = descriptionStringForMessage(item.message, strings: item.strings, accountPeerId: item.account.peerId) if let applyImage = applyImage { applyImage() @@ -172,13 +143,19 @@ final class ChatMessageNotificationItemNode: NotificationItemNode { } if let updateImageSignal = updateImageSignal { - self.imageNode.setSignal(account: item.account, signal: updateImageSignal) + self.imageNode.setSignal(updateImageSignal) } - self.textNode.attributedText = NSAttributedString(string: messageText, font: Font.regular(16.0), textColor: .black) + self.textAttributedText = NSAttributedString(string: messageText, font: Font.regular(16.0), textColor: presentationData.theme.inAppNotification.primaryTextColor) + + if let validLayout = self.validLayout { + let _ = self.updateLayout(width: validLayout, transition: .immediate) + } } override func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { + self.validLayout = width + let panelHeight: CGFloat = 74.0 let leftInset: CGFloat = 77.0 var rightInset: CGFloat = 8.0 @@ -189,13 +166,21 @@ final class ChatMessageNotificationItemNode: NotificationItemNode { transition.updateFrame(node: self.avatarNode, frame: CGRect(origin: CGPoint(x: 10.0, y: 10.0), size: CGSize(width: 54.0, height: 54.0))) - let textSize = self.textNode.measure(CGSize(width: width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude)) - let textSpacing: CGFloat = -2.0 + let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: self.titleAttributedText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .left, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) + let _ = titleApply() - let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: 1.0 + floor((panelHeight - textSize.height - 22.0) / 2.0)), size: CGSize(width: width - leftInset - rightInset, height: 22.0)) + let makeTextLayout = TextNode.asyncLayout(self.textNode) + let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: self.textAttributedText, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .left, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) + let _ = titleApply() + let _ = textApply() + + let textSpacing: CGFloat = 1.0 + + let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: 1.0 + floor((panelHeight - textLayout.size.height - titleLayout.size.height - textSpacing) / 2.0)), size: titleLayout.size) transition.updateFrame(node: self.titleNode, frame: titleFrame) - transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: leftInset, y: titleFrame.maxY + textSpacing), size: textSize)) + transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: leftInset, y: titleFrame.maxY + textSpacing), size: textLayout.size)) transition.updateFrame(node: self.imageNode, frame: CGRect(origin: CGPoint(x: width - 9.0 - 55.0, y: 9.0), size: CGSize(width: 55.0, height: 55.0))) diff --git a/TelegramUI/ChatMessageReplyInfoNode.swift b/TelegramUI/ChatMessageReplyInfoNode.swift index d0670d6c0d..06de78ea7a 100644 --- a/TelegramUI/ChatMessageReplyInfoNode.swift +++ b/TelegramUI/ChatMessageReplyInfoNode.swift @@ -14,63 +14,6 @@ private let titleFont: UIFont = { }() private let textFont = Font.regular(14.0) -func textStringForReplyMessage(_ message: Message) -> (String, Bool) { - if !message.text.isEmpty { - return (message.text, false) - } else { - for media in message.media { - switch media { - case _ as TelegramMediaImage: - return ("Photo", true) - case let file as TelegramMediaFile: - var fileName: String = "File" - for attribute in file.attributes { - switch attribute { - case let .Sticker(text, _, _): - return ("\(text) Sticker", true) - case let .FileName(name): - fileName = name - case let .Audio(isVoice, _, title, performer, _): - if isVoice { - return ("Voice Message", true) - } else { - if let title = title, let performer = performer, !title.isEmpty, !performer.isEmpty { - return (title + " — " + performer, true) - } else if let title = title, !title.isEmpty { - return (title, true) - } else if let performer = performer, !performer.isEmpty { - return (performer, true) - } else { - return ("Audio", true) - } - } - case .Video: - if file.isAnimated { - return ("GIF", true) - } else { - return ("Video", true) - } - default: - break - } - } - return (fileName, true) - case _ as TelegramMediaContact: - return ("Contact", true) - case let game as TelegramMediaGame: - return (game.title, true) - case _ as TelegramMediaMap: - return ("Map", true) - case let action as TelegramMediaAction: - return ("", true) - default: - break - } - } - return ("", false) - } -} - enum ChatMessageReplyInfoType { case bubble(incoming: Bool) case standalone @@ -78,7 +21,7 @@ enum ChatMessageReplyInfoType { class ChatMessageReplyInfoNode: ASDisplayNode { private let contentNode: ASDisplayNode - private let lineNode: ASDisplayNode + private let lineNode: ASImageNode private var titleNode: TextNode? private var textNode: TextNode? private var imageNode: TransformImageNode? @@ -92,8 +35,9 @@ class ChatMessageReplyInfoNode: ASDisplayNode { self.contentNode.contentMode = .left self.contentNode.contentsScale = UIScreenScale - self.lineNode = ASDisplayNode() + self.lineNode = ASImageNode() self.lineNode.displaysAsynchronously = false + self.lineNode.displayWithoutProcessing = true self.lineNode.isLayerBacked = true super.init() @@ -102,29 +46,29 @@ class ChatMessageReplyInfoNode: ASDisplayNode { self.contentNode.addSubnode(self.lineNode) } - class func asyncLayout(_ maybeNode: ChatMessageReplyInfoNode?) -> (_ theme: PresentationTheme, _ account: Account, _ type: ChatMessageReplyInfoType, _ message: Message, _ constrainedSize: CGSize) -> (CGSize, () -> ChatMessageReplyInfoNode) { + class func asyncLayout(_ maybeNode: ChatMessageReplyInfoNode?) -> (_ theme: PresentationTheme, _ strings: PresentationStrings, _ account: Account, _ type: ChatMessageReplyInfoType, _ message: Message, _ constrainedSize: CGSize) -> (CGSize, () -> ChatMessageReplyInfoNode) { let titleNodeLayout = TextNode.asyncLayout(maybeNode?.titleNode) let textNodeLayout = TextNode.asyncLayout(maybeNode?.textNode) let imageNodeLayout = TransformImageNode.asyncLayout(maybeNode?.imageNode) let previousMedia = maybeNode?.previousMedia - return { theme, account, type, message, constrainedSize in + return { theme, strings, account, type, message, constrainedSize in let titleString = message.author?.displayTitle ?? "" - let (textString, textMedia) = textStringForReplyMessage(message) + let textString = descriptionStringForMessage(message, strings: strings, accountPeerId: account.peerId) let titleColor: UIColor - let lineColor: UIColor + let lineImage: UIImage? let textColor: UIColor switch type { case let .bubble(incoming): - titleColor = incoming ? theme.chat.bubble.incomingAccentColor : theme.chat.bubble.outgoingAccentColor - lineColor = incoming ? theme.chat.bubble.incomingAccentColor : theme.chat.bubble.outgoingAccentColor + titleColor = incoming ? theme.chat.bubble.incomingAccentTextColor : theme.chat.bubble.outgoingAccentTextColor + lineImage = incoming ? PresentationResourcesChat.chatBubbleVerticalLineIncomingImage(theme) : PresentationResourcesChat.chatBubbleVerticalLineOutgoingImage(theme) textColor = incoming ? theme.chat.bubble.incomingPrimaryTextColor : theme.chat.bubble.outgoingPrimaryTextColor case .standalone: titleColor = theme.chat.serviceMessage.serviceMessagePrimaryTextColor - lineColor = titleColor + lineImage = PresentationResourcesChat.chatServiceVerticalLineImage(theme) textColor = titleColor } @@ -134,22 +78,26 @@ class ChatMessageReplyInfoNode: ASDisplayNode { var updatedMedia: Media? var imageDimensions: CGSize? - for media in message.media { - if let image = media as? TelegramMediaImage { - updatedMedia = image - if let representation = largestRepresentationForPhoto(image) { - imageDimensions = representation.dimensions + if !message.containsSecretMedia { + for media in message.media { + if let image = media as? TelegramMediaImage { + updatedMedia = image + if let representation = largestRepresentationForPhoto(image) { + imageDimensions = representation.dimensions + } + break + } else if let file = media as? TelegramMediaFile, file.isVideo { + updatedMedia = file + if !file.isInstantVideo { + if let dimensions = file.dimensions { + imageDimensions = dimensions + } else if let representation = largestImageRepresentation(file.previewRepresentations), !file.isSticker { + imageDimensions = representation.dimensions + } + overlayIcon = PresentationResourcesChat.chatBubbleReplyThumbnailPlayImage(theme) + } + break } - break - } else if let file = media as? TelegramMediaFile, file.isVideo { - updatedMedia = file - if let dimensions = file.dimensions { - imageDimensions = dimensions - } else if let representation = largestImageRepresentation(file.previewRepresentations), !file.isSticker { - imageDimensions = representation.dimensions - } - overlayIcon = PresentationResourcesChat.chatBubbleReplyThumbnailPlayImage(theme) - break } } @@ -172,7 +120,12 @@ class ChatMessageReplyInfoNode: ASDisplayNode { if let image = updatedMedia as? TelegramMediaImage { updateImageSignal = chatMessagePhotoThumbnail(account: account, photo: image) } else if let file = updatedMedia as? TelegramMediaFile { - updateImageSignal = chatMessageVideoThumbnail(account: account, file: file) + if file.isVideo { + updateImageSignal = chatMessageVideoThumbnail(account: account, file: file) + } else if let iconImageRepresentation = smallestImageRepresentation(file.previewRepresentations) { + let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [iconImageRepresentation], reference: nil) + updateImageSignal = chatWebpageSnippetPhoto(account: account, photo: tmpImage) + } } } @@ -180,8 +133,8 @@ class ChatMessageReplyInfoNode: ASDisplayNode { let contrainedTextSize = CGSize(width: maximumTextWidth, height: constrainedSize.height) - let (titleLayout, titleApply) = titleNodeLayout(NSAttributedString(string: titleString, font: titleFont, textColor: titleColor), nil, 1, .end, contrainedTextSize, .natural, nil, UIEdgeInsets()) - let (textLayout, textApply) = textNodeLayout(NSAttributedString(string: textString, font: textFont, textColor: textMedia ? titleColor : textColor), nil, 1, .end, contrainedTextSize, .natural, nil, UIEdgeInsets()) + let (titleLayout, titleApply) = titleNodeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: titleString, font: titleFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: contrainedTextSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (textLayout, textApply) = textNodeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: textString, font: textFont, textColor: textColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: contrainedTextSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let size = CGSize(width: max(titleLayout.size.width, textLayout.size.width) + leftInset, height: titleLayout.size.height + textLayout.size.height) @@ -222,7 +175,7 @@ class ChatMessageReplyInfoNode: ASDisplayNode { imageNode.frame = CGRect(origin: CGPoint(x: 8.0, y: 3.0), size: CGSize(width: 30.0, height: 30.0)) if let updateImageSignal = updateImageSignal { - imageNode.setSignal(account: account, signal: updateImageSignal) + imageNode.setSignal(updateImageSignal) } } else if let imageNode = node.imageNode { imageNode.removeFromSupernode() @@ -251,8 +204,8 @@ class ChatMessageReplyInfoNode: ASDisplayNode { titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: titleLayout.size) textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: titleLayout.size.height), size: textLayout.size) - node.lineNode.backgroundColor = lineColor - node.lineNode.frame = CGRect(origin: CGPoint(x: 1.0, y: 3.0), size: CGSize(width: 2.0, height: size.height - 4.0)) + node.lineNode.image = lineImage + node.lineNode.frame = CGRect(origin: CGPoint(x: 1.0, y: 3.0), size: CGSize(width: 2.0, height: max(0.0, size.height - 4.0))) node.contentNode.frame = CGRect(origin: CGPoint(), size: size) diff --git a/TelegramUI/ChatMessageSelectionInputPanelNode.swift b/TelegramUI/ChatMessageSelectionInputPanelNode.swift index cb67916c3f..da0472e3c3 100644 --- a/TelegramUI/ChatMessageSelectionInputPanelNode.swift +++ b/TelegramUI/ChatMessageSelectionInputPanelNode.swift @@ -3,19 +3,41 @@ import AsyncDisplayKit import Display import Postbox import TelegramCore +import SwiftSignalKit final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { private let deleteButton: UIButton private let forwardButton: UIButton + private let shareButton: UIButton private var presentationInterfaceState: ChatPresentationInterfaceState? private var theme: PresentationTheme - var selectedMessageCount: Int = 0 { + private let canDeleteMessagesDisposable = MetaDisposable() + + var selectedMessages = Set() { didSet { - self.deleteButton.isEnabled = self.selectedMessageCount != 0 - self.forwardButton.isEnabled = self.selectedMessageCount != 0 + if oldValue != self.selectedMessages { + self.forwardButton.isEnabled = self.selectedMessages.count != 0 + + let transition: ContainedViewLayoutTransition = .animated(duration: 0.15, curve: .easeInOut) + + if self.selectedMessages.isEmpty { + self.canDeleteMessagesDisposable.set(nil) + self.deleteButton.isEnabled = false + self.shareButton.isEnabled = false + } else if let account = self.account { + let isEmpty = self.selectedMessages.isEmpty + self.canDeleteMessagesDisposable.set((chatDeleteMessagesOptions(postbox: account.postbox, accountPeerId: account.peerId, messageIds: self.selectedMessages) + |> deliverOnMainQueue).start(next: { [weak self] options in + if let strongSelf = self { + strongSelf.deleteButton.isEnabled = !options.isEmpty + strongSelf.shareButton.isEnabled = !isEmpty + } + })) + } + } } } @@ -23,23 +45,33 @@ final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { self.theme = theme self.deleteButton = UIButton() + self.deleteButton.isEnabled = false self.forwardButton = UIButton() + self.shareButton = UIButton() + self.shareButton.isEnabled = false self.deleteButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Acessory Panels/MessageSelectionThrash"), color: theme.chat.inputPanel.panelControlAccentColor), for: [.normal]) self.deleteButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Acessory Panels/MessageSelectionThrash"), color: theme.chat.inputPanel.panelControlDisabledColor), for: [.disabled]) self.forwardButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Acessory Panels/MessageSelectionForward"), color: theme.chat.inputPanel.panelControlAccentColor), for: [.normal]) self.forwardButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Acessory Panels/MessageSelectionForward"), color: theme.chat.inputPanel.panelControlDisabledColor), for: [.disabled]) + self.shareButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat List/NavigationShare"), color: theme.chat.inputPanel.panelControlAccentColor), for: [.normal]) + self.shareButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat List/NavigationShare"), color: theme.chat.inputPanel.panelControlDisabledColor), for: [.disabled]) super.init() self.view.addSubview(self.deleteButton) self.view.addSubview(self.forwardButton) + self.view.addSubview(self.shareButton) self.forwardButton.isEnabled = false - self.deleteButton.isEnabled = false self.deleteButton.addTarget(self, action: #selector(self.deleteButtonPressed), for: [.touchUpInside]) self.forwardButton.addTarget(self, action: #selector(self.forwardButtonPressed), for: [.touchUpInside]) + self.shareButton.addTarget(self, action: #selector(self.shareButtonPressed), for: [.touchUpInside]) + } + + deinit { + self.canDeleteMessagesDisposable.dispose() } func updateTheme(theme: PresentationTheme) { @@ -61,26 +93,18 @@ final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { self.interfaceInteraction?.forwardSelectedMessages() } - override func updateLayout(width: CGFloat, maxHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { + @objc func shareButtonPressed() { + self.interfaceInteraction?.shareSelectedMessages() + } + + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { if self.presentationInterfaceState != interfaceState { self.presentationInterfaceState = interfaceState - - var canDelete = false - if let channel = interfaceState.peer as? TelegramChannel { - switch channel.info { - case .broadcast: - canDelete = channel.hasAdminRights(.canDeleteMessages) - case .group: - canDelete = channel.hasAdminRights(.canDeleteMessages) - } - } else { - canDelete = true - } - self.deleteButton.isHidden = !canDelete } - self.deleteButton.frame = CGRect(origin: CGPoint(), size: CGSize(width: 53.0, height: 47.0)) - self.forwardButton.frame = CGRect(origin: CGPoint(x: width - 57.0, y: 0.0), size: CGSize(width: 57.0, height: 47.0)) + self.deleteButton.frame = CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: 53.0, height: 47.0)) + self.forwardButton.frame = CGRect(origin: CGPoint(x: width - rightInset - 57.0, y: 0.0), size: CGSize(width: 57.0, height: 47.0)) + self.shareButton.frame = CGRect(origin: CGPoint(x: floor((width - rightInset - 57.0) / 2.0), y: 0.0), size: CGSize(width: 57.0, height: 47.0)) return 47.0 } diff --git a/TelegramUI/ChatMessageSelectionNode.swift b/TelegramUI/ChatMessageSelectionNode.swift index d18d8ceb36..3d25315934 100644 --- a/TelegramUI/ChatMessageSelectionNode.swift +++ b/TelegramUI/ChatMessageSelectionNode.swift @@ -1,25 +1,18 @@ import Foundation import AsyncDisplayKit -private let checkedImage = UIImage(bundleImageName: "Chat/Message/SelectionChecked")?.precomposed() -private let uncheckedImage = UIImage(bundleImageName: "Chat/Message/SelectionUnchecked")?.precomposed() - final class ChatMessageSelectionNode: ASDisplayNode { - private let toggle: () -> Void + private let toggle: (Bool) -> Void private var selected = false - private let checkNode: ASImageNode + private let checkNode: CheckNode - init(toggle: @escaping () -> Void) { + init(theme: PresentationTheme, toggle: @escaping (Bool) -> Void) { self.toggle = toggle - self.checkNode = ASImageNode() - self.checkNode.displaysAsynchronously = false - self.checkNode.displayWithoutProcessing = true - self.checkNode.isLayerBacked = true + self.checkNode = CheckNode(strokeColor: theme.list.itemCheckColors.strokeColor, fillColor: theme.list.itemCheckColors.fillColor, foregroundColor: theme.list.itemCheckColors.foregroundColor, style: .overlay) super.init() - self.checkNode.image = uncheckedImage self.addSubnode(self.checkNode) self.hitTestSlop = UIEdgeInsetsMake(0.0, 42.0, 0.0, 0.0) @@ -34,23 +27,20 @@ final class ChatMessageSelectionNode: ASDisplayNode { func updateSelected(_ selected: Bool, animated: Bool) { if self.selected != selected { self.selected = selected - self.checkNode.image = selected ? checkedImage : uncheckedImage - if animated { - self.checkNode.layer.animateSpring(from: 0.8 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4) - } + self.checkNode.setIsChecked(selected, animated: animated) } } @objc func tapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { - self.toggle() + self.toggle(!self.selected) } } override func layout() { super.layout() - let checkSize = self.checkNode.measure(CGSize(width: 200.0, height: 200.0)) + let checkSize = CGSize(width: 32.0, height: 32.0) self.checkNode.frame = CGRect(origin: CGPoint(x: 4.0, y: floor((self.bounds.size.height - checkSize.height) / 2.0)), size: checkSize) } } diff --git a/TelegramUI/ChatMessageStickerItemNode.swift b/TelegramUI/ChatMessageStickerItemNode.swift index 8c7127b220..6987c498ee 100644 --- a/TelegramUI/ChatMessageStickerItemNode.swift +++ b/TelegramUI/ChatMessageStickerItemNode.swift @@ -8,7 +8,9 @@ import TelegramCore class ChatMessageStickerItemNode: ChatMessageItemView { let imageNode: TransformImageNode var progressNode: RadialProgressNode? - var tapRecognizer: UITapGestureRecognizer? + + private var swipeToReplyNode: ChatMessageSwipeToReplyNode? + private var swipeToReplyFeedback: HapticFeedback? private var selectionNode: ChatMessageSelectionNode? @@ -22,6 +24,8 @@ class ChatMessageStickerItemNode: ChatMessageItemView { private var highlightedState: Bool = false + private var currentSwipeToReplyTranslation: CGFloat = 0.0 + required init() { self.imageNode = TransformImageNode() self.dateAndStatusNode = ChatMessageDateAndStatusNode() @@ -49,6 +53,18 @@ class ChatMessageStickerItemNode: ChatMessageItemView { return .waitForSingleTap } self.view.addGestureRecognizer(recognizer) + + let replyRecognizer = ChatSwipeToReplyRecognizer(target: self, action: #selector(self.swipeToReplyGesture(_:))) + replyRecognizer.shouldBegin = { [weak self] in + if let strongSelf = self, let item = strongSelf.item { + if strongSelf.selectionNode != nil { + return false + } + return item.controllerInteraction.canSetupReply() + } + return false + } + self.view.addGestureRecognizer(replyRecognizer) } override func setupItem(_ item: ChatMessageItem) { @@ -60,7 +76,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { self.telegramFile = telegramFile let signal = chatMessageSticker(account: item.account, file: telegramFile, small: false) - self.imageNode.setSignal(account: item.account, signal: signal) + self.imageNode.setSignal(signal) self.fetchDisposable.set(freeMediaFileInteractiveFetched(account: item.account, file: telegramFile).start()) } @@ -69,7 +85,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } } - override func asyncLayout() -> (_ item: ChatMessageItem, _ width: CGFloat, _ mergedTop: Bool, _ mergedBottom: Bool, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { + override func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { let displaySize = CGSize(width: 200.0, height: 200.0) let telegramFile = self.telegramFile let layoutConstants = self.layoutConstants @@ -79,8 +95,8 @@ class ChatMessageStickerItemNode: ChatMessageItemView { let makeReplyInfoLayout = ChatMessageReplyInfoNode.asyncLayout(self.replyInfoNode) let currentReplyBackgroundNode = self.replyBackgroundNode - return { item, width, mergedTop, mergedBottom, dateHeaderAtBottom in - let incoming = item.message.effectivelyIncoming + return { item, params, mergedTop, mergedBottom, dateHeaderAtBottom in + let incoming = item.message.effectivelyIncoming(item.account.peerId) var imageSize: CGSize = CGSize(width: 100.0, height: 100.0) if let telegramFile = telegramFile { if let dimensions = telegramFile.dimensions { @@ -93,15 +109,20 @@ class ChatMessageStickerItemNode: ChatMessageItemView { let avatarInset: CGFloat var hasAvatar = false - if item.peerId.isGroupOrChannel && item.message.author != nil { - var isBroadcastChannel = false - if let peer = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { - isBroadcastChannel = true - } - - if !isBroadcastChannel { + switch item.chatLocation { + case let .peer(peerId): + if peerId.isGroupOrChannel && item.message.author != nil { + var isBroadcastChannel = false + if let peer = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { + isBroadcastChannel = true + } + + if !isBroadcastChannel { + hasAvatar = true + } + } + case .group: hasAvatar = true - } } if hasAvatar { @@ -115,16 +136,16 @@ class ChatMessageStickerItemNode: ChatMessageItemView { layoutInsets.top += layoutConstants.timestampHeaderHeight } - let displayLeftInset = layoutConstants.bubble.edgeInset + avatarInset + let displayLeftInset = params.leftInset + layoutConstants.bubble.edgeInset + avatarInset - let imageFrame = CGRect(origin: CGPoint(x: (incoming ? (layoutConstants.bubble.edgeInset + avatarInset + layoutConstants.bubble.contentInsets.left) : (width - imageSize.width - layoutConstants.bubble.edgeInset - layoutConstants.bubble.contentInsets.left)), y: 0.0), size: imageSize) + let imageFrame = CGRect(origin: CGPoint(x: (incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + avatarInset + layoutConstants.bubble.contentInsets.left) : (params.width - params.rightInset - imageSize.width - layoutConstants.bubble.edgeInset - layoutConstants.bubble.contentInsets.left)), y: 0.0), size: imageSize) let arguments = TransformImageArguments(corners: ImageCorners(), imageSize: imageFrame.size, boundingSize: imageFrame.size, intrinsicInsets: UIEdgeInsets()) let imageApply = imageLayout(arguments) let statusType: ChatMessageDateAndStatusType - if item.message.effectivelyIncoming { + if item.message.effectivelyIncoming(item.account.peerId) { statusType = .FreeIncoming } else { if item.message.flags.contains(.Failed) { @@ -136,10 +157,6 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } } - var t = Int(item.message.timestamp) - var timeinfo = tm() - localtime_r(&t, &timeinfo) - var edited = false var sentViaBot = false var viewCount: Int? @@ -153,7 +170,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } } - var dateText = String(format: "%02d:%02d", arguments: [Int(timeinfo.tm_hour), Int(timeinfo.tm_min)]) + var dateText = stringForMessageTimestamp(timestamp: item.message.timestamp, timeFormat: item.presentationData.timeFormat) if let author = item.message.author as? TelegramUser { if author.botInfo != nil { @@ -164,34 +181,38 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } } - let (dateAndStatusSize, dateAndStatusApply) = makeDateAndStatusLayout(item.theme, edited && !sentViaBot, viewCount, dateText, statusType, CGSize(width: width, height: CGFloat.greatestFiniteMagnitude)) + let (dateAndStatusSize, dateAndStatusApply) = makeDateAndStatusLayout(item.presentationData.theme, item.presentationData.strings, edited && !sentViaBot, viewCount, dateText, statusType, CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude)) var replyInfoApply: (CGSize, () -> ChatMessageReplyInfoNode)? var updatedReplyBackgroundNode: ASImageNode? var replyBackgroundImage: UIImage? for attribute in item.message.attributes { if let replyAttribute = attribute as? ReplyMessageAttribute, let replyMessage = item.message.associatedMessages[replyAttribute.messageId] { - let availableWidth = max(60.0, width - imageSize.width - 20.0 - layoutConstants.bubble.edgeInset * 2.0 - avatarInset - layoutConstants.bubble.contentInsets.left) - replyInfoApply = makeReplyInfoLayout(item.theme, item.account, .standalone, replyMessage, CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude)) + let availableWidth = max(60.0, params.width - params.leftInset - params.rightInset - imageSize.width - 20.0 - layoutConstants.bubble.edgeInset * 2.0 - avatarInset - layoutConstants.bubble.contentInsets.left) + replyInfoApply = makeReplyInfoLayout(item.presentationData.theme, item.presentationData.strings, item.account, .standalone, replyMessage, CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude)) if let currentReplyBackgroundNode = currentReplyBackgroundNode { updatedReplyBackgroundNode = currentReplyBackgroundNode } else { updatedReplyBackgroundNode = ASImageNode() } - replyBackgroundImage = PresentationResourcesChat.chatFreeformContentAdditionalInfoBackgroundImage(item.theme) + replyBackgroundImage = PresentationResourcesChat.chatFreeformContentAdditionalInfoBackgroundImage(item.presentationData.theme) break } } - return (ListViewItemNodeLayout(contentSize: CGSize(width: width, height: imageSize.height), insets: layoutInsets), { [weak self] animation in + let contentHeight = max(imageSize.height, layoutConstants.image.minDimensions.height) + + return (ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: contentHeight), insets: layoutInsets), { [weak self] animation in if let strongSelf = self { - strongSelf.imageNode.frame = imageFrame + let updatedImageFrame = imageFrame.offsetBy(dx: 0.0, dy: floor((contentHeight - imageSize.height) / 2.0)) + + strongSelf.imageNode.frame = updatedImageFrame strongSelf.progressNode?.position = strongSelf.imageNode.position imageApply() dateAndStatusApply(false) - strongSelf.dateAndStatusNode.frame = CGRect(origin: CGPoint(x: max(displayLeftInset, imageFrame.maxX - dateAndStatusSize.width - 4.0), y: imageFrame.maxY - dateAndStatusSize.height), size: dateAndStatusSize) + strongSelf.dateAndStatusNode.frame = CGRect(origin: CGPoint(x: max(displayLeftInset, updatedImageFrame.maxX - dateAndStatusSize.width - 4.0), y: updatedImageFrame.maxY - dateAndStatusSize.height), size: dateAndStatusSize) if let updatedReplyBackgroundNode = updatedReplyBackgroundNode { if strongSelf.replyBackgroundNode == nil { @@ -210,7 +231,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { strongSelf.replyInfoNode = replyInfoNode strongSelf.addSubnode(replyInfoNode) } - let replyInfoFrame = CGRect(origin: CGPoint(x: (!incoming ? (layoutConstants.bubble.edgeInset + 10.0) : (width - replyInfoSize.width - layoutConstants.bubble.edgeInset - 10.0)), y: imageSize.height - replyInfoSize.height - 8.0), size: replyInfoSize) + let replyInfoFrame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 10.0) : (params.width - params.rightInset - replyInfoSize.width - layoutConstants.bubble.edgeInset - 10.0)), y: imageSize.height - replyInfoSize.height - 8.0), size: replyInfoSize) replyInfoNode.frame = replyInfoFrame strongSelf.replyBackgroundNode?.frame = CGRect(origin: CGPoint(x: replyInfoFrame.minX - 4.0, y: replyInfoFrame.minY - 2.0), size: CGSize(width: replyInfoFrame.size.width + 8.0, height: replyInfoFrame.size.height + 5.0)) } else if let replyInfoNode = strongSelf.replyInfoNode { @@ -222,18 +243,6 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { - super.animateInsertion(currentTimestamp, duration: duration, short: short) - - self.imageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - } - - override func animateAdded(_ currentTimestamp: Double, duration: Double) { - super.animateAdded(currentTimestamp, duration: duration) - - self.imageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - } - @objc func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { switch recognizer.state { case .ended: @@ -242,7 +251,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { case .tap: if let avatarNode = self.accessoryItemNode as? ChatMessageAvatarAccessoryItemNode, avatarNode.frame.contains(location) { if let item = self.item, let author = item.message.author { - self.controllerInteraction?.openPeer(author.id, .info, item.message.id) + item.controllerInteraction.openPeer(author.id, .info, item.message.id) } return } @@ -263,7 +272,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { if let item = self.item { for attribute in item.message.attributes { if let attribute = attribute as? ReplyMessageAttribute { - self.controllerInteraction?.navigateToMessage(item.message.id, attribute.messageId) + item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId) return } } @@ -282,14 +291,14 @@ class ChatMessageStickerItemNode: ChatMessageItemView { }*/ if let item = self.item, self.imageNode.frame.contains(location) { - self.controllerInteraction?.openMessage(item.message.id) + item.controllerInteraction.openMessage(item.message.id) return } - self.controllerInteraction?.clickThroughMessage() + self.item?.controllerInteraction.clickThroughMessage() case .longTap, .doubleTap: if let item = self.item, self.imageNode.frame.contains(location) { - self.controllerInteraction?.openMessageContextMenu(item.message.id, self, self.imageNode.frame) + item.controllerInteraction.openMessageContextMenu(item.message.id, self, self.imageNode.frame) } case .hold: break @@ -300,22 +309,82 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } } + @objc func swipeToReplyGesture(_ recognizer: ChatSwipeToReplyRecognizer) { + switch recognizer.state { + case .began: + self.currentSwipeToReplyTranslation = 0.0 + if self.swipeToReplyFeedback == nil { + self.swipeToReplyFeedback = HapticFeedback() + self.swipeToReplyFeedback?.prepareImpact() + } + (self.view.window as? WindowHost)?.cancelInteractiveKeyboardGestures() + case .changed: + let translation = recognizer.translation(in: self.view) + var animateReplyNodeIn = false + if (translation.x < -45.0) != (self.currentSwipeToReplyTranslation < -45.0) { + if translation.x < -45.0, self.swipeToReplyNode == nil, let item = self.item { + self.swipeToReplyFeedback?.impact() + + let swipeToReplyNode = ChatMessageSwipeToReplyNode(fillColor: item.presentationData.theme.chat.bubble.shareButtonFillColor, strokeColor: item.presentationData.theme.chat.bubble.shareButtonStrokeColor, foregroundColor: item.presentationData.theme.chat.bubble.shareButtonForegroundColor) + self.swipeToReplyNode = swipeToReplyNode + self.addSubnode(swipeToReplyNode) + animateReplyNodeIn = true + } + } + self.currentSwipeToReplyTranslation = translation.x + var bounds = self.bounds + bounds.origin.x = -translation.x + self.bounds = bounds + + if let swipeToReplyNode = self.swipeToReplyNode { + swipeToReplyNode.frame = CGRect(origin: CGPoint(x: bounds.size.width, y: floor((self.contentSize.height - 33.0) / 2.0)), size: CGSize(width: 33.0, height: 33.0)) + if animateReplyNodeIn { + swipeToReplyNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.12) + swipeToReplyNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4) + } + } + case .cancelled, .ended: + self.swipeToReplyFeedback = nil + + let translation = recognizer.translation(in: self.view) + if case .ended = recognizer.state, translation.x < -45.0 { + if let item = self.item { + item.controllerInteraction.setupReply(item.message.id) + } + } + var bounds = self.bounds + let previousBounds = bounds + bounds.origin.x = 0.0 + self.bounds = bounds + self.layer.animateBounds(from: previousBounds, to: bounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + if let swipeToReplyNode = self.swipeToReplyNode { + self.swipeToReplyNode = nil + swipeToReplyNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak swipeToReplyNode] _ in + swipeToReplyNode?.removeFromSupernode() + }) + swipeToReplyNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + } + default: + break + } + } + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { return super.hitTest(point, with: event) } override func updateSelectionState(animated: Bool) { - guard let controllerInteraction = self.controllerInteraction else { + guard let item = self.item else { return } - if let selectionState = controllerInteraction.selectionState { + if let selectionState = item.controllerInteraction.selectionState { var selected = false var incoming = true - if let item = self.item { - selected = selectionState.selectedIds.contains(item.message.id) - incoming = item.message.effectivelyIncoming - } + + selected = selectionState.selectedIds.contains(item.message.id) + incoming = item.message.effectivelyIncoming(item.account.peerId) + let offset: CGFloat = incoming ? 42.0 : 0.0 if let selectionNode = self.selectionNode { @@ -323,9 +392,9 @@ class ChatMessageStickerItemNode: ChatMessageItemView { selectionNode.frame = CGRect(origin: CGPoint(x: -offset, y: 0.0), size: CGSize(width: self.contentBounds.size.width, height: self.contentBounds.size.height)) self.subnodeTransform = CATransform3DMakeTranslation(offset, 0.0, 0.0); } else { - let selectionNode = ChatMessageSelectionNode(toggle: { [weak self] in + let selectionNode = ChatMessageSelectionNode(theme: item.presentationData.theme, toggle: { [weak self] value in if let strongSelf = self, let item = strongSelf.item { - strongSelf.controllerInteraction?.toggleMessageSelection(item.message.id) + item.controllerInteraction.toggleMessagesSelection([item.message.id], value) } }) @@ -367,9 +436,9 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } override func updateHighlightedState(animated: Bool) { - if let controllerInteraction = self.controllerInteraction, let item = self.item { + if let item = self.item { var highlighted = false - if let highlightedState = controllerInteraction.highlightedState { + if let highlightedState = item.controllerInteraction.highlightedState { if highlightedState.messageStableId == item.message.stableId { highlighted = true } @@ -386,4 +455,22 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } } } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + super.animateInsertion(currentTimestamp, duration: duration, short: short) + + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + super.animateRemoved(currentTimestamp, duration: duration) + + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + } + + override func animateAdded(_ currentTimestamp: Double, duration: Double) { + super.animateAdded(currentTimestamp, duration: duration) + + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } } diff --git a/TelegramUI/ChatMessageSwipeToReplyNode.swift b/TelegramUI/ChatMessageSwipeToReplyNode.swift new file mode 100644 index 0000000000..ae6800dfa3 --- /dev/null +++ b/TelegramUI/ChatMessageSwipeToReplyNode.swift @@ -0,0 +1,43 @@ +import Foundation +import Display +import AsyncDisplayKit + +final class ChatMessageSwipeToReplyNode: ASDisplayNode { + private let backgroundNode: ASImageNode + + init(fillColor: UIColor, strokeColor: UIColor, foregroundColor: UIColor) { + self.backgroundNode = ASImageNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.image = generateImage(CGSize(width: 33.0, height: 33.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(fillColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + + let lineWidth: CGFloat = 1.0 + let halfLineWidth = lineWidth / 2.0 + var strokeAlpha: CGFloat = 0.0 + strokeColor.getRed(nil, green: nil, blue: nil, alpha: &strokeAlpha) + if !strokeAlpha.isZero { + context.setStrokeColor(strokeColor.cgColor) + context.setLineWidth(lineWidth) + context.strokeEllipse(in: CGRect(origin: CGPoint(x: halfLineWidth, y: halfLineWidth), size: CGSize(width: size.width - lineWidth, height: size.width - lineWidth))) + } + + if let image = UIImage(bundleImageName: "Chat/Message/ShareIcon") { + let imageRect = CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size) + + context.translateBy(x: imageRect.midX, y: imageRect.midY) + context.scaleBy(x: -1.0, y: -1.0) + context.translateBy(x: -imageRect.midX, y: -imageRect.midY) + context.clip(to: imageRect, mask: image.cgImage!) + context.setFillColor(foregroundColor.cgColor) + context.fill(imageRect) + } + }) + + super.init() + + self.addSubnode(self.backgroundNode) + self.backgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 33.0, height: 33.0)) + } +} diff --git a/TelegramUI/ChatMessageTextBubbleContentNode.swift b/TelegramUI/ChatMessageTextBubbleContentNode.swift index c223c8f8d4..8f26f7a521 100644 --- a/TelegramUI/ChatMessageTextBubbleContentNode.swift +++ b/TelegramUI/ChatMessageTextBubbleContentNode.swift @@ -4,16 +4,38 @@ import Display import TelegramCore import Postbox -private let messageFont: UIFont = UIFont.systemFont(ofSize: 17.0) -private let messageBoldFont: UIFont = UIFont.boldSystemFont(ofSize: 17.0) -private let messageFixedFont: UIFont = UIFont(name: "Menlo-Regular", size: 16.0) ?? UIFont.systemFont(ofSize: 17.0) +private final class CachedChatMessageText { + let text: String + let inputEntities: [MessageTextEntity]? + let entities: [MessageTextEntity]? + + init(text: String, inputEntities: [MessageTextEntity]?, entities: [MessageTextEntity]?) { + self.text = text + self.inputEntities = inputEntities + self.entities = entities + } + + func matches(text: String, inputEntities: [MessageTextEntity]?) -> Bool { + if self.text != text { + return false + } + if let current = self.inputEntities, let inputEntities = inputEntities { + if current != inputEntities { + return false + } + } else if (self.inputEntities != nil) != (inputEntities != nil) { + return false + } + return true + } +} class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { private let textNode: TextNode private let statusNode: ChatMessageDateAndStatusNode private var linkHighlightingNode: LinkHighlightingNode? - private var item: ChatMessageItem? + private var cachedChatMessageText: CachedChatMessageText? required init() { self.textNode = TextNode() @@ -32,23 +54,23 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { fatalError("init(coder:) has not been implemented") } - override func asyncLayoutContent() -> (_ item: ChatMessageItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ position: ChatMessageBubbleContentPosition, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))) { + override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))) { let textLayout = TextNode.asyncLayout(self.textNode) let statusLayout = self.statusNode.asyncLayout() - return { item, layoutConstants, position, _ in - return (CGFloat.greatestFiniteMagnitude, { constrainedSize in + let currentCachedChatMessageText = self.cachedChatMessageText + + return { item, layoutConstants, _, _, _ in + let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackgroundForEmptyWallpapers: false, forceFullCorners: false) + + return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in let message = item.message - let incoming = item.message.effectivelyIncoming + let incoming = item.message.effectivelyIncoming(item.account.peerId) let horizontalInset = layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right let textConstrainedSize = CGSize(width: constrainedSize.width - horizontalInset, height: constrainedSize.height) - var t = Int(item.message.timestamp) - var timeinfo = tm() - localtime_r(&t, &timeinfo) - var edited = false var sentViaBot = false var viewCount: Int? @@ -62,7 +84,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } } - var dateText = String(format: "%02d:%02d", arguments: [Int(timeinfo.tm_hour), Int(timeinfo.tm_min)]) + var dateText = stringForMessageTimestamp(timestamp: item.message.timestamp, timeFormat: item.presentationData.timeFormat) if let author = item.message.author as? TelegramUser { if author.botInfo != nil { @@ -74,64 +96,80 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } let statusType: ChatMessageDateAndStatusType? - if case .None = position.bottom { - if incoming { - statusType = .BubbleIncoming - } else { - if message.flags.contains(.Failed) { - statusType = .BubbleOutgoing(.Failed) - } else if message.flags.isSending { - statusType = .BubbleOutgoing(.Sending) + switch position { + case .linear(_, .None): + if incoming { + statusType = .BubbleIncoming } else { - statusType = .BubbleOutgoing(.Sent(read: item.read)) + if message.flags.contains(.Failed) { + statusType = .BubbleOutgoing(.Failed) + } else if message.flags.isSending { + statusType = .BubbleOutgoing(.Sending) + } else { + statusType = .BubbleOutgoing(.Sent(read: item.read)) + } } - } - } else { - statusType = nil + default: + statusType = nil } var statusSize: CGSize? var statusApply: ((Bool) -> Void)? if let statusType = statusType { - let (size, apply) = statusLayout(item.theme, edited && !sentViaBot, viewCount, dateText, statusType, textConstrainedSize) + let (size, apply) = statusLayout(item.presentationData.theme, item.presentationData.strings, edited && !sentViaBot, viewCount, dateText, statusType, textConstrainedSize) statusSize = size statusApply = apply } let attributedText: NSAttributedString - var entities: TextEntitiesMessageAttribute? + var messageEntities: [MessageTextEntity]? for attribute in item.message.attributes { if let attribute = attribute as? TextEntitiesMessageAttribute { - entities = attribute + messageEntities = attribute.entities break } } - if entities == nil { - var generateEntities = false - for media in message.media { - if media is TelegramMediaImage || media is TelegramMediaFile { - generateEntities = true - break - } - } - if generateEntities { - let parsedEntities = generateTextEntities(message.text) - if !parsedEntities.isEmpty { - entities = TextEntitiesMessageAttribute(entities: parsedEntities) + + var entities: [MessageTextEntity]? + + var updatedCachedChatMessageText: CachedChatMessageText? + if let cached = currentCachedChatMessageText, cached.matches(text: message.text, inputEntities: messageEntities) { + entities = cached.entities + } else { + entities = messageEntities + if let entitiesValue = entities { + if let result = addLocallyGeneratedEntities(message.text, enabledTypes: .all, entities: entitiesValue) { + entities = result + } + } else { + var generateEntities = false + for media in message.media { + if media is TelegramMediaImage || media is TelegramMediaFile { + generateEntities = true + break + } + } + if generateEntities { + let parsedEntities = generateTextEntities(message.text, enabledTypes: .all) + if !parsedEntities.isEmpty { + entities = parsedEntities + } } } + updatedCachedChatMessageText = CachedChatMessageText(text: message.text, inputEntities: messageEntities, entities: entities) } - let bubbleTheme = item.theme.chat.bubble + + let bubbleTheme = item.presentationData.theme.chat.bubble if let entities = entities { - attributedText = stringWithAppliedEntities(message.text, entities: entities.entities, baseColor: incoming ? bubbleTheme.incomingPrimaryTextColor : bubbleTheme.outgoingPrimaryTextColor, linkColor: incoming ? bubbleTheme.incomingLinkTextColor : bubbleTheme.outgoingLinkTextColor, baseFont: messageFont, boldFont: messageBoldFont, fixedFont: messageFixedFont) + attributedText = stringWithAppliedEntities(message.text, entities: entities, baseColor: incoming ? bubbleTheme.incomingPrimaryTextColor : bubbleTheme.outgoingPrimaryTextColor, linkColor: incoming ? bubbleTheme.incomingLinkTextColor : bubbleTheme.outgoingLinkTextColor, baseFont: item.presentationData.messageFont, boldFont: item.presentationData.messageBoldFont, fixedFont: item.presentationData.messageFixedFont) } else { - attributedText = NSAttributedString(string: message.text, font: messageFont, textColor: incoming ? bubbleTheme.incomingPrimaryTextColor : bubbleTheme.outgoingPrimaryTextColor) + attributedText = NSAttributedString(string: message.text, font: item.presentationData.messageFont, textColor: incoming ? bubbleTheme.incomingPrimaryTextColor : bubbleTheme.outgoingPrimaryTextColor) } - let (textLayout, textApply) = textLayout(attributedText, nil, 0, .end, textConstrainedSize, .natural, nil, UIEdgeInsets()) + let (textLayout, textApply) = textLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) var textFrame = CGRect(origin: CGPoint(), size: textLayout.size) let textSize = textLayout.size @@ -172,6 +210,10 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { return (boundingSize, { [weak self] animation in if let strongSelf = self { strongSelf.item = item + if let updatedCachedChatMessageText = updatedCachedChatMessageText { + strongSelf.cachedChatMessageText = updatedCachedChatMessageText + } + let cachedLayout = strongSelf.textNode.cachedLayout if case .System = animation { @@ -288,7 +330,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { if let current = self.linkHighlightingNode { linkHighlightingNode = current } else { - linkHighlightingNode = LinkHighlightingNode(color: item.message.effectivelyIncoming ? item.theme.chat.bubble.incomingLinkHighlightColor : item.theme.chat.bubble.outgoingLinkHighlightColor) + linkHighlightingNode = LinkHighlightingNode(color: item.message.effectivelyIncoming(item.account.peerId) ? item.presentationData.theme.chat.bubble.incomingLinkHighlightColor : item.presentationData.theme.chat.bubble.outgoingLinkHighlightColor) self.linkHighlightingNode = linkHighlightingNode self.insertSubnode(linkHighlightingNode, belowSubnode: self.textNode) } diff --git a/TelegramUI/ChatMessageWebpageBubbleContentNode.swift b/TelegramUI/ChatMessageWebpageBubbleContentNode.swift index e47dcd9809..3b0df855c7 100644 --- a/TelegramUI/ChatMessageWebpageBubbleContentNode.swift +++ b/TelegramUI/ChatMessageWebpageBubbleContentNode.swift @@ -5,16 +5,89 @@ import AsyncDisplayKit import SwiftSignalKit import TelegramCore +enum WebsiteType { + case generic + case twitter + case instagram +} + +func websiteType(of webpage: TelegramMediaWebpageLoadedContent) -> WebsiteType { + if let websiteName = webpage.websiteName?.lowercased() { + if websiteName == "twitter" { + return .twitter + } else if websiteName == "instagram" { + return .instagram + } + } + return .generic +} + +func instantPageGalleryMedia(webpageId: MediaId, page: InstantPage, galleryMedia: Media) -> [InstantPageGalleryEntry] { + var result: [InstantPageGalleryEntry] = [] + var counter: Int = 0 + + for block in page.blocks { + result.append(contentsOf: instantPageBlockMedia(pageId: webpageId, block: block, media: page.media, counter: &counter)) + } + + var found = false + for item in result { + if item.media.media.id == galleryMedia.id { + found = true + break + } + } + + if !found { + result.insert(InstantPageGalleryEntry(index: Int32(counter), pageId: webpageId, media: InstantPageMedia(index: counter, media: galleryMedia, caption: ""), caption: "", location: InstantPageGalleryEntryLocation(position: Int32(counter), totalCount: 0)), at: 0) + } + + for i in 0 ..< result.count { + let item = result[i] + result[i] = InstantPageGalleryEntry(index: Int32(i), pageId: item.pageId, media: item.media, caption: item.caption, location: InstantPageGalleryEntryLocation(position: Int32(i), totalCount: Int32(result.count))) + } + return result +} + +private func instantPageBlockMedia(pageId: MediaId, block: InstantPageBlock, media: [MediaId: Media], counter: inout Int) -> [InstantPageGalleryEntry] { + switch block { + case let .image(id, caption): + if let m = media[id] { + let captionText = caption.plainText + let result = [InstantPageGalleryEntry(index: Int32(counter), pageId: pageId, media: InstantPageMedia(index: counter, media: m, caption: captionText), caption: captionText, location: InstantPageGalleryEntryLocation(position: Int32(counter), totalCount: 0))] + counter += 1 + return result + } + case let .video(id, caption, _, loop): + if let m = media[id] { + let captionText = caption.plainText + let result = [InstantPageGalleryEntry(index: Int32(counter), pageId: pageId, media: InstantPageMedia(index: counter, media: m, caption: captionText), caption: captionText, location: InstantPageGalleryEntryLocation(position: Int32(counter), totalCount: 0))] + counter += 1 + return result + } + case let .collage(items, _): + var result: [InstantPageGalleryEntry] = [] + for item in items { + result.append(contentsOf: instantPageBlockMedia(pageId: pageId, block: item, media: media, counter: &counter)) + } + return result + case let .slideshow(items, _): + var result: [InstantPageGalleryEntry] = [] + for item in items { + result.append(contentsOf: instantPageBlockMedia(pageId: pageId, block: item, media: media, counter: &counter)) + } + return result + default: + break + } + return [] +} + final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { - private var item: ChatMessageItem? private var webPage: TelegramMediaWebpage? private let contentNode: ChatMessageAttachedContentNode - override var properties: ChatMessageBubbleContentProperties { - return ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0) - } - override var visibility: ListViewItemNodeVisibility { didSet { self.contentNode.visibility = self.visibility @@ -28,12 +101,12 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { self.addSubnode(self.contentNode) self.contentNode.openMedia = { [weak self] in - if let strongSelf = self, let item = strongSelf.item, let controllerInteraction = strongSelf.controllerInteraction { - controllerInteraction.openMessage(item.message.id) + if let strongSelf = self, let item = strongSelf.item { + item.controllerInteraction.openMessage(item.message.id) } } self.contentNode.activateAction = { [weak self] in - if let strongSelf = self, let item = strongSelf.item, let controllerInteraction = strongSelf.controllerInteraction { + if let strongSelf = self, let item = strongSelf.item { var webPageContent: TelegramMediaWebpageLoadedContent? for media in item.message.media { if let media = media as? TelegramMediaWebpage { @@ -44,7 +117,7 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { } } if let webpage = webPageContent { - controllerInteraction.openUrl(webpage.url) + item.controllerInteraction.openUrl(webpage.url) } } } @@ -54,10 +127,10 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { fatalError("init(coder:) has not been implemented") } - override func asyncLayoutContent() -> (_ item: ChatMessageItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ position: ChatMessageBubbleContentPosition, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))) { + override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))) { let contentNodeLayout = self.contentNode.asyncLayout() - return { item, layoutConstants, position, constrainedSize in + return { item, layoutConstants, _, _, constrainedSize in var webPage: TelegramMediaWebpage? var webPageContent: TelegramMediaWebpageLoadedContent? for media in item.message.media { @@ -73,11 +146,14 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { var title: String? var subtitle: String? var text: String? + var entities: [MessageTextEntity]? var mediaAndFlags: (Media, ChatMessageAttachedContentNodeMediaFlags)? var actionIcon: ChatMessageAttachedContentActionIcon? var actionTitle: String? if let webpage = webPageContent { + let type = websiteType(of: webpage) + if let websiteName = webpage.websiteName, !websiteName.isEmpty { title = websiteName } @@ -88,21 +164,40 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { if let textValue = webpage.text, !textValue.isEmpty { text = textValue + var entityTypes: EnabledEntityTypes = [.url] + switch type { + case .twitter, .instagram: + entityTypes.insert(.mention) + default: + break + } + entities = generateTextEntities(textValue, enabledTypes: entityTypes) } - if let file = webpage.file { + var mainMedia: Media? + + switch type { + case .instagram, .twitter: + mainMedia = webpage.image + default: + mainMedia = webpage.file ?? webpage.image + } + + if let file = mainMedia as? TelegramMediaFile { if let image = webpage.image, let embedUrl = webpage.embedUrl, !embedUrl.isEmpty { - mediaAndFlags = (image, []) + mediaAndFlags = (image, [.preferMediaBeforeText]) } else { mediaAndFlags = (file, []) } - } else if let image = webpage.image { + } else if let image = mainMedia as? TelegramMediaImage { if let type = webpage.type, ["photo", "video", "embed", "article"].contains(type) { var flags = ChatMessageAttachedContentNodeMediaFlags() if webpage.instantPage != nil, let largest = largestImageRepresentation(image.representations) { if largest.dimensions.width >= 256.0 { flags.insert(.preferMediaBeforeText) } + } else if let embedUrl = webpage.embedUrl, !embedUrl.isEmpty { + flags.insert(.preferMediaBeforeText) } mediaAndFlags = (image, flags) } else if let _ = largestImageRepresentation(image.representations)?.dimensions { @@ -111,24 +206,33 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { } if let _ = webpage.instantPage { - actionIcon = .instant - actionTitle = item.strings.Conversation_InstantPagePreview + switch type { + case .twitter, .instagram: + break + default: + actionIcon = .instant + actionTitle = item.presentationData.strings.Conversation_InstantPagePreview + } } else if let type = webpage.type { switch type { case "telegram_channel": - actionTitle = item.strings.Conversation_ViewChannel - case "telegram_supergroup": - actionTitle = item.strings.Conversation_ViewGroup + actionTitle = item.presentationData.strings.Conversation_ViewChannel + case "telegram_chat": + actionTitle = item.presentationData.strings.Conversation_ViewGroup + case "telegram_message": + actionTitle = item.presentationData.strings.Conversation_ViewMessage default: break } } } - let (initialWidth, continueLayout) = contentNodeLayout(item.theme, item.strings, item.controllerInteraction.automaticMediaDownloadSettings, item.account, item.message, item.read, title, subtitle, text, nil, mediaAndFlags, actionIcon, actionTitle, true, layoutConstants, position, constrainedSize) + let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.account, item.message, item.read, title, subtitle, text, entities, mediaAndFlags, actionIcon, actionTitle, true, layoutConstants, constrainedSize) - return (initialWidth, { constrainedSize in - let (refinedWidth, finalizeLayout) = continueLayout(constrainedSize) + let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackgroundForEmptyWallpapers: false, forceFullCorners: false) + + return (contentProperties, nil, initialWidth, { constrainedSize, position in + let (refinedWidth, finalizeLayout) = continueLayout(constrainedSize, position) return (refinedWidth, { boundingWidth in let (size, apply) = finalizeLayout(boundingWidth) @@ -166,12 +270,35 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { override func tapActionAtPoint(_ point: CGPoint) -> ChatMessageBubbleContentTapAction { if self.bounds.contains(point) { + let contentNodeFrame = self.contentNode.frame + let result = self.contentNode.tapActionAtPoint(point.offsetBy(dx: -contentNodeFrame.minX, dy: -contentNodeFrame.minY)) + switch result { + case .none: + break + case let .textMention(value): + if let webPage = self.webPage, case let .Loaded(content) = webPage.content { + var mention = value + if mention.hasPrefix("@") { + mention = String(mention[mention.index(after: mention.startIndex)...]) + } + switch websiteType(of: content) { + case .twitter: + return .url("https://twitter.com/\(mention)") + case .instagram: + return .url("https://instagram.com/\(mention)") + default: + break + } + } + default: + return result + } + if let webPage = self.webPage, case let .Loaded(content) = webPage.content { if content.instantPage != nil { return .instantPage } } - let contentNodeFrame = self.contentNode.frame if self.contentNode.hasActionAtPoint(point.offsetBy(dx: -contentNodeFrame.minX, dy: -contentNodeFrame.minY)) { return .ignore } @@ -203,7 +330,11 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { } } - override func transitionNode(media: Media) -> ASDisplayNode? { + override func transitionNode(messageId: MessageId, media: Media) -> ASDisplayNode? { + if self.item?.message.id != messageId { + return nil + } + if let result = self.contentNode.transitionNode(media: media) { return result } @@ -219,4 +350,9 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { } return nil } + + override func updateTouchesAtPoint(_ point: CGPoint?) { + let contentNodeFrame = self.contentNode.frame + self.contentNode.updateTouchesAtPoint(point.flatMap { $0.offsetBy(dx: -contentNodeFrame.minX, dy: -contentNodeFrame.minY) }) + } } diff --git a/TelegramUI/ChatMultipleAvatarsNavigationNode.swift b/TelegramUI/ChatMultipleAvatarsNavigationNode.swift new file mode 100644 index 0000000000..6c5dd8caa3 --- /dev/null +++ b/TelegramUI/ChatMultipleAvatarsNavigationNode.swift @@ -0,0 +1,57 @@ +import Foundation +import AsyncDisplayKit +import Display +import TelegramCore +import Postbox + +final class ChatMultipleAvatarsNavigationNode: ASDisplayNode { + private let multipleAvatarsNode: MultipleAvatarsNode + + private weak var account: Account? + private var peers: [Peer] = [] + + override init() { + self.multipleAvatarsNode = MultipleAvatarsNode() + + super.init() + + self.addSubnode(self.multipleAvatarsNode) + } + + override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { + if constrainedSize.height.isLessThanOrEqualTo(32.0) { + return CGSize(width: 26.0, height: 26.0) + } else { + return CGSize(width: 37.0, height: 37.0) + } + } + + override func layout() { + super.layout() + + let bounds = self.bounds + if let account = self.account, !bounds.width.isZero { + let avatarsLayout = MultipleAvatarsNode.asyncLayout(self.multipleAvatarsNode) + let apply = avatarsLayout(account, self.peers, bounds.size) + let _ = apply(false) + } + if self.bounds.size.height.isLessThanOrEqualTo(26.0) { + self.multipleAvatarsNode.frame = bounds.offsetBy(dx: 8.0, dy: 0.0) + } else { + self.multipleAvatarsNode.frame = bounds.offsetBy(dx: 10.0, dy: 1.0) + } + } + + func setPeers(account: Account, peers: [Peer], animated: Bool) { + self.account = account + self.peers = peers + + let bounds = self.bounds + if !bounds.width.isZero { + let avatarsLayout = MultipleAvatarsNode.asyncLayout(self.multipleAvatarsNode) + let apply = avatarsLayout(account, peers, bounds.size) + let _ = apply(animated) + } + } +} + diff --git a/TelegramUI/ChatOverlayNavigationBar.swift b/TelegramUI/ChatOverlayNavigationBar.swift new file mode 100644 index 0000000000..e1a8cec513 --- /dev/null +++ b/TelegramUI/ChatOverlayNavigationBar.swift @@ -0,0 +1,73 @@ +import Foundation +import AsyncDisplayKit +import Display + +private let titleFont = Font.regular(14.0) + +final class ChatOverlayNavigationBar: ASDisplayNode { + private let theme: PresentationTheme + private let close: () -> Void + + private let separatorNode: ASDisplayNode + private let titleNode: TextNode + private let closeButton: HighlightableButtonNode + + init(theme: PresentationTheme, close: @escaping () -> Void) { + self.theme = theme + self.close = close + + self.separatorNode = ASDisplayNode() + self.separatorNode.isLayerBacked = true + self.separatorNode.backgroundColor = theme.inAppNotification.expandedNotification.navigationBar.separatorColor + + self.titleNode = TextNode() + self.titleNode.displaysAsynchronously = false + self.titleNode.isLayerBacked = true + + self.closeButton = HighlightableButtonNode() + self.closeButton.hitTestSlop = UIEdgeInsetsMake(-8.0, -8.0, -8.0, -8.0) + self.closeButton.displaysAsynchronously = false + + let closeImage = generateImage(CGSize(width: 12.0, height: 12.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setStrokeColor(theme.inAppNotification.expandedNotification.navigationBar.controlColor.cgColor) + context.setLineWidth(2.0) + context.setLineCap(.round) + context.move(to: CGPoint(x: 1.0, y: 1.0)) + context.addLine(to: CGPoint(x: size.width - 1.0, y: size.height - 1.0)) + context.strokePath() + context.move(to: CGPoint(x: size.width - 1.0, y: 1.0)) + context.addLine(to: CGPoint(x: 1.0, y: size.height - 1.0)) + context.strokePath() + }) + self.closeButton.setImage(closeImage, for: []) + + super.init() + + self.backgroundColor = theme.inAppNotification.expandedNotification.navigationBar.backgroundColor + + self.addSubnode(self.separatorNode) + self.addSubnode(self.titleNode) + self.addSubnode(self.closeButton) + + self.closeButton.addTarget(self, action: #selector(self.closePressed), forControlEvents: [.touchUpInside]) + } + + func updateLayout(size: CGSize, presentationInterfaceState: ChatPresentationInterfaceState, transition: ContainedViewLayoutTransition) { + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: size.height - UIScreenPixel), size: CGSize(width: size.width, height: UIScreenPixel))) + + let sideInset: CGFloat = 10.0 + + let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: presentationInterfaceState.peer?.displayTitle ?? "", font: titleFont, textColor: self.theme.inAppNotification.expandedNotification.navigationBar.primaryTextColor), maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: size.width - sideInset * 2.0 - 40.0, height: size.height))) + let _ = titleApply() + transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: sideInset, y: floor((size.height - titleLayout.size.height) / 2.0)), size: titleLayout.size)) + + let closeButtonSize = self.closeButton.measure(CGSize(width: 100.0, height: 100.0)) + transition.updateFrame(node: self.closeButton, frame: CGRect(origin: CGPoint(x: size.width - sideInset - closeButtonSize.width - 6.0, y: floor((size.height - closeButtonSize.height) / 2.0)), size: closeButtonSize)) + } + + @objc func closePressed() { + self.close() + } +} diff --git a/TelegramUI/ChatPanelInterfaceInteraction.swift b/TelegramUI/ChatPanelInterfaceInteraction.swift index 732f281604..d75727fe17 100644 --- a/TelegramUI/ChatPanelInterfaceInteraction.swift +++ b/TelegramUI/ChatPanelInterfaceInteraction.swift @@ -4,6 +4,12 @@ import SwiftSignalKit import TelegramCore import Display +public enum ChatFinishMediaRecordingAction { + case dismiss + case preview + case send +} + final class ChatPanelInterfaceInteractionStatuses { let editingMessage: Signal let startingBot: Signal @@ -28,17 +34,19 @@ enum ChatPanelSearchNavigationAction { final class ChatPanelInterfaceInteraction { let setupReplyMessage: (MessageId) -> Void let setupEditMessage: (MessageId) -> Void - let beginMessageSelection: (MessageId) -> Void + let beginMessageSelection: ([MessageId]) -> Void let deleteSelectedMessages: () -> Void let forwardSelectedMessages: () -> Void + let shareSelectedMessages: () -> Void let updateTextInputState: (@escaping (ChatTextInputState) -> ChatTextInputState) -> Void let updateInputModeAndDismissedButtonKeyboardMessageId: ((ChatPresentationInterfaceState) -> (ChatInputMode, MessageId?)) -> Void - let editMessage: (MessageId, String) -> Void - let beginMessageSearch: () -> Void + let editMessage: () -> Void + let beginMessageSearch: (ChatSearchDomain) -> Void let dismissMessageSearch: () -> Void let updateMessageSearch: (String) -> Void let navigateMessageSearch: (ChatPanelSearchNavigationAction) -> Void let openCalendarSearch: () -> Void + let toggleMembersSearch: (Bool) -> Void let navigateToMessage: (MessageId) -> Void let openPeerInfo: () -> Void let togglePeerNotifications: () -> Void @@ -47,9 +55,11 @@ final class ChatPanelInterfaceInteraction { let sendBotStart: (String?) -> Void let botSwitchChatWithPayload: (PeerId, String) -> Void let beginMediaRecording: (Bool) -> Void - let finishMediaRecording: (Bool) -> Void + let finishMediaRecording: (ChatFinishMediaRecordingAction) -> Void let stopMediaRecording: () -> Void let lockMediaRecording: () -> Void + let deleteRecordedMedia: () -> Void + let sendRecordedMedia: () -> Void let switchMediaRecordingMode: () -> Void let setupMessageAutoremoveTimeout: () -> Void let sendSticker: (TelegramMediaFile) -> Void @@ -61,15 +71,17 @@ final class ChatPanelInterfaceInteraction { let deleteChat: () -> Void let beginCall: () -> Void let toggleMessageStickerStarred: (MessageId) -> Void - let presentController: (ViewController) -> Void + let presentController: (ViewController, Any?) -> Void + let navigateFeed: () -> Void let statuses: ChatPanelInterfaceInteractionStatuses? - init(setupReplyMessage: @escaping (MessageId) -> Void, setupEditMessage: @escaping (MessageId) -> Void, beginMessageSelection: @escaping (MessageId) -> Void, deleteSelectedMessages: @escaping () -> Void, forwardSelectedMessages: @escaping () -> Void, updateTextInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputModeAndDismissedButtonKeyboardMessageId: @escaping ((ChatPresentationInterfaceState) -> (ChatInputMode, MessageId?)) -> Void, editMessage: @escaping (MessageId, String) -> Void, beginMessageSearch: @escaping () -> Void, dismissMessageSearch: @escaping () -> Void, updateMessageSearch: @escaping (String) -> Void, navigateMessageSearch: @escaping (ChatPanelSearchNavigationAction) -> Void, openCalendarSearch: @escaping () -> Void, navigateToMessage: @escaping (MessageId) -> Void, openPeerInfo: @escaping () -> Void, togglePeerNotifications: @escaping () -> Void, sendContextResult: @escaping (ChatContextResultCollection, ChatContextResult) -> Void, sendBotCommand: @escaping (Peer, String) -> Void, sendBotStart: @escaping (String?) -> Void, botSwitchChatWithPayload: @escaping (PeerId, String) -> Void, beginMediaRecording: @escaping (Bool) -> Void, finishMediaRecording: @escaping (Bool) -> Void, stopMediaRecording: @escaping () -> Void, lockMediaRecording: @escaping () -> Void, switchMediaRecordingMode: @escaping () -> Void, setupMessageAutoremoveTimeout: @escaping () -> Void, sendSticker: @escaping (TelegramMediaFile) -> Void, unblockPeer: @escaping () -> Void, pinMessage: @escaping (MessageId) -> Void, unpinMessage: @escaping () -> Void, reportPeer: @escaping () -> Void, dismissReportPeer: @escaping () -> Void, deleteChat: @escaping () -> Void, beginCall: @escaping () -> Void, toggleMessageStickerStarred: @escaping (MessageId) -> Void, presentController: @escaping (ViewController) -> Void, statuses: ChatPanelInterfaceInteractionStatuses?) { + init(setupReplyMessage: @escaping (MessageId) -> Void, setupEditMessage: @escaping (MessageId) -> Void, beginMessageSelection: @escaping ([MessageId]) -> Void, deleteSelectedMessages: @escaping () -> Void, forwardSelectedMessages: @escaping () -> Void, shareSelectedMessages: @escaping () -> Void, updateTextInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputModeAndDismissedButtonKeyboardMessageId: @escaping ((ChatPresentationInterfaceState) -> (ChatInputMode, MessageId?)) -> Void, editMessage: @escaping () -> Void, beginMessageSearch: @escaping (ChatSearchDomain) -> Void, dismissMessageSearch: @escaping () -> Void, updateMessageSearch: @escaping (String) -> Void, navigateMessageSearch: @escaping (ChatPanelSearchNavigationAction) -> Void, openCalendarSearch: @escaping () -> Void, toggleMembersSearch: @escaping (Bool) -> Void, navigateToMessage: @escaping (MessageId) -> Void, openPeerInfo: @escaping () -> Void, togglePeerNotifications: @escaping () -> Void, sendContextResult: @escaping (ChatContextResultCollection, ChatContextResult) -> Void, sendBotCommand: @escaping (Peer, String) -> Void, sendBotStart: @escaping (String?) -> Void, botSwitchChatWithPayload: @escaping (PeerId, String) -> Void, beginMediaRecording: @escaping (Bool) -> Void, finishMediaRecording: @escaping (ChatFinishMediaRecordingAction) -> Void, stopMediaRecording: @escaping () -> Void, lockMediaRecording: @escaping () -> Void, deleteRecordedMedia: @escaping () -> Void, sendRecordedMedia: @escaping () -> Void, switchMediaRecordingMode: @escaping () -> Void, setupMessageAutoremoveTimeout: @escaping () -> Void, sendSticker: @escaping (TelegramMediaFile) -> Void, unblockPeer: @escaping () -> Void, pinMessage: @escaping (MessageId) -> Void, unpinMessage: @escaping () -> Void, reportPeer: @escaping () -> Void, dismissReportPeer: @escaping () -> Void, deleteChat: @escaping () -> Void, beginCall: @escaping () -> Void, toggleMessageStickerStarred: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, navigateFeed: @escaping () -> Void, statuses: ChatPanelInterfaceInteractionStatuses?) { self.setupReplyMessage = setupReplyMessage self.setupEditMessage = setupEditMessage self.beginMessageSelection = beginMessageSelection self.deleteSelectedMessages = deleteSelectedMessages self.forwardSelectedMessages = forwardSelectedMessages + self.shareSelectedMessages = shareSelectedMessages self.updateTextInputState = updateTextInputState self.updateInputModeAndDismissedButtonKeyboardMessageId = updateInputModeAndDismissedButtonKeyboardMessageId self.editMessage = editMessage @@ -78,6 +90,7 @@ final class ChatPanelInterfaceInteraction { self.updateMessageSearch = updateMessageSearch self.navigateMessageSearch = navigateMessageSearch self.openCalendarSearch = openCalendarSearch + self.toggleMembersSearch = toggleMembersSearch self.navigateToMessage = navigateToMessage self.openPeerInfo = openPeerInfo self.togglePeerNotifications = togglePeerNotifications @@ -89,6 +102,8 @@ final class ChatPanelInterfaceInteraction { self.finishMediaRecording = finishMediaRecording self.stopMediaRecording = stopMediaRecording self.lockMediaRecording = lockMediaRecording + self.deleteRecordedMedia = deleteRecordedMedia + self.sendRecordedMedia = sendRecordedMedia self.switchMediaRecordingMode = switchMediaRecordingMode self.setupMessageAutoremoveTimeout = setupMessageAutoremoveTimeout self.sendSticker = sendSticker @@ -101,6 +116,7 @@ final class ChatPanelInterfaceInteraction { self.beginCall = beginCall self.toggleMessageStickerStarred = toggleMessageStickerStarred self.presentController = presentController + self.navigateFeed = navigateFeed self.statuses = statuses } } diff --git a/TelegramUI/ChatPinnedMessageTitlePanelNode.swift b/TelegramUI/ChatPinnedMessageTitlePanelNode.swift index bd73105579..3379ba3a7c 100644 --- a/TelegramUI/ChatPinnedMessageTitlePanelNode.swift +++ b/TelegramUI/ChatPinnedMessageTitlePanelNode.swift @@ -12,10 +12,13 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { private let lineNode: ASImageNode private let titleNode: TextNode private let textNode: TextNode + private let imageNode: TransformImageNode + private let separatorNode: ASDisplayNode - private var currentLayout: CGFloat? + private var currentLayout: (CGFloat, CGFloat, CGFloat)? private var currentMessage: Message? + private var previousMedia: Media? private let queue = Queue() @@ -43,6 +46,10 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { self.textNode.displaysAsynchronously = true self.textNode.isLayerBacked = true + self.imageNode = TransformImageNode() + self.imageNode.contentAnimations = [.subsequentUpdates] + self.imageNode.isHidden = true + super.init() self.tapButton.highligthedChanged = { [weak self] highlighted in @@ -71,6 +78,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { self.addSubnode(self.lineNode) self.addSubnode(self.titleNode) self.addSubnode(self.textNode) + self.addSubnode(self.imageNode) self.tapButton.addTarget(self, action: #selector(self.tapped), forControlEvents: [.touchUpInside]) self.addSubnode(self.tapButton) @@ -80,7 +88,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { private var theme: PresentationTheme? - override func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { let panelHeight: CGFloat = 50.0 if self.theme !== interfaceState.theme { @@ -105,12 +113,12 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { self.currentMessage = interfaceState.pinnedMessage if let currentMessage = currentMessage, let currentLayout = self.currentLayout { - self.enqueueTransition(width: currentLayout, transition: .immediate, message: currentMessage, theme: interfaceState.theme, strings: interfaceState.strings, accountPeerId: self.account.peerId, firstTime: previousMessageWasNil) + self.enqueueTransition(width: currentLayout.0, leftInset: currentLayout.1, rightInset: currentLayout.2, transition: .immediate, message: currentMessage, theme: interfaceState.theme, strings: interfaceState.strings, accountPeerId: self.account.peerId, firstTime: previousMessageWasNil) } } - let leftInset: CGFloat = 10.0 - let rightInset: CGFloat = 18.0 + let leftInset: CGFloat = 10.0 + leftInset + let rightInset: CGFloat = 18.0 + rightInset transition.updateFrame(node: self.lineNode, frame: CGRect(origin: CGPoint(x: leftInset, y: 7.0), size: CGSize(width: 2.0, height: panelHeight - 14.0))) @@ -120,20 +128,24 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelHeight - UIScreenPixel), size: CGSize(width: width, height: UIScreenPixel))) self.tapButton.frame = CGRect(origin: CGPoint(), size: CGSize(width: width - rightInset - closeButtonSize.width - 4.0, height: panelHeight)) - if self.currentLayout != width { - self.currentLayout = width + if self.currentLayout?.0 != width || self.currentLayout?.1 != leftInset || self.currentLayout?.2 != rightInset { + self.currentLayout = (width, leftInset, rightInset) if let currentMessage = self.currentMessage { - self.enqueueTransition(width: width, transition: .immediate, message: currentMessage, theme: interfaceState.theme, strings: interfaceState.strings, accountPeerId: interfaceState.accountPeerId, firstTime: true) + self.enqueueTransition(width: width, leftInset: leftInset, rightInset: rightInset, transition: .immediate, message: currentMessage, theme: interfaceState.theme, strings: interfaceState.strings, accountPeerId: interfaceState.accountPeerId, firstTime: true) } } return panelHeight } - private func enqueueTransition(width: CGFloat, transition: ContainedViewLayoutTransition, message: Message, theme: PresentationTheme, strings: PresentationStrings, accountPeerId: PeerId, firstTime: Bool) { + private func enqueueTransition(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, message: Message, theme: PresentationTheme, strings: PresentationStrings, accountPeerId: PeerId, firstTime: Bool) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeTextLayout = TextNode.asyncLayout(self.textNode) + let imageNodeLayout = self.imageNode.asyncLayout() + + let previousMedia = self.previousMedia + let account = self.account let targetQueue: Queue if firstTime { @@ -144,22 +156,88 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { targetQueue.async { [weak self] in let leftInset: CGFloat = 10.0 - let textLineInset: CGFloat = 10.0 - let rightInset: CGFloat = 18.0 + var textLineInset: CGFloat = 10.0 + let rightInset: CGFloat = 18.0 + rightInset let textRightInset: CGFloat = 25.0 - let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: strings.Conversation_PinnedMessage, font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor), nil, 1, .end, CGSize(width: width - leftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets(top: 2.0, left: 0.0, bottom: 2.0, right: 0.0)) + var updatedMedia: Media? + var imageDimensions: CGSize? + for media in message.media { + if let image = media as? TelegramMediaImage { + updatedMedia = image + if let representation = largestRepresentationForPhoto(image) { + imageDimensions = representation.dimensions + } + break + } else if let file = media as? TelegramMediaFile { + updatedMedia = file + if !file.isInstantVideo, let representation = largestImageRepresentation(file.previewRepresentations), !file.isSticker { + imageDimensions = representation.dimensions + } + break + } + } - let (textLayout, textApply) = makeTextLayout(NSAttributedString(string: descriptionStringForMessage(message, strings: strings, accountPeerId: accountPeerId), font: Font.regular(15.0), textColor: theme.chat.inputPanel.primaryTextColor), nil, 1, .end, CGSize(width: width - leftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets(top: 2.0, left: 0.0, bottom: 2.0, right: 0.0)) + var applyImage: (() -> Void)? + if let imageDimensions = imageDimensions { + let boundingSize = CGSize(width: 35.0, height: 35.0) + applyImage = imageNodeLayout(TransformImageArguments(corners: ImageCorners(radius: 2.0), imageSize: imageDimensions.aspectFilled(boundingSize), boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets())) + + textLineInset += 9.0 + 35.0 + } + + var mediaUpdated = false + if let updatedMedia = updatedMedia, let previousMedia = previousMedia { + mediaUpdated = !updatedMedia.isEqual(previousMedia) + } else if (updatedMedia != nil) != (previousMedia != nil) { + mediaUpdated = true + } + + var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? + if mediaUpdated { + if let updatedMedia = updatedMedia, imageDimensions != nil { + if let image = updatedMedia as? TelegramMediaImage { + updateImageSignal = chatMessagePhotoThumbnail(account: account, photo: image) + } else if let file = updatedMedia as? TelegramMediaFile { + if file.isVideo { + updateImageSignal = chatMessageVideoThumbnail(account: account, file: file) + } else if let iconImageRepresentation = smallestImageRepresentation(file.previewRepresentations) { + let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [iconImageRepresentation], reference: nil) + updateImageSignal = chatWebpageSnippetPhoto(account: account, photo: tmpImage) + } + } + } else { + updateImageSignal = .single({ _ in return nil }) + } + } + + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: strings.Conversation_PinnedMessage, font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - leftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 0.0, bottom: 2.0, right: 0.0))) + + let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: descriptionStringForMessage(message, strings: strings, accountPeerId: accountPeerId), font: Font.regular(15.0), textColor: theme.chat.inputPanel.primaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - leftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 0.0, bottom: 2.0, right: 0.0))) Queue.mainQueue().async { if let strongSelf = self { let _ = titleApply() let _ = textApply() - strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset + textLineInset, y: 6.0), size: titleLayout.size) + strongSelf.previousMedia = updatedMedia - strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset + textLineInset, y: 24.0), size: textLayout.size) + strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset + textLineInset, y: 5.0), size: titleLayout.size) + + strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset + textLineInset, y: 23.0), size: textLayout.size) + + strongSelf.imageNode.frame = CGRect(origin: CGPoint(x: leftInset + 9.0, y: 7.0), size: CGSize(width: 35.0, height: 35.0)) + + if let applyImage = applyImage { + applyImage() + strongSelf.imageNode.isHidden = false + } else { + strongSelf.imageNode.isHidden = true + } + + if let updateImageSignal = updateImageSignal { + strongSelf.imageNode.setSignal(updateImageSignal) + } } } } diff --git a/TelegramUI/ChatPresentationData.swift b/TelegramUI/ChatPresentationData.swift new file mode 100644 index 0000000000..b2b971f5bd --- /dev/null +++ b/TelegramUI/ChatPresentationData.swift @@ -0,0 +1,43 @@ +import Foundation + +extension PresentationFontSize { + var baseDisplaySize: CGFloat { + switch self { + case .extraSmall: + return 13.0 + case .small: + return 15.0 + case .regular: + return 17.0 + case .large: + return 19.0 + case .extraLarge: + return 21.0 + } + } +} + +public final class ChatPresentationData { + let theme: PresentationTheme + let fontSize: PresentationFontSize + let strings: PresentationStrings + let wallpaper: TelegramWallpaper + let timeFormat: PresentationTimeFormat + + let messageFont: UIFont + let messageBoldFont: UIFont + let messageFixedFont: UIFont + + init(theme: PresentationTheme, fontSize: PresentationFontSize, strings: PresentationStrings, wallpaper: TelegramWallpaper, timeFormat: PresentationTimeFormat) { + self.theme = theme + self.fontSize = fontSize + self.strings = strings + self.wallpaper = wallpaper + self.timeFormat = timeFormat + + let baseFontSize = fontSize.baseDisplaySize + self.messageFont = UIFont.systemFont(ofSize: baseFontSize) + self.messageBoldFont = UIFont.boldSystemFont(ofSize: baseFontSize) + self.messageFixedFont = UIFont(name: "Menlo-Regular", size: baseFontSize - 1.0) ?? UIFont.systemFont(ofSize: baseFontSize) + } +} diff --git a/TelegramUI/ChatPresentationInterfaceState.swift b/TelegramUI/ChatPresentationInterfaceState.swift index 38654f83e5..5478ece8cf 100644 --- a/TelegramUI/ChatPresentationInterfaceState.swift +++ b/TelegramUI/ChatPresentationInterfaceState.swift @@ -2,13 +2,63 @@ import Foundation import Postbox import TelegramCore -enum ChatPresentationInputQuery: Equatable { +enum ChatPresentationInputQueryKind: Int32 { + case emoji + case hashtag + case mention + case command + case contextRequest +} + +struct ChatInputQueryMentionTypes: OptionSet { + var rawValue: Int32 + + init(rawValue: Int32) { + self.rawValue = rawValue + } + + static let contextBots = ChatInputQueryMentionTypes(rawValue: 1 << 0) + static let members = ChatInputQueryMentionTypes(rawValue: 1 << 1) + static let accountPeer = ChatInputQueryMentionTypes(rawValue: 1 << 2) +} + +enum ChatPresentationInputQuery: Hashable, Equatable { case emoji(String) case hashtag(String) - case mention(String) + case mention(query: String, types: ChatInputQueryMentionTypes) case command(String) case contextRequest(addressName: String, query: String) + var kind: ChatPresentationInputQueryKind { + switch self { + case .emoji: + return .emoji + case .hashtag: + return .hashtag + case .mention: + return .mention + case .command: + return .command + case .contextRequest: + return .contextRequest + } + } + + var hashValue: Int { + switch self { + case let .emoji(value): + return 1 &+ value.hashValue + case let .hashtag(value): + return 2 &+ value.hashValue + case let .mention(text, types): + return 3 &+ text.hashValue &* 31 &+ types.rawValue.hashValue + case let .command(value): + return 4 &+ value.hashValue + case let .contextRequest(addressName, query): + return 5 &+ addressName.hashValue &* 31 + query.hashValue + } + } + static func ==(lhs: ChatPresentationInputQuery, rhs: ChatPresentationInputQuery) -> Bool { switch lhs { case let .emoji(query): @@ -23,8 +73,8 @@ enum ChatPresentationInputQuery: Equatable { } else { return false } - case let .mention(query): - if case .mention(query) = rhs { + case let .mention(query, types): + if case .mention(query, types) = rhs { return true } else { return false @@ -50,7 +100,7 @@ enum ChatPresentationInputQueryResult: Equatable { case hashtags([String]) case mentions([Peer]) case commands([PeerCommand]) - case contextRequestResult(Peer, ChatContextResultCollection?) + case contextRequestResult(Peer?, ChatContextResultCollection?) static func ==(lhs: ChatPresentationInputQueryResult, rhs: ChatPresentationInputQueryResult) -> Bool { switch lhs { @@ -92,9 +142,14 @@ enum ChatPresentationInputQueryResult: Equatable { } case let .contextRequestResult(lhsPeer, lhsCollection): if case let .contextRequestResult(rhsPeer, rhsCollection) = rhs { - if !lhsPeer.isEqual(rhsPeer) { + if let lhsPeer = lhsPeer, let rhsPeer = rhsPeer { + if !lhsPeer.isEqual(rhsPeer) { + return false + } + } else if (lhsPeer != nil) != (rhsPeer != nil) { return false } + if lhsCollection != rhsCollection { return false } @@ -106,11 +161,45 @@ enum ChatPresentationInputQueryResult: Equatable { } } -enum ChatInputMode { +enum ChatMediaInputMode { + case gif + case other +} + +enum ChatInputMode: Equatable { case none case text - case media + case media(ChatMediaInputMode) case inputButtons + + static func ==(lhs: ChatInputMode, rhs: ChatInputMode) -> Bool { + switch lhs { + case .none: + if case .none = rhs { + return true + } else { + return false + } + case .text: + if case .text = rhs { + return true + } else { + return false + } + case let .media(mode): + if case .media(mode) = rhs { + return true + } else { + return false + } + case .inputButtons: + if case .inputButtons = rhs { + return true + } else { + return false + } + } + } } enum ChatTitlePanelContext: Comparable { @@ -181,12 +270,67 @@ struct ChatSearchResultsState: Equatable { } } +enum ChatSearchDomain: Equatable { + case everything + case members + case member(Peer) + + static func ==(lhs: ChatSearchDomain, rhs: ChatSearchDomain) -> Bool { + switch lhs { + case .everything: + if case .everything = rhs { + return true + } else { + return false + } + case .members: + if case .members = rhs { + return true + } else { + return false + } + case let .member(lhsPeer): + if case let .member(rhsPeer) = rhs, arePeersEqual(lhsPeer, rhsPeer) { + return true + } else { + return false + } + } + } +} + +enum ChatSearchDomainSuggestionContext: Equatable { + case none + case members(String) + + static func ==(lhs: ChatSearchDomainSuggestionContext, rhs: ChatSearchDomainSuggestionContext) -> Bool { + switch lhs { + case .none: + if case .none = rhs { + return true + } else { + return false + } + case let .members(query): + if case .members(query) = rhs { + return true + } else { + return false + } + } + } +} + struct ChatSearchData: Equatable { let query: String + let domain: ChatSearchDomain + let domainSuggestionContext: ChatSearchDomainSuggestionContext let resultsState: ChatSearchResultsState? - init(query: String = "", resultsState: ChatSearchResultsState? = nil) { + init(query: String = "", domain: ChatSearchDomain = .everything, domainSuggestionContext: ChatSearchDomainSuggestionContext = .none, resultsState: ChatSearchResultsState? = nil) { self.query = query + self.domain = domain + self.domainSuggestionContext = domainSuggestionContext self.resultsState = resultsState } @@ -194,6 +338,12 @@ struct ChatSearchData: Equatable { if lhs.query != rhs.query { return false } + if lhs.domain != rhs.domain { + return false + } + if lhs.domainSuggestionContext != rhs.domainSuggestionContext { + return false + } if lhs.resultsState != rhs.resultsState { return false } @@ -201,19 +351,59 @@ struct ChatSearchData: Equatable { } func withUpdatedQuery(_ query: String) -> ChatSearchData { - return ChatSearchData(query: query, resultsState: self.resultsState) + return ChatSearchData(query: query, domain: self.domain, domainSuggestionContext: self.domainSuggestionContext, resultsState: self.resultsState) + } + + func withUpdatedDomain(_ domain: ChatSearchDomain) -> ChatSearchData { + return ChatSearchData(query: self.query, domain: domain, domainSuggestionContext: self.domainSuggestionContext, resultsState: self.resultsState) + } + + func withUpdatedDomainSuggestionContext(_ domain: ChatSearchDomainSuggestionContext) -> ChatSearchData { + return ChatSearchData(query: self.query, domain: self.domain, domainSuggestionContext: domainSuggestionContext, resultsState: self.resultsState) } func withUpdatedResultsState(_ resultsState: ChatSearchResultsState?) -> ChatSearchData { - return ChatSearchData(query: self.query, resultsState: resultsState) + return ChatSearchData(query: self.query, domain: self.domain, domainSuggestionContext: self.domainSuggestionContext, resultsState: resultsState) + } +} + +final class ChatRecordedMediaPreview: Equatable { + let resource: TelegramMediaResource + let fileSize: Int32 + let duration: Int32 + let waveform: AudioWaveform + + init(resource: TelegramMediaResource, duration: Int32, fileSize: Int32, waveform: AudioWaveform) { + self.resource = resource + self.duration = duration + self.fileSize = fileSize + self.waveform = waveform + } + + static func ==(lhs: ChatRecordedMediaPreview, rhs: ChatRecordedMediaPreview) -> Bool { + if !lhs.resource.isEqual(to: rhs.resource) { + return false + } + if lhs.duration != rhs.duration { + return false + } + if lhs.fileSize != rhs.fileSize { + return false + } + if lhs.waveform != rhs.waveform { + return false + } + return true } } struct ChatPresentationInterfaceState: Equatable { let interfaceState: ChatInterfaceState + let chatLocation: ChatLocation let peer: Peer? let inputTextPanelState: ChatTextInputPanelState - let inputQueryResult: ChatPresentationInputQueryResult? + let recordedMediaPreview: ChatRecordedMediaPreview? + let inputQueryResults: [ChatPresentationInputQueryKind: ChatPresentationInputQueryResult] let inputMode: ChatInputMode let titlePanelContexts: [ChatTitlePanelContext] let keyboardButtonsMessage: Message? @@ -225,17 +415,23 @@ struct ChatPresentationInterfaceState: Equatable { let chatHistoryState: ChatHistoryNodeHistoryState? let botStartPayload: String? let urlPreview: (String, TelegramMediaWebpage)? + let editingUrlPreview: (String, TelegramMediaWebpage)? let search: ChatSearchData? + let searchQuerySuggestionResult: ChatPresentationInputQueryResult? let chatWallpaper: TelegramWallpaper let theme: PresentationTheme let strings: PresentationStrings + let fontSize: PresentationFontSize let accountPeerId: PeerId + let mode: ChatControllerPresentationMode - init(chatWallpaper: TelegramWallpaper, theme: PresentationTheme, strings: PresentationStrings, accountPeerId: PeerId) { + init(chatWallpaper: TelegramWallpaper, theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize, accountPeerId: PeerId, mode: ChatControllerPresentationMode, chatLocation: ChatLocation) { self.interfaceState = ChatInterfaceState() self.inputTextPanelState = ChatTextInputPanelState() + self.recordedMediaPreview = nil + self.chatLocation = chatLocation self.peer = nil - self.inputQueryResult = nil + self.inputQueryResults = [:] self.inputMode = .none self.titlePanelContexts = [] self.keyboardButtonsMessage = nil @@ -247,18 +443,24 @@ struct ChatPresentationInterfaceState: Equatable { self.chatHistoryState = nil self.botStartPayload = nil self.urlPreview = nil + self.editingUrlPreview = nil self.search = nil + self.searchQuerySuggestionResult = nil self.chatWallpaper = chatWallpaper self.theme = theme self.strings = strings + self.fontSize = fontSize self.accountPeerId = accountPeerId + self.mode = mode } - init(interfaceState: ChatInterfaceState, peer: Peer?, inputTextPanelState: ChatTextInputPanelState, inputQueryResult: ChatPresentationInputQueryResult?, inputMode: ChatInputMode, titlePanelContexts: [ChatTitlePanelContext], keyboardButtonsMessage: Message?, pinnedMessageId: MessageId?, pinnedMessage: Message?, peerIsBlocked: Bool, peerIsMuted: Bool, canReportPeer: Bool, chatHistoryState: ChatHistoryNodeHistoryState?, botStartPayload: String?, urlPreview: (String, TelegramMediaWebpage)?, search: ChatSearchData?, chatWallpaper: TelegramWallpaper, theme: PresentationTheme, strings: PresentationStrings, accountPeerId: PeerId) { + init(interfaceState: ChatInterfaceState, chatLocation: ChatLocation, peer: Peer?, inputTextPanelState: ChatTextInputPanelState, recordedMediaPreview: ChatRecordedMediaPreview?, inputQueryResults: [ChatPresentationInputQueryKind: ChatPresentationInputQueryResult], inputMode: ChatInputMode, titlePanelContexts: [ChatTitlePanelContext], keyboardButtonsMessage: Message?, pinnedMessageId: MessageId?, pinnedMessage: Message?, peerIsBlocked: Bool, peerIsMuted: Bool, canReportPeer: Bool, chatHistoryState: ChatHistoryNodeHistoryState?, botStartPayload: String?, urlPreview: (String, TelegramMediaWebpage)?, editingUrlPreview: (String, TelegramMediaWebpage)?, search: ChatSearchData?, searchQuerySuggestionResult: ChatPresentationInputQueryResult?, chatWallpaper: TelegramWallpaper, theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize, accountPeerId: PeerId, mode: ChatControllerPresentationMode) { self.interfaceState = interfaceState + self.chatLocation = chatLocation self.peer = peer self.inputTextPanelState = inputTextPanelState - self.inputQueryResult = inputQueryResult + self.recordedMediaPreview = recordedMediaPreview + self.inputQueryResults = inputQueryResults self.inputMode = inputMode self.titlePanelContexts = titlePanelContexts self.keyboardButtonsMessage = keyboardButtonsMessage @@ -270,11 +472,15 @@ struct ChatPresentationInterfaceState: Equatable { self.chatHistoryState = chatHistoryState self.botStartPayload = botStartPayload self.urlPreview = urlPreview + self.editingUrlPreview = editingUrlPreview self.search = search + self.searchQuerySuggestionResult = searchQuerySuggestionResult self.chatWallpaper = chatWallpaper self.theme = theme self.strings = strings + self.fontSize = fontSize self.accountPeerId = accountPeerId + self.mode = mode } static func ==(lhs: ChatPresentationInterfaceState, rhs: ChatPresentationInterfaceState) -> Bool { @@ -293,7 +499,11 @@ struct ChatPresentationInterfaceState: Equatable { return false } - if lhs.inputQueryResult != rhs.inputQueryResult { + if lhs.recordedMediaPreview != rhs.recordedMediaPreview { + return false + } + + if lhs.inputQueryResults != rhs.inputQueryResults { return false } @@ -362,10 +572,25 @@ struct ChatPresentationInterfaceState: Equatable { return false } + if let lhsEditingUrlPreview = lhs.editingUrlPreview, let rhsEditingUrlPreview = rhs.editingUrlPreview { + if lhsEditingUrlPreview.0 != rhsEditingUrlPreview.0 { + return false + } + if !lhsEditingUrlPreview.1.isEqual(rhsEditingUrlPreview.1) { + return false + } + } else if (lhs.editingUrlPreview != nil) != (rhs.editingUrlPreview != nil) { + return false + } + if lhs.search != rhs.search { return false } + if lhs.searchQuerySuggestionResult != rhs.searchQuerySuggestionResult { + return false + } + if lhs.chatWallpaper != rhs.chatWallpaper { return false } @@ -378,74 +603,105 @@ struct ChatPresentationInterfaceState: Equatable { return false } + if lhs.fontSize != rhs.fontSize { + return false + } + if lhs.accountPeerId != rhs.accountPeerId { return false } + if lhs.mode != rhs.mode { + return false + } + return true } func updatedInterfaceState(_ f: (ChatInterfaceState) -> ChatInterfaceState) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: f(self.interfaceState), peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, search: self.search, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, accountPeerId: self.accountPeerId) + return ChatPresentationInterfaceState(interfaceState: f(self.interfaceState), chatLocation: self.chatLocation, peer: self.peer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) } func updatedPeer(_ f: (Peer?) -> Peer?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: f(self.peer), inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, search: self.search, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, accountPeerId: self.accountPeerId) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, peer: f(self.peer), inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) } - func updatedInputQueryResult(_ f: (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: f(self.inputQueryResult), inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, search: self.search, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, accountPeerId: self.accountPeerId) + func updatedInputQueryResult(queryKind: ChatPresentationInputQueryKind, _ f: (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?) -> ChatPresentationInterfaceState { + var inputQueryResults = self.inputQueryResults + let updated = f(inputQueryResults[queryKind]) + if let updated = updated { + inputQueryResults[queryKind] = updated + } else { + inputQueryResults.removeValue(forKey: queryKind) + } + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, peer: self.peer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) } func updatedInputTextPanelState(_ f: (ChatTextInputPanelState) -> ChatTextInputPanelState) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: f(self.inputTextPanelState), inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, search: self.search, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, accountPeerId: self.accountPeerId) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, peer: self.peer, inputTextPanelState: f(self.inputTextPanelState), recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) + } + + func updatedRecordedMediaPreview(_ recordedMediaPreview: ChatRecordedMediaPreview?) -> ChatPresentationInterfaceState { + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, peer: self.peer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) } func updatedInputMode(_ f: (ChatInputMode) -> ChatInputMode) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: f(self.inputMode), titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, search: self.search, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, accountPeerId: self.accountPeerId) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, peer: self.peer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: f(self.inputMode), titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) } func updatedTitlePanelContext(_ f: ([ChatTitlePanelContext]) -> [ChatTitlePanelContext]) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: f(self.titlePanelContexts), keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, search: self.search, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, accountPeerId: self.accountPeerId) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, peer: self.peer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: f(self.titlePanelContexts), keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) } func updatedKeyboardButtonsMessage(_ message: Message?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: message, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, search: self.search, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, accountPeerId: self.accountPeerId) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, peer: self.peer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: message, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) } func updatedPinnedMessageId(_ pinnedMessageId: MessageId?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, search: self.search, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, accountPeerId: self.accountPeerId) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, peer: self.peer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) } func updatedPinnedMessage(_ pinnedMessage: Message?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, search: self.search, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, accountPeerId: self.accountPeerId) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, peer: self.peer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) } func updatedPeerIsBlocked(_ peerIsBlocked: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, search: self.search, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, accountPeerId: self.accountPeerId) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, peer: self.peer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) } func updatedPeerIsMuted(_ peerIsMuted: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, search: self.search, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, accountPeerId: self.accountPeerId) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, peer: self.peer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) } func updatedCanReportPeer(_ canReportPeer: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, search: self.search, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, accountPeerId: self.accountPeerId) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, peer: self.peer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) } func updatedBotStartPayload(_ botStartPayload: String?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: botStartPayload, urlPreview: self.urlPreview, search: self.search, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, accountPeerId: self.accountPeerId) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, peer: self.peer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) } func updatedChatHistoryState(_ chatHistoryState: ChatHistoryNodeHistoryState?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, search: self.search, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, accountPeerId: self.accountPeerId) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, peer: self.peer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) } func updatedUrlPreview(_ urlPreview: (String, TelegramMediaWebpage)?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: urlPreview, search: self.search, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, accountPeerId: self.accountPeerId) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, peer: self.peer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) + } + + func updatedEditingUrlPreview(_ editingUrlPreview: (String, TelegramMediaWebpage)?) -> ChatPresentationInterfaceState { + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, peer: self.peer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) } func updatedSearch(_ search: ChatSearchData?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, search: search, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, accountPeerId: self.accountPeerId) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, peer: self.peer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) + } + + func updatedSearchQuerySuggestionResult(_ f: (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?) -> ChatPresentationInterfaceState { + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, peer: self.peer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: f(self.searchQuerySuggestionResult), chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) + } + + func updatedMode(_ mode: ChatControllerPresentationMode) -> ChatPresentationInterfaceState { + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, peer: self.peer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: mode) } } diff --git a/TelegramUI/ChatRecordingPreviewInputPanelNode.swift b/TelegramUI/ChatRecordingPreviewInputPanelNode.swift new file mode 100644 index 0000000000..41910c341f --- /dev/null +++ b/TelegramUI/ChatRecordingPreviewInputPanelNode.swift @@ -0,0 +1,160 @@ +import Foundation +import AsyncDisplayKit +import Display +import TelegramCore +import Postbox +import SwiftSignalKit + +private func generatePauseIcon(_ theme: PresentationTheme) -> UIImage? { + return generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/MinimizedPause"), color: theme.chat.inputPanel.actionControlForegroundColor) +} + +private func generatePlayIcon(_ theme: PresentationTheme) -> UIImage? { + return generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/MinimizedPlay"), color: theme.chat.inputPanel.actionControlForegroundColor) +} + +final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { + private let deleteButton: HighlightableButtonNode + private let sendButton: HighlightableButtonNode + private let playButton: HighlightableButtonNode + private let pauseButton: HighlightableButtonNode + private let waveformButton: ASButtonNode + private let waveformBackgroundNode: ASImageNode + + private let waveformNode: AudioWaveformNode + private let waveformForegroundNode: AudioWaveformNode + private let waveformScubberNode: MediaPlayerScrubbingNode + + private var presentationInterfaceState: ChatPresentationInterfaceState? + + private var mediaPlayer: MediaPlayer? + private let durationLabel: MediaPlayerTimeTextNode + + private let statusDisposable = MetaDisposable() + + init(theme: PresentationTheme) { + self.deleteButton = HighlightableButtonNode() + self.deleteButton.displaysAsynchronously = false + self.deleteButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Acessory Panels/MessageSelectionThrash"), color: theme.chat.inputPanel.panelControlAccentColor), for: []) + + self.sendButton = HighlightableButtonNode() + self.sendButton.displaysAsynchronously = false + self.sendButton.setImage(PresentationResourcesChat.chatInputPanelSendButtonImage(theme), for: []) + + self.waveformBackgroundNode = ASImageNode() + self.waveformBackgroundNode.isLayerBacked = true + self.waveformBackgroundNode.displaysAsynchronously = false + self.waveformBackgroundNode.displayWithoutProcessing = true + self.waveformBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 33.0, color: theme.chat.inputPanel.actionControlFillColor) + + self.playButton = HighlightableButtonNode() + self.playButton.displaysAsynchronously = false + self.playButton.setImage(generatePlayIcon(theme), for: []) + self.pauseButton = HighlightableButtonNode() + self.pauseButton.displaysAsynchronously = false + self.pauseButton.setImage(generatePauseIcon(theme), for: []) + self.pauseButton.isHidden = true + + self.waveformButton = ASButtonNode() + + self.waveformNode = AudioWaveformNode() + self.waveformNode.isLayerBacked = true + self.waveformForegroundNode = AudioWaveformNode() + self.waveformForegroundNode.isLayerBacked = true + + self.waveformScubberNode = MediaPlayerScrubbingNode(content: .custom(backgroundNode: self.waveformNode, foregroundContentNode: self.waveformForegroundNode)) + + self.durationLabel = MediaPlayerTimeTextNode(textColor: theme.chat.inputPanel.actionControlForegroundColor) + self.durationLabel.alignment = .right + self.durationLabel.mode = .normal + + super.init() + + self.addSubnode(self.deleteButton) + self.addSubnode(self.sendButton) + self.addSubnode(self.waveformBackgroundNode) + self.addSubnode(self.waveformScubberNode) + self.addSubnode(self.playButton) + self.addSubnode(self.pauseButton) + self.addSubnode(self.durationLabel) + self.addSubnode(self.waveformButton) + + self.deleteButton.addTarget(self, action: #selector(self.deletePressed), forControlEvents: [.touchUpInside]) + self.sendButton.addTarget(self, action: #selector(self.sendPressed), forControlEvents: [.touchUpInside]) + + self.waveformButton.addTarget(self, action: #selector(self.waveformPressed), forControlEvents: .touchUpInside) + } + + deinit { + self.mediaPlayer?.pause() + self.statusDisposable.dispose() + } + + @objc func buttonPressed() { + self.interfaceInteraction?.deleteChat() + } + + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { + if self.presentationInterfaceState != interfaceState { + var updateWaveform = false + if self.presentationInterfaceState?.recordedMediaPreview != interfaceState.recordedMediaPreview { + updateWaveform = true + } + self.presentationInterfaceState = interfaceState + + if let recordedMediaPreview = interfaceState.recordedMediaPreview, updateWaveform { + self.waveformNode.setup(color: interfaceState.theme.chat.inputPanel.actionControlForegroundColor.withAlphaComponent(0.5), waveform: recordedMediaPreview.waveform) + self.waveformForegroundNode.setup(color: interfaceState.theme.chat.inputPanel.actionControlForegroundColor, waveform: recordedMediaPreview.waveform) + + if self.mediaPlayer != nil { + self.mediaPlayer?.pause() + } + if let account = self.account { + let mediaPlayer = MediaPlayer(audioSessionManager: account.telegramApplicationContext.mediaManager.audioSession, postbox: account.postbox, resource: recordedMediaPreview.resource, streamable: false, video: false, preferSoftwareDecoding: false, enableSound: true) + self.mediaPlayer = mediaPlayer + self.durationLabel.defaultDuration = Double(recordedMediaPreview.duration) + self.durationLabel.status = mediaPlayer.status + self.waveformScubberNode.status = mediaPlayer.status + self.statusDisposable.set((mediaPlayer.status + |> deliverOnMainQueue).start(next: { [weak self] status in + if let strongSelf = self { + switch status.status { + case .playing, .buffering(_, true): + strongSelf.playButton.isHidden = true + default: + strongSelf.playButton.isHidden = false + } + strongSelf.pauseButton.isHidden = !strongSelf.playButton.isHidden + } + })) + } + } + } + + let panelHeight: CGFloat = 47.0 + + transition.updateFrame(node: self.deleteButton, frame: CGRect(origin: CGPoint(x: leftInset, y: -1.0), size: CGSize(width: 48.0, height: 47.0))) + transition.updateFrame(node: self.sendButton, frame: CGRect(origin: CGPoint(x: width - rightInset - 43.0 - UIScreenPixel, y: -UIScreenPixel), size: CGSize(width: 44.0, height: panelHeight))) + transition.updateFrame(node: self.playButton, frame: CGRect(origin: CGPoint(x: leftInset + 52.0, y: 10.0), size: CGSize(width: 26.0, height: 26.0))) + transition.updateFrame(node: self.pauseButton, frame: CGRect(origin: CGPoint(x: leftInset + 50.0, y: 10.0), size: CGSize(width: 26.0, height: 26.0))) + transition.updateFrame(node: self.waveformBackgroundNode, frame: CGRect(origin: CGPoint(x: leftInset + 45.0, y: 7.0 - UIScreenPixel), size: CGSize(width: width - leftInset - rightInset - 90.0, height: 33.0))) + transition.updateFrame(node: self.waveformButton, frame: CGRect(origin: CGPoint(x: leftInset + 45.0, y: 0.0), size: CGSize(width: width - leftInset - rightInset - 90.0, height: panelHeight))) + transition.updateFrame(node: self.waveformScubberNode, frame: CGRect(origin: CGPoint(x: leftInset + 45.0 + 35.0, y: 7.0 + floor((33.0 - 13.0) / 2.0)), size: CGSize(width: width - leftInset - rightInset - 90.0 - 45.0 - 40.0, height: 13.0))) + transition.updateFrame(node: self.durationLabel, frame: CGRect(origin: CGPoint(x: width - rightInset - 90.0 - 4.0, y: 15.0), size: CGSize(width: 35.0, height: 20.0))) + + return panelHeight + } + + @objc func deletePressed() { + self.interfaceInteraction?.deleteRecordedMedia() + } + + @objc func sendPressed() { + self.interfaceInteraction?.sendRecordedMedia() + } + + @objc func waveformPressed() { + self.mediaPlayer?.togglePlayPause() + } +} + diff --git a/TelegramUI/ChatReportPeerTitlePanelNode.swift b/TelegramUI/ChatReportPeerTitlePanelNode.swift index 01f5e7e706..38d52ddd0a 100644 --- a/TelegramUI/ChatReportPeerTitlePanelNode.swift +++ b/TelegramUI/ChatReportPeerTitlePanelNode.swift @@ -43,7 +43,7 @@ final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode { self.addSubnode(self.closeButton) } - override func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { if interfaceState.theme !== self.theme { self.theme = interfaceState.theme @@ -54,10 +54,10 @@ final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode { let panelHeight: CGFloat = 40.0 - let rightInset: CGFloat = 18.0 + let contentRightInset: CGFloat = 18.0 + rightInset let closeButtonSize = self.closeButton.measure(CGSize(width: 100.0, height: 100.0)) - transition.updateFrame(node: self.closeButton, frame: CGRect(origin: CGPoint(x: width - rightInset - closeButtonSize.width, y: 14.0), size: closeButtonSize)) + transition.updateFrame(node: self.closeButton, frame: CGRect(origin: CGPoint(x: width - contentRightInset - closeButtonSize.width, y: 14.0), size: closeButtonSize)) let updatedButtons: [ChatReportPeerTitleButton] if let _ = interfaceState.peer { @@ -96,8 +96,8 @@ final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode { } if !self.buttons.isEmpty { - let buttonWidth = floor(width / CGFloat(self.buttons.count)) - var nextButtonOrigin: CGFloat = 0.0 + let buttonWidth = floor((width - leftInset - rightInset) / CGFloat(self.buttons.count)) + var nextButtonOrigin: CGFloat = leftInset for (_, view) in self.buttons { view.frame = CGRect(origin: CGPoint(x: nextButtonOrigin, y: 0.0), size: CGSize(width: buttonWidth, height: panelHeight)) nextButtonOrigin += buttonWidth diff --git a/TelegramUI/ChatRequestInProgressTitlePanelNode.swift b/TelegramUI/ChatRequestInProgressTitlePanelNode.swift index c477ab4007..374e9451ab 100644 --- a/TelegramUI/ChatRequestInProgressTitlePanelNode.swift +++ b/TelegramUI/ChatRequestInProgressTitlePanelNode.swift @@ -18,17 +18,15 @@ final class ChatRequestInProgressTitlePanelNode: ChatTitleAccessoryPanelNode { super.init() - self.backgroundColor = UIColor(rgb: 0xF5F6F8) - self.addSubnode(self.titleNode) self.addSubnode(self.separatorNode) } - override func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { if interfaceState.strings !== self.strings { self.strings = interfaceState.strings - self.titleNode.attributedText = NSAttributedString(string: interfaceState.strings.Channel_NotificationLoading, font: Font.regular(14.0), textColor: UIColor.black) + self.titleNode.attributedText = NSAttributedString(string: interfaceState.strings.Channel_NotificationLoading, font: Font.regular(14.0), textColor: interfaceState.theme.rootController.navigationBar.primaryTextColor) } if interfaceState.theme !== self.theme { @@ -40,7 +38,7 @@ final class ChatRequestInProgressTitlePanelNode: ChatTitleAccessoryPanelNode { let panelHeight: CGFloat = 40.0 - let titleSize = self.titleNode.measure(CGSize(width: width, height: 100.0)) + let titleSize = self.titleNode.measure(CGSize(width: width - leftInset - rightInset, height: 100.0)) transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floor((width - titleSize.width) / 2.0), y: floor((panelHeight - titleSize.height) / 2.0)), size: titleSize)) transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelHeight - UIScreenPixel), size: CGSize(width: width, height: UIScreenPixel))) diff --git a/TelegramUI/ChatSearchInputPanelNode.swift b/TelegramUI/ChatSearchInputPanelNode.swift index 3a8f33be87..1a7ae19cfe 100644 --- a/TelegramUI/ChatSearchInputPanelNode.swift +++ b/TelegramUI/ChatSearchInputPanelNode.swift @@ -11,6 +11,7 @@ final class ChatSearchInputPanelNode: ChatInputPanelNode { private let upButton: HighlightableButtonNode private let downButton: HighlightableButtonNode private let calendarButton: HighlightableButtonNode + private let membersButton: HighlightableButtonNode private let resultsLabel: TextNode private let activityIndicator: ActivityIndicator @@ -44,6 +45,7 @@ final class ChatSearchInputPanelNode: ChatInputPanelNode { self.downButton = HighlightableButtonNode() self.downButton.isEnabled = false self.calendarButton = HighlightableButtonNode() + self.membersButton = HighlightableButtonNode() self.resultsLabel = TextNode() self.resultsLabel.isLayerBacked = true self.resultsLabel.displaysAsynchronously = false @@ -55,12 +57,14 @@ final class ChatSearchInputPanelNode: ChatInputPanelNode { self.addSubnode(self.upButton) self.addSubnode(self.downButton) self.addSubnode(self.calendarButton) + self.addSubnode(self.membersButton) self.addSubnode(self.resultsLabel) self.addSubnode(self.activityIndicator) self.upButton.addTarget(self, action: #selector(self.upPressed), forControlEvents: [.touchUpInside]) self.downButton.addTarget(self, action: #selector(self.downPressed), forControlEvents: [.touchUpInside]) self.calendarButton.addTarget(self, action: #selector(self.calendarPressed), forControlEvents: [.touchUpInside]) + self.membersButton.addTarget(self, action: #selector(self.membersPressed), forControlEvents: [.touchUpInside]) } deinit { @@ -79,7 +83,11 @@ final class ChatSearchInputPanelNode: ChatInputPanelNode { self.interfaceInteraction?.openCalendarSearch() } - override func updateLayout(width: CGFloat, maxHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { + @objc func membersPressed() { + self.interfaceInteraction?.toggleMembersSearch(true) + } + + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { if self.presentationInterfaceState != interfaceState { let themeUpdated = self.presentationInterfaceState?.theme !== interfaceState.theme @@ -91,14 +99,17 @@ final class ChatSearchInputPanelNode: ChatInputPanelNode { self.downButton.setImage(PresentationResourcesChat.chatInputSearchPanelDownImage(interfaceState.theme), for: [.normal]) self.downButton.setImage(PresentationResourcesChat.chatInputSearchPanelDownDisabledImage(interfaceState.theme), for: [.disabled]) self.calendarButton.setImage(PresentationResourcesChat.chatInputSearchPanelCalendarImage(interfaceState.theme), for: []) + + self.membersButton.setImage(PresentationResourcesChat.chatInputSearchPanelMembersImage(interfaceState.theme), for: []) } } let panelHeight: CGFloat = 47.0 - transition.updateFrame(node: self.downButton, frame: CGRect(origin: CGPoint(x: 12.0, y: 0.0), size: CGSize(width: 40.0, height: panelHeight))) - transition.updateFrame(node: self.upButton, frame: CGRect(origin: CGPoint(x: 12.0 + 43.0, y: 0.0), size: CGSize(width: 40.0, height: panelHeight))) - transition.updateFrame(node: self.calendarButton, frame: CGRect(origin: CGPoint(x: width - 60.0, y: 0.0), size: CGSize(width: 60.0, height: panelHeight))) + transition.updateFrame(node: self.downButton, frame: CGRect(origin: CGPoint(x: leftInset + 12.0, y: 0.0), size: CGSize(width: 40.0, height: panelHeight))) + transition.updateFrame(node: self.upButton, frame: CGRect(origin: CGPoint(x: leftInset + 12.0 + 43.0, y: 0.0), size: CGSize(width: 40.0, height: panelHeight))) + transition.updateFrame(node: self.calendarButton, frame: CGRect(origin: CGPoint(x: width - rightInset - 60.0, y: 0.0), size: CGSize(width: 60.0, height: panelHeight))) + transition.updateFrame(node: self.membersButton, frame: CGRect(origin: CGPoint(x: width - rightInset - 60.0 * 2.0, y: 0.0), size: CGSize(width: 60.0, height: panelHeight))) var resultIndex: Int? var resultCount: Int? @@ -117,13 +128,27 @@ final class ChatSearchInputPanelNode: ChatInputPanelNode { self.downButton.isEnabled = resultIndex != nil && resultCount != nil && resultIndex != resultCount! - 1 self.calendarButton.isHidden = (!(interfaceState.search?.query.isEmpty ?? true)) || self.displayActivity + var canSearchMembers = false + if let search = interfaceState.search { + if case .everything = search.domain { + if let _ = interfaceState.peer as? TelegramGroup { + canSearchMembers = true + } else if let peer = interfaceState.peer as? TelegramChannel, case .group = peer.info { + canSearchMembers = true + } + } else { + canSearchMembers = false + } + } + self.membersButton.isHidden = (!(interfaceState.search?.query.isEmpty ?? true)) || self.displayActivity || !canSearchMembers + let makeLabelLayout = TextNode.asyncLayout(self.resultsLabel) - let (labelSize, labelApply) = makeLabelLayout(resultsText, nil, 1, .end, CGSize(width: 200.0, height: 100.0), .left, nil, UIEdgeInsets()) + let (labelSize, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: resultsText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - leftInset - rightInset - 50.0, height: 100.0), alignment: .left, cutout: nil, insets: UIEdgeInsets())) let _ = labelApply() - self.resultsLabel.frame = CGRect(origin: CGPoint(x: 105.0, y: floor((panelHeight - labelSize.size.height) / 2.0)), size: labelSize.size) + self.resultsLabel.frame = CGRect(origin: CGPoint(x: leftInset + 105.0, y: floor((panelHeight - labelSize.size.height) / 2.0)), size: labelSize.size) let indicatorSize = self.activityIndicator.measure(CGSize(width: 22.0, height: 22.0)) - self.activityIndicator.frame = CGRect(origin: CGPoint(x: width - 41.0, y: floor((panelHeight - indicatorSize.height) / 2.0)), size: indicatorSize) + self.activityIndicator.frame = CGRect(origin: CGPoint(x: width - rightInset - 41.0, y: floor((panelHeight - indicatorSize.height) / 2.0)), size: indicatorSize) return panelHeight } diff --git a/TelegramUI/ChatSearchNavigationContentNode.swift b/TelegramUI/ChatSearchNavigationContentNode.swift index fe6ce3bd73..eb7763d7b6 100644 --- a/TelegramUI/ChatSearchNavigationContentNode.swift +++ b/TelegramUI/ChatSearchNavigationContentNode.swift @@ -1,14 +1,21 @@ import Foundation import AsyncDisplayKit import Display +import Postbox +import TelegramCore -private let searchBarFont = Font.regular(15.0) +private let searchBarFont = Font.regular(14.0) final class ChatSearchNavigationContentNode: NavigationBarContentNode { + private let theme: PresentationTheme + private let strings: PresentationStrings + private let searchBar: SearchBarNode private let interaction: ChatPanelInterfaceInteraction init(theme: PresentationTheme, strings: PresentationStrings, interaction: ChatPanelInterfaceInteraction) { + self.theme = theme + self.strings = strings self.interaction = interaction self.searchBar = SearchBarNode(theme: theme, strings: strings) @@ -26,6 +33,10 @@ final class ChatSearchNavigationContentNode: NavigationBarContentNode { self.searchBar.textUpdated = { [weak self] query in self?.interaction.updateMessageSearch(query) } + + self.searchBar.clearPrefix = { [weak self] in + self?.interaction.toggleMembersSearch(false) + } } override func layout() { @@ -33,8 +44,9 @@ final class ChatSearchNavigationContentNode: NavigationBarContentNode { let size = self.bounds.size - let searchBarFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height - 64.0), size: CGSize(width: size.width, height: 64.0)) + let searchBarFrame = CGRect(origin: CGPoint(), size: size) self.searchBar.frame = searchBarFrame + self.searchBar.updateLayout(boundingSize: size, leftInset: 0.0, rightInset: 0.0, transition: .immediate) } func activate() { @@ -44,4 +56,28 @@ final class ChatSearchNavigationContentNode: NavigationBarContentNode { func deactivate() { self.searchBar.deactivate(clear: false) } + + func update(presentationInterfaceState: ChatPresentationInterfaceState) { + if let search = presentationInterfaceState.search { + switch search.domain { + case .everything: + self.searchBar.prefixString = nil + self.searchBar.placeholderString = NSAttributedString(string: strings.Conversation_SearchPlaceholder, font: searchBarFont, textColor: theme.rootController.activeNavigationSearchBar.inputPlaceholderTextColor) + case .members: + self.searchBar.prefixString = NSAttributedString(string: strings.Conversation_SearchByName_Prefix, font: searchBarFont, textColor: theme.rootController.activeNavigationSearchBar.inputTextColor) + self.searchBar.placeholderString = nil + case let .member(peer): + let prefixString = NSMutableAttributedString() + prefixString.append(NSAttributedString(string: self.strings.Conversation_SearchByName_Prefix, font: searchBarFont, textColor: theme.rootController.activeNavigationSearchBar.inputTextColor)) + prefixString.append(NSAttributedString(string: "\(peer.compactDisplayTitle) ", font: searchBarFont, textColor: theme.rootController.activeNavigationSearchBar.accentColor)) + self.searchBar.prefixString = prefixString + self.searchBar.placeholderString = nil + } + + if self.searchBar.text != search.query { + self.searchBar.text = search.query + self.interaction.updateMessageSearch(search.query) + } + } + } } diff --git a/TelegramUI/ChatSwipeToReplyRecognizer.swift b/TelegramUI/ChatSwipeToReplyRecognizer.swift new file mode 100644 index 0000000000..db98237633 --- /dev/null +++ b/TelegramUI/ChatSwipeToReplyRecognizer.swift @@ -0,0 +1,54 @@ +import Foundation +import UIKit + +class ChatSwipeToReplyRecognizer: UIPanGestureRecognizer { + var validatedGesture = false + var firstLocation: CGPoint = CGPoint() + + var shouldBegin: (() -> Bool)? + + override init(target: Any?, action: Selector?) { + super.init(target: target, action: action) + + self.maximumNumberOfTouches = 1 + } + + override func reset() { + super.reset() + + self.validatedGesture = false + } + + override func touchesBegan(_ touches: Set, with event: UIEvent) { + super.touchesBegan(touches, with: event) + + if let shouldBegin = self.shouldBegin, !shouldBegin() { + self.state = .failed + } else { + let touch = touches.first! + self.firstLocation = touch.location(in: self.view) + } + } + + override func touchesMoved(_ touches: Set, with event: UIEvent) { + let location = touches.first!.location(in: self.view) + let translation = CGPoint(x: location.x - firstLocation.x, y: location.y - firstLocation.y) + + let absTranslationX: CGFloat = abs(translation.x) + let absTranslationY: CGFloat = abs(translation.y) + + if !validatedGesture { + if translation.x > 0.0 { + self.state = .failed + } else if absTranslationY > 2.0 && absTranslationY > absTranslationX * 2.0 { + self.state = .failed + } else if absTranslationX > 2.0 && absTranslationY * 2.0 < absTranslationX { + validatedGesture = true + } + } + + if validatedGesture { + super.touchesMoved(touches, with: event) + } + } +} diff --git a/TelegramUI/ChatTextInputAudioRecordingCancelIndicator.swift b/TelegramUI/ChatTextInputAudioRecordingCancelIndicator.swift index 2b3edf1b09..af885e0ef6 100644 --- a/TelegramUI/ChatTextInputAudioRecordingCancelIndicator.swift +++ b/TelegramUI/ChatTextInputAudioRecordingCancelIndicator.swift @@ -23,6 +23,7 @@ final class ChatTextInputAudioRecordingCancelIndicator: ASDisplayNode { self.arrowNode.image = PresentationResourcesChat.chatInputPanelMediaRecordingCancelArrowImage(theme) self.labelNode = TextNode() + self.labelNode.displaysAsynchronously = false self.labelNode.isLayerBacked = true self.cancelButton = HighlightableButtonNode() @@ -36,14 +37,14 @@ final class ChatTextInputAudioRecordingCancelIndicator: ASDisplayNode { self.addSubnode(self.cancelButton) let makeLayout = TextNode.asyncLayout(self.labelNode) - let (labelLayout, labelApply) = makeLayout(NSAttributedString(string: strings.Conversation_SlideToCancel, font: Font.regular(14.0), textColor: theme.chat.inputPanel.panelControlColor), nil, 1, .end, CGSize(width: 200.0, height: 100.0), .natural, nil, UIEdgeInsets()) + let (labelLayout, labelApply) = makeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: strings.Conversation_SlideToCancel, font: Font.regular(14.0), textColor: theme.chat.inputPanel.panelControlColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 200.0, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let _ = labelApply() let arrowSize = self.arrowNode.image?.size ?? CGSize() let height = max(arrowSize.height, labelLayout.size.height) self.frame = CGRect(origin: CGPoint(), size: CGSize(width: arrowSize.width + 12.0 + labelLayout.size.width, height: height)) self.arrowNode.frame = CGRect(origin: CGPoint(x: 0.0, y: floor((height - arrowSize.height) / 2.0)), size: arrowSize) - self.labelNode.frame = CGRect(origin: CGPoint(x: arrowSize.width + 6.0, y: floor((height - labelLayout.size.height) / 2.0) - UIScreenPixel), size: labelLayout.size) + self.labelNode.frame = CGRect(origin: CGPoint(x: arrowSize.width + 6.0, y: 1.0 + floor((height - labelLayout.size.height) / 2.0)), size: labelLayout.size) let cancelSize = self.cancelButton.measure(CGSize(width: 200.0, height: 100.0)) self.cancelButton.frame = CGRect(origin: CGPoint(x: floor((self.bounds.size.width - cancelSize.width) / 2.0), y: floor((height - cancelSize.height) / 2.0)), size: cancelSize) diff --git a/TelegramUI/ChatTextInputAudioRecordingTimeNode.swift b/TelegramUI/ChatTextInputAudioRecordingTimeNode.swift index a964217ef4..12c6c77bb4 100644 --- a/TelegramUI/ChatTextInputAudioRecordingTimeNode.swift +++ b/TelegramUI/ChatTextInputAudioRecordingTimeNode.swift @@ -70,7 +70,7 @@ final class ChatTextInputAudioRecordingTimeNode: ASDisplayNode { override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { let makeLayout = TextNode.asyncLayout(self.textNode) - let (size, apply) = makeLayout(NSAttributedString(string: "00:00,00", font: Font.regular(15.0), textColor: theme.chat.inputPanel.primaryTextColor), nil, 1, .end, CGSize(width: 200.0, height: 100.0), .natural, nil, UIEdgeInsets()) + let (size, apply) = makeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "00:00,00", font: Font.regular(15.0), textColor: theme.chat.inputPanel.primaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 200.0, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let _ = apply() self.textNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 1.0 + UIScreenPixel), size: size.size) return size.size diff --git a/TelegramUI/ChatTextInputMediaRecordingButton.swift b/TelegramUI/ChatTextInputMediaRecordingButton.swift index 80245473e1..01daff7f56 100644 --- a/TelegramUI/ChatTextInputMediaRecordingButton.swift +++ b/TelegramUI/ChatTextInputMediaRecordingButton.swift @@ -218,14 +218,14 @@ final class ChatTextInputMediaRecordingButton: TGModernConversationInputMicButto self.innerIconView = UIImageView() self.presentController = presentController - let mediaRecordingControl = theme.chat.inputPanel.mediaRecordingControl - super.init(theme: TGModernConversationInputMicButtonTheme(buttonColor: mediaRecordingControl.buttonColor, micLevel: mediaRecordingControl.micLevelColor, buttonControlColor: mediaRecordingControl.activeIconColor, panelControlFill: mediaRecordingControl.panelControlFillColor, panelControlStroke: mediaRecordingControl.panelControlStrokeColor, panelControlContentPrimaryColor: mediaRecordingControl.panelControlContentPrimaryColor, panelControlContentAccentColor: mediaRecordingControl.panelControlContentAccentColor)) + super.init(frame: CGRect()) + + let inputPanelTheme = theme.chat.inputPanel + + self.pallete = TGModernConversationInputMicPallete(dark: theme.overallDarkAppearance, buttonColor: inputPanelTheme.actionControlFillColor, iconColor: inputPanelTheme.actionControlForegroundColor, backgroundColor: inputPanelTheme.panelBackgroundColor, borderColor: inputPanelTheme.panelStrokeColor, lock: inputPanelTheme.panelControlAccentColor, textColor: inputPanelTheme.primaryTextColor, secondaryTextColor: inputPanelTheme.secondaryTextColor, recording: inputPanelTheme.mediaRecordingDotColor) self.insertSubview(self.innerIconView, at: 0) - self.isExclusiveTouch = true - self.adjustsImageWhenHighlighted = false - self.adjustsImageWhenDisabled = false self.disablesInteractiveTransitionGestureRecognizer = true self.updateMode(mode: self.mode, animated: false, force: true) @@ -305,66 +305,6 @@ final class ChatTextInputMediaRecordingButton: TGModernConversationInputMicButto self.isEnabled = true } - /*override func beginTracking(_ touch: UITouch, with touchEvent: UIEvent?) -> Bool { - if super.beginTracking(touch, with: touchEvent) { - self.startTouchLocation = touch.location(in: self) - - self.controlsOffset = 0.0 - self.beginRecording() - let recordingOverlay: ChatTextInputAudioRecordingOverlay - if let currentRecordingOverlay = self.recordingOverlay { - recordingOverlay = currentRecordingOverlay - } else { - recordingOverlay = ChatTextInputAudioRecordingOverlay(anchorView: self) - self.recordingOverlay = recordingOverlay - } - if let account = self.account, let applicationContext = account.applicationContext as? TelegramApplicationContext, let topWindow = applicationContext.applicationBindings.getTopWindow() { - recordingOverlay.present(in: topWindow) - } - return true - } else { - return false - } - } - - override func endTracking(_ touch: UITouch?, with touchEvent: UIEvent?) { - super.endTracking(touch, with: touchEvent) - - self.endRecording(self.controlsOffset < 40.0) - self.dismissRecordingOverlay() - } - - override func cancelTracking(with event: UIEvent?) { - super.cancelTracking(with: event) - - self.endRecording(false) - self.dismissRecordingOverlay() - } - - override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { - if super.continueTracking(touch, with: event) { - if let startTouchLocation = self.startTouchLocation { - let horiontalOffset = startTouchLocation.x - touch.location(in: self).x - let controlsOffset = max(0.0, horiontalOffset - offsetThreshold) - if !controlsOffset.isEqual(to: self.controlsOffset) { - self.recordingOverlay?.dismissFactor = 1.0 - controlsOffset / dismissOffsetThreshold - self.controlsOffset = controlsOffset - self.offsetRecordingControls() - } - } - return true - } else { - return false - } - } - - private func dismissRecordingOverlay() { - if let recordingOverlay = self.recordingOverlay { - self.recordingOverlay = nil - recordingOverlay.dismiss() - } - }*/ - func micButtonInteractionBegan() { self.modeTimeoutTimer?.invalidate() let modeTimeoutTimer = SwiftSignalKit.Timer(timeout: 0.1, repeat: false, completion: { [weak self] in diff --git a/TelegramUI/ChatTextInputPanelNode.swift b/TelegramUI/ChatTextInputPanelNode.swift index bd57692f03..03552010a3 100644 --- a/TelegramUI/ChatTextInputPanelNode.swift +++ b/TelegramUI/ChatTextInputPanelNode.swift @@ -4,6 +4,7 @@ import Display import AsyncDisplayKit import Postbox import TelegramCore +import MobileCoreServices private let searchLayoutProgressImage = generateImage(CGSize(width: 22.0, height: 22.0), contextGenerator: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) @@ -83,11 +84,14 @@ private final class AccessoryItemIconButton: HighlightableButton { class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { var textPlaceholderNode: TextNode var contextPlaceholderNode: TextNode? + let textInputContainer: ASDisplayNode var textInputNode: ASEditableTextNode? let textInputBackgroundView: UIImageView let micButton: ChatTextInputMediaRecordingButton let sendButton: HighlightableButton + private var sendButtonHasApplyIcon = false + private var animatingSendButton = false let attachmentButton: HighlightableButton let searchLayoutClearButton: HighlightableButton let searchLayoutProgressView: UIImageView @@ -98,10 +102,11 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { private var accessoryItemButtons: [(ChatTextInputAccessoryItem, AccessoryItemIconButton)] = [] - private var validLayout: (CGFloat, CGFloat)? + private var validLayout: (CGFloat, CGFloat, CGFloat, CGFloat)? var displayAttachmentMenu: () -> Void = { } var sendMessage: () -> Void = { } + var pasteImages: ([UIImage]) -> Void = { _ in } var updateHeight: () -> Void = { } var updateActivity: () -> Void = { } @@ -184,6 +189,10 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { private let accessoryButtonInset: CGFloat = 4.0 + UIScreenPixel init(theme: PresentationTheme, presentController: @escaping (ViewController) -> Void) { + self.textInputContainer = ASDisplayNode() + self.textInputContainer.clipsToBounds = true + self.textInputContainer.backgroundColor = theme.chat.inputPanel.inputBackgroundColor + self.textInputBackgroundView = UIImageView() self.textPlaceholderNode = TextNode() self.textPlaceholderNode.isLayerBacked = true @@ -212,14 +221,18 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } self.micButton.endRecording = { [weak self] sendMedia in - if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction { - interfaceInteraction.finishMediaRecording(sendMedia) + if let strongSelf = self, let interfaceState = strongSelf.presentationInterfaceState, let interfaceInteraction = strongSelf.interfaceInteraction, let _ = interfaceState.inputTextPanelState.mediaRecordingState { + if sendMedia { + interfaceInteraction.finishMediaRecording(.send) + } else { + interfaceInteraction.finishMediaRecording(.dismiss) + } } } self.micButton.offsetRecordingControls = { [weak self] in if let strongSelf = self, let presentationInterfaceState = strongSelf.presentationInterfaceState { - if let (width, maxHeight) = strongSelf.validLayout { - let _ = strongSelf.updateLayout(width: width, maxHeight: maxHeight, transition: .immediate, interfaceState: presentationInterfaceState) + if let (width, leftInset, rightInset, maxHeight) = strongSelf.validLayout { + let _ = strongSelf.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, maxHeight: maxHeight, transition: .immediate, interfaceState: presentationInterfaceState) } } } @@ -249,6 +262,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.searchLayoutClearButton.addSubview(self.searchLayoutProgressView) + self.addSubnode(self.textInputContainer) self.view.addSubview(self.textInputBackgroundView) self.addSubnode(self.textPlaceholderNode) @@ -273,9 +287,13 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { private func loadTextInputNode() { let textInputNode = ASEditableTextNode() var textColor: UIColor = .black + var tintColor: UIColor = .blue + var baseFontSize: CGFloat = 17.0 var keyboardAppearance: UIKeyboardAppearance = UIKeyboardAppearance.default if let presentationInterfaceState = self.presentationInterfaceState { textColor = presentationInterfaceState.theme.chat.inputPanel.inputTextColor + tintColor = presentationInterfaceState.theme.list.itemAccentColor + //baseFontSize = presentationInterfaceState.fontSize.baseDisplaySize switch presentationInterfaceState.theme.chat.inputPanel.keyboardColor { case .light: keyboardAppearance = .default @@ -291,16 +309,35 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { paragraphStyle.maximumLineHeight = 20.0 paragraphStyle.minimumLineHeight = 20.0 - textInputNode.typingAttributes = [NSAttributedStringKey.font.rawValue: Font.regular(17.0), NSAttributedStringKey.foregroundColor.rawValue: textColor, NSAttributedStringKey.paragraphStyle.rawValue: paragraphStyle] - textInputNode.clipsToBounds = true + textInputNode.typingAttributes = [NSAttributedStringKey.font.rawValue: Font.regular(max(17.0, baseFontSize)), NSAttributedStringKey.foregroundColor.rawValue: textColor, NSAttributedStringKey.paragraphStyle.rawValue: paragraphStyle] + textInputNode.clipsToBounds = false + textInputNode.textView.clipsToBounds = false textInputNode.delegate = self textInputNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0) textInputNode.keyboardAppearance = keyboardAppearance textInputNode.textContainerInset = UIEdgeInsets(top: self.textInputViewRealInsets.top, left: 0.0, bottom: self.textInputViewRealInsets.bottom, right: 0.0) - self.addSubnode(textInputNode) + textInputNode.tintColor = tintColor + textInputNode.textView.scrollIndicatorInsets = UIEdgeInsets(top: 9.0, left: 0.0, bottom: 9.0, right: -13.0) + self.textInputContainer.addSubnode(textInputNode) self.textInputNode = textInputNode - textInputNode.frame = CGRect(x: self.textFieldInsets.left + self.textInputViewInternalInsets.left, y: self.textFieldInsets.top + self.textInputViewInternalInsets.top, width: self.frame.size.width - self.textFieldInsets.left - self.textFieldInsets.right - self.textInputViewInternalInsets.left - self.textInputViewInternalInsets.right, height: self.frame.size.height - self.textFieldInsets.top - self.textFieldInsets.bottom - self.textInputViewInternalInsets.top - self.textInputViewInternalInsets.bottom) + if !self.textInputContainer.bounds.size.width.isZero { + let textInputFrame = self.textInputContainer.frame + + var accessoryButtonsWidth: CGFloat = 0.0 + var firstButton = true + for (_, button) in self.accessoryItemButtons { + if firstButton { + firstButton = false + accessoryButtonsWidth += accessoryButtonInset + } else { + accessoryButtonsWidth += accessoryButtonSpacing + } + accessoryButtonsWidth += button.buttonWidth + } + + textInputNode.frame = CGRect(origin: CGPoint(x: self.textInputViewInternalInsets.left, y: self.textInputViewInternalInsets.top), size: CGSize(width: textInputFrame.size.width - (self.textInputViewInternalInsets.left + self.textInputViewInternalInsets.right + accessoryButtonsWidth), height: textInputFrame.size.height - self.textInputViewInternalInsets.top - self.textInputViewInternalInsets.bottom)) + } self.textInputBackgroundView.isUserInteractionEnabled = false self.textInputBackgroundView.removeGestureRecognizer(self.textInputBackgroundView.gestureRecognizers![0]) @@ -342,9 +379,6 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { let maxNumberOfLines = min(12, (Int(fieldMaxHeight - 11.0) - 33) / 22) - //let numberOfLines = Int((unboundTextFieldHeight - 11.0) + 11.0) / 22 - //unboundTextFieldHeight = CGFloat(numberOfLines) * 22.0 + 11.0 - let updatedMaxHeight = (CGFloat(maxNumberOfLines) * 22.0 + 10.0) textFieldHeight = min(updatedMaxHeight, unboundTextFieldHeight) @@ -359,13 +393,20 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { return textFieldHeight + self.textFieldInsets.top + self.textFieldInsets.bottom + self.textInputViewInternalInsets.top + self.textInputViewInternalInsets.bottom } - override func updateLayout(width: CGFloat, maxHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { - self.validLayout = (width, maxHeight) + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { + self.validLayout = (width, leftInset, rightInset, maxHeight) + let baseWidth = width - leftInset - rightInset if self.presentationInterfaceState != interfaceState { let previousState = self.presentationInterfaceState self.presentationInterfaceState = interfaceState + var updateSendButtonIcon = false + if (previousState?.interfaceState.editMessage != nil) != (interfaceState.interfaceState.editMessage != nil) { + updateSendButtonIcon = true + } if self.theme !== interfaceState.theme { + updateSendButtonIcon = true + if self.theme == nil || !self.theme!.chat.inputPanel.inputTextColor.isEqual(interfaceState.theme.chat.inputPanel.inputTextColor) { let textColor = interfaceState.theme.chat.inputPanel.inputTextColor @@ -388,11 +429,13 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } self.textInputNode?.keyboardAppearance = keyboardAppearance + self.textInputContainer.backgroundColor = interfaceState.theme.chat.inputPanel.inputBackgroundColor + self.theme = interfaceState.theme self.attachmentButton.setImage(PresentationResourcesChat.chatInputPanelAttachmentButtonImage(interfaceState.theme), for: []) - self.sendButton.setImage(PresentationResourcesChat.chatInputPanelSendButtonImage(interfaceState.theme), for: []) + self.micButton.updateTheme(theme: interfaceState.theme) self.textInputBackgroundView.image = PresentationResourcesChat.chatInputTextFieldBackgroundImage(interfaceState.theme) @@ -427,11 +470,36 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { if self.currentPlaceholder != placeholder { self.currentPlaceholder = placeholder let placeholderLayout = TextNode.asyncLayout(self.textPlaceholderNode) - let (placeholderSize, placeholderApply) = placeholderLayout(NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: interfaceState.theme.chat.inputPanel.inputPlaceholderColor), nil, 1, .end, CGSize(width: 320.0, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let (placeholderSize, placeholderApply) = placeholderLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: interfaceState.theme.chat.inputPanel.inputPlaceholderColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 320.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) self.textPlaceholderNode.frame = CGRect(origin: self.textPlaceholderNode.frame.origin, size: placeholderSize.size) let _ = placeholderApply() } } + + let sendButtonHasApplyIcon = interfaceState.interfaceState.editMessage != nil + + if updateSendButtonIcon { + if !self.animatingSendButton { + if transition.isAnimated && !self.sendButton.alpha.isZero && self.sendButton.layer.animation(forKey: "opacity") == nil, let imageView = self.sendButton.imageView, let previousImage = imageView.image { + let tempView = UIImageView(image: previousImage) + self.sendButton.addSubview(tempView) + tempView.frame = imageView.frame + tempView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak tempView] _ in + tempView?.removeFromSuperview() + }) + tempView.layer.animateScale(from: 1.0, to: 0.2, duration: 0.2, removeOnCompletion: false) + + imageView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + imageView.layer.animateScale(from: 0.2, to: 1.0, duration: 0.2) + } + self.sendButtonHasApplyIcon = sendButtonHasApplyIcon + if self.sendButtonHasApplyIcon { + self.sendButton.setImage(PresentationResourcesChat.chatInputPanelApplyButtonImage(interfaceState.theme), for: []) + } else { + self.sendButton.setImage(PresentationResourcesChat.chatInputPanelSendButtonImage(interfaceState.theme), for: []) + } + } + } } let minimalHeight: CGFloat = 47.0 @@ -486,7 +554,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.accessoryItemButtons = updatedButtons } - let (accessoryButtonsWidth, textFieldHeight) = self.calculateTextFieldMetrics(width: width, maxHeight: maxHeight) + let (accessoryButtonsWidth, textFieldHeight) = self.calculateTextFieldMetrics(width: baseWidth, maxHeight: maxHeight) let panelHeight = self.panelHeight(textFieldHeight: textFieldHeight) self.micButton.updateMode(mode: interfaceState.interfaceState.mediaRecordingMode, animated: transition.isAnimated) @@ -523,13 +591,13 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { animateCancelSlideIn = transition.isAnimated audioRecordingCancelIndicator = ChatTextInputAudioRecordingCancelIndicator(theme: interfaceState.theme, strings: interfaceState.strings, cancel: { [weak self] in - self?.interfaceInteraction?.finishMediaRecording(false) + self?.interfaceInteraction?.finishMediaRecording(.dismiss) }) self.audioRecordingCancelIndicator = audioRecordingCancelIndicator self.insertSubnode(audioRecordingCancelIndicator, at: 0) } - audioRecordingCancelIndicator.frame = CGRect(origin: CGPoint(x: floor((width - audioRecordingCancelIndicator.bounds.size.width) / 2.0) - self.micButton.controlsOffset, y: panelHeight - minimalHeight + floor((minimalHeight - audioRecordingCancelIndicator.bounds.size.height) / 2.0)), size: audioRecordingCancelIndicator.bounds.size) + audioRecordingCancelIndicator.frame = CGRect(origin: CGPoint(x: leftInset + floor((baseWidth - audioRecordingCancelIndicator.bounds.size.width) / 2.0) - self.micButton.controlsOffset, y: panelHeight - minimalHeight + floor((minimalHeight - audioRecordingCancelIndicator.bounds.size.height) / 2.0)), size: audioRecordingCancelIndicator.bounds.size) if animateCancelSlideIn { let position = audioRecordingCancelIndicator.layer.position @@ -554,7 +622,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { let audioRecordingTimeSize = audioRecordingTimeNode.measure(CGSize(width: 200.0, height: 100.0)) - audioRecordingInfoContainerNode.frame = CGRect(origin: CGPoint(x: min(0.0, audioRecordingCancelIndicator.frame.minX - audioRecordingTimeSize.width - 8.0 - 28.0), y: 0.0), size: CGSize(width: width, height: panelHeight)) + audioRecordingInfoContainerNode.frame = CGRect(origin: CGPoint(x: min(leftInset, audioRecordingCancelIndicator.frame.minX - audioRecordingTimeSize.width - 8.0 - 28.0), y: 0.0), size: CGSize(width: baseWidth, height: panelHeight)) audioRecordingTimeNode.frame = CGRect(origin: CGPoint(x: 28.0, y: panelHeight - minimalHeight + floor((minimalHeight - audioRecordingTimeSize.height) / 2.0)), size: audioRecordingTimeSize) if animateTimeSlideIn { @@ -647,7 +715,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } - transition.updateFrame(layer: self.attachmentButton.layer, frame: CGRect(origin: CGPoint(x: 2.0 - UIScreenPixel, y: panelHeight - minimalHeight + audioRecordingItemsVerticalOffset), size: CGSize(width: 40.0, height: minimalHeight))) + transition.updateFrame(layer: self.attachmentButton.layer, frame: CGRect(origin: CGPoint(x: leftInset + 2.0 - UIScreenPixel, y: panelHeight - minimalHeight + audioRecordingItemsVerticalOffset), size: CGSize(width: 40.0, height: minimalHeight))) var composeButtonsOffset: CGFloat = 0.0 var textInputBackgroundWidthOffset: CGFloat = 0.0 @@ -656,24 +724,31 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { textInputBackgroundWidthOffset = 36.0 } + transition.updateFrame(layer: self.micButton.layer, frame: CGRect(origin: CGPoint(x: width - rightInset - 43.0 - UIScreenPixel + composeButtonsOffset, y: panelHeight - minimalHeight - UIScreenPixel), size: CGSize(width: 44.0, height: minimalHeight))) self.micButton.layoutItems() - transition.updateFrame(layer: self.micButton.layer, frame: CGRect(origin: CGPoint(x: width - 43.0 - UIScreenPixel + composeButtonsOffset, y: panelHeight - minimalHeight - UIScreenPixel), size: CGSize(width: 44.0, height: minimalHeight))) - transition.updateFrame(layer: self.sendButton.layer, frame: CGRect(origin: CGPoint(x: width - 43.0 - UIScreenPixel + composeButtonsOffset, y: panelHeight - minimalHeight - UIScreenPixel), size: CGSize(width: 44.0, height: minimalHeight))) + transition.updateFrame(layer: self.sendButton.layer, frame: CGRect(origin: CGPoint(x: width - rightInset - 43.0 - UIScreenPixel + composeButtonsOffset, y: panelHeight - minimalHeight - UIScreenPixel), size: CGSize(width: 44.0, height: minimalHeight))) let searchLayoutClearButtonSize = CGSize(width: 44.0, height: minimalHeight) - transition.updateFrame(layer: self.searchLayoutClearButton.layer, frame: CGRect(origin: CGPoint(x: width - self.textFieldInsets.left - self.textFieldInsets.right + textInputBackgroundWidthOffset + 3.0, y: panelHeight - minimalHeight), size: searchLayoutClearButtonSize)) + transition.updateFrame(layer: self.searchLayoutClearButton.layer, frame: CGRect(origin: CGPoint(x: width - rightInset - self.textFieldInsets.left - self.textFieldInsets.right + textInputBackgroundWidthOffset + 3.0, y: panelHeight - minimalHeight), size: searchLayoutClearButtonSize)) let searchProgressSize = self.searchLayoutProgressView.bounds.size transition.updateFrame(layer: self.searchLayoutProgressView.layer, frame: CGRect(origin: CGPoint(x: floor((searchLayoutClearButtonSize.width - searchProgressSize.width) / 2.0), y: floor((searchLayoutClearButtonSize.height - searchProgressSize.height) / 2.0)), size: searchProgressSize)) + let textInputFrame = CGRect(x: leftInset + self.textFieldInsets.left, y: self.textFieldInsets.top + audioRecordingItemsVerticalOffset, width: baseWidth - self.textFieldInsets.left - self.textFieldInsets.right, height: panelHeight - self.textFieldInsets.top - self.textFieldInsets.bottom) + transition.updateFrame(node: self.textInputContainer, frame: textInputFrame) + if let textInputNode = self.textInputNode { - transition.updateFrame(node: textInputNode, frame: CGRect(x: self.textFieldInsets.left + self.textInputViewInternalInsets.left, y: self.textFieldInsets.top + self.textInputViewInternalInsets.top + audioRecordingItemsVerticalOffset, width: width - self.textFieldInsets.left - self.textFieldInsets.right - self.textInputViewInternalInsets.left - self.textInputViewInternalInsets.right - accessoryButtonsWidth, height: panelHeight - self.textFieldInsets.top - self.textFieldInsets.bottom - self.textInputViewInternalInsets.top - self.textInputViewInternalInsets.bottom)) - textInputNode.layout() + let textFieldFrame = CGRect(origin: CGPoint(x: self.textInputViewInternalInsets.left, y: self.textInputViewInternalInsets.top), size: CGSize(width: textInputFrame.size.width - (self.textInputViewInternalInsets.left + self.textInputViewInternalInsets.right + accessoryButtonsWidth), height: textInputFrame.size.height - self.textInputViewInternalInsets.top - textInputViewInternalInsets.bottom)) + let shouldUpdateLayout = textFieldFrame.size != textInputNode.frame.size + transition.updateFrame(node: textInputNode, frame: textFieldFrame) + if shouldUpdateLayout { + textInputNode.layout() + } } if let contextPlaceholder = interfaceState.inputTextPanelState.contextPlaceholder { let placeholderLayout = TextNode.asyncLayout(self.contextPlaceholderNode) - let (placeholderSize, placeholderApply) = placeholderLayout(contextPlaceholder, nil, 1, .end, CGSize(width: width - self.textFieldInsets.left - self.textFieldInsets.right - self.textInputViewInternalInsets.left - self.textInputViewInternalInsets.right - accessoryButtonsWidth, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let (placeholderSize, placeholderApply) = placeholderLayout(TextNodeLayoutArguments(attributedString: contextPlaceholder, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - leftInset - rightInset - self.textFieldInsets.left - self.textFieldInsets.right - self.textInputViewInternalInsets.left - self.textInputViewInternalInsets.right - accessoryButtonsWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let contextPlaceholderNode = placeholderApply() if let currentContextPlaceholderNode = self.contextPlaceholderNode, currentContextPlaceholderNode !== contextPlaceholderNode { self.contextPlaceholderNode = nil @@ -681,23 +756,33 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } if self.contextPlaceholderNode !== contextPlaceholderNode { + contextPlaceholderNode.displaysAsynchronously = false self.contextPlaceholderNode = contextPlaceholderNode self.insertSubnode(contextPlaceholderNode, aboveSubnode: self.textPlaceholderNode) } let _ = placeholderApply() - contextPlaceholderNode.frame = CGRect(origin: CGPoint(x: self.textFieldInsets.left + self.textInputViewInternalInsets.left, y: self.textFieldInsets.top + self.textInputViewInternalInsets.top + self.textInputViewRealInsets.top + audioRecordingItemsVerticalOffset), size: placeholderSize.size) + contextPlaceholderNode.frame = CGRect(origin: CGPoint(x: leftInset + self.textFieldInsets.left + self.textInputViewInternalInsets.left, y: self.textFieldInsets.top + self.textInputViewInternalInsets.top + self.textInputViewRealInsets.top + audioRecordingItemsVerticalOffset + UIScreenPixel), size: placeholderSize.size) + + self.textPlaceholderNode.isHidden = true } else if let contextPlaceholderNode = self.contextPlaceholderNode { self.contextPlaceholderNode = nil contextPlaceholderNode.removeFromSupernode() + self.textPlaceholderNode.alpha = 1.0 + + var hasText = false + if let textInputNode = self.textInputNode, let attributedText = textInputNode.attributedText, attributedText.length != 0 { + hasText = true + } + self.textPlaceholderNode.isHidden = hasText } - transition.updateFrame(node: self.textPlaceholderNode, frame: CGRect(origin: CGPoint(x: self.textFieldInsets.left + self.textInputViewInternalInsets.left, y: self.textFieldInsets.top + self.textInputViewInternalInsets.top + self.textInputViewRealInsets.top + audioRecordingItemsVerticalOffset), size: self.textPlaceholderNode.frame.size)) + transition.updateFrame(node: self.textPlaceholderNode, frame: CGRect(origin: CGPoint(x: leftInset + self.textFieldInsets.left + self.textInputViewInternalInsets.left, y: self.textFieldInsets.top + self.textInputViewInternalInsets.top + self.textInputViewRealInsets.top + audioRecordingItemsVerticalOffset + UIScreenPixel), size: self.textPlaceholderNode.frame.size)) - transition.updateFrame(layer: self.textInputBackgroundView.layer, frame: CGRect(x: self.textFieldInsets.left, y: self.textFieldInsets.top + audioRecordingItemsVerticalOffset, width: width - self.textFieldInsets.left - self.textFieldInsets.right + textInputBackgroundWidthOffset, height: panelHeight - self.textFieldInsets.top - self.textFieldInsets.bottom)) + transition.updateFrame(layer: self.textInputBackgroundView.layer, frame: CGRect(x: leftInset + self.textFieldInsets.left, y: self.textFieldInsets.top + audioRecordingItemsVerticalOffset, width: baseWidth - self.textFieldInsets.left - self.textFieldInsets.right + textInputBackgroundWidthOffset, height: panelHeight - self.textFieldInsets.top - self.textFieldInsets.bottom)) - var nextButtonTopRight = CGPoint(x: width - self.textFieldInsets.right - accessoryButtonInset, y: panelHeight - self.textFieldInsets.bottom - minimalInputHeight + audioRecordingItemsVerticalOffset) + var nextButtonTopRight = CGPoint(x: width - rightInset - self.textFieldInsets.right - accessoryButtonInset, y: panelHeight - self.textFieldInsets.bottom - minimalInputHeight + audioRecordingItemsVerticalOffset) for (_, button) in self.accessoryItemButtons.reversed() { let buttonSize = CGSize(width: button.buttonWidth, height: minimalInputHeight) let buttonFrame = CGRect(origin: CGPoint(x: nextButtonTopRight.x - buttonSize.width, y: nextButtonTopRight.y + floor((minimalInputHeight - buttonSize.height) / 2.0)), size: buttonSize) @@ -780,7 +865,13 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { if !self.sendButton.alpha.isZero { self.sendButton.alpha = 0.0 if animated { - self.sendButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + self.animatingSendButton = true + self.sendButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] _ in + if let strongSelf = self { + strongSelf.animatingSendButton = false + strongSelf.applyUpdateSendButtonIcon() + } + }) self.sendButton.layer.animateScale(from: 1.0, to: 0.2, duration: 0.2) } } @@ -819,7 +910,13 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { if !self.sendButton.alpha.isZero { self.sendButton.alpha = 0.0 if animated { - self.sendButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + self.animatingSendButton = true + self.sendButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] _ in + if let strongSelf = self { + strongSelf.animatingSendButton = false + strongSelf.applyUpdateSendButtonIcon() + } + }) } } } @@ -846,8 +943,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } - if let (width, maxHeight) = self.validLayout { - let (_, textFieldHeight) = self.calculateTextFieldMetrics(width: width, maxHeight: maxHeight) + if let (width, leftInset, rightInset, maxHeight) = self.validLayout { + let (_, textFieldHeight) = self.calculateTextFieldMetrics(width: width - leftInset - rightInset, maxHeight: maxHeight) let panelHeight = self.panelHeight(textFieldHeight: textFieldHeight) if !self.bounds.size.height.isEqual(to: panelHeight) { self.updateHeight() @@ -855,6 +952,21 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } + private func applyUpdateSendButtonIcon() { + if let interfaceState = self.presentationInterfaceState { + let sendButtonHasApplyIcon = interfaceState.interfaceState.editMessage != nil + + if sendButtonHasApplyIcon != self.sendButtonHasApplyIcon { + self.sendButtonHasApplyIcon = sendButtonHasApplyIcon + if self.sendButtonHasApplyIcon { + self.sendButton.setImage(PresentationResourcesChat.chatInputPanelApplyButtonImage(interfaceState.theme), for: []) + } else { + self.sendButton.setImage(PresentationResourcesChat.chatInputPanelSendButtonImage(interfaceState.theme), for: []) + } + } + } + } + @objc func editableTextNodeDidChangeSelection(_ editableTextNode: ASEditableTextNode, fromSelectedRange: NSRange, toSelectedRange: NSRange, dueToEditing: Bool) { if !dueToEditing && !updatingInputState { let inputTextState = self.inputTextState @@ -863,9 +975,24 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } @objc func editableTextNodeDidBeginEditing(_ editableTextNode: ASEditableTextNode) { + var activateGifInput = false + if let presentationInterfaceState = self.presentationInterfaceState { + if case .media(.gif) = presentationInterfaceState.inputMode { + activateGifInput = true + } + } self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in return (.text, state.keyboardButtonsMessage?.id) }) + if activateGifInput { + self.interfaceInteraction?.updateTextInputState { state in + if state.inputText.isEmpty { + return ChatTextInputState(inputText: "@gif ") + } else { + return state + } + } + } } @objc func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { @@ -873,6 +1000,46 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { return true } + @objc func editableTextNodeShouldPaste(_ editableTextNode: ASEditableTextNode) -> Bool { + var images: [UIImage] = [] + var text: String? + + for item in UIPasteboard.general.items { + if let image = item[kUTTypeJPEG as String] as? UIImage { + images.append(image) + } else if let image = item[kUTTypePNG as String] as? UIImage { + images.append(image) + } else if let image = item[kUTTypeGIF as String] as? UIImage { + images.append(image) + } + } + + if !images.isEmpty { + self.pasteImages(images) + + return false + } else { + return true + } + + /*for (NSDictionary *item in pasteBoard.items) { + if (item[(__bridge NSString *)kUTTypeJPEG] != nil) { + [images addObject:item[(__bridge NSString *)kUTTypeJPEG]]; + } else if (item[(__bridge NSString *)kUTTypePNG] != nil) { + [images addObject:item[(__bridge NSString *)kUTTypePNG]]; + } else if (item[(__bridge NSString *)kUTTypeGIF] != nil) { + [images addObject:item[(__bridge NSString *)kUTTypeGIF]]; + } else if (item[(__bridge NSString *)kUTTypeURL] != nil) { + id url = item[(__bridge NSString *)kUTTypeURL]; + if ([url respondsToSelector:@selector(characterAtIndex:)]) { + text = url; + } else if ([url isKindOfClass:[NSURL class]]) { + text = ((NSURL *)url).absoluteString; + } + } + }*/ + } + @objc func sendButtonPressed() { self.sendMessage() } @@ -884,16 +1051,20 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { @objc func searchLayoutClearButtonPressed() { if let interfaceInteraction = self.interfaceInteraction { interfaceInteraction.updateTextInputState { textInputState in - if let (_, type, queryRange) = textInputStateContextQueryRangeAndType(textInputState), type == [.contextRequest] { - if let queryRange = queryRange, !queryRange.isEmpty { - var inputText = textInputState.inputText - inputText.replaceSubrange(queryRange, with: "") - return ChatTextInputState(inputText: inputText) - } else { - return ChatTextInputState(inputText: "") + var mentionQueryRange: Range? + inner: for (_, type, queryRange) in textInputStateContextQueryRangeAndType(textInputState) { + if type == [.contextRequest] { + mentionQueryRange = queryRange + break inner } } - return textInputState + if let mentionQueryRange = mentionQueryRange, !mentionQueryRange.isEmpty { + var inputText = textInputState.inputText + inputText.replaceSubrange(mentionQueryRange, with: "") + return ChatTextInputState(inputText: inputText) + } else { + return ChatTextInputState(inputText: "") + } } } } @@ -926,7 +1097,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { switch item { case .stickers: self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in - return (.media, state.interfaceState.messageActionsState.closedButtonKeyboardMessageId) + return (.media(.other), state.interfaceState.messageActionsState.closedButtonKeyboardMessageId) }) case .keyboard: self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in @@ -950,6 +1121,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { return result } } - return super.hitTest(point, with: event) + let result = super.hitTest(point, with: event) + return result } } diff --git a/TelegramUI/ChatTitleAccessoryPanelNode.swift b/TelegramUI/ChatTitleAccessoryPanelNode.swift index f7c65927e3..1046b14e40 100644 --- a/TelegramUI/ChatTitleAccessoryPanelNode.swift +++ b/TelegramUI/ChatTitleAccessoryPanelNode.swift @@ -5,7 +5,7 @@ import AsyncDisplayKit class ChatTitleAccessoryPanelNode: ASDisplayNode { var interfaceInteraction: ChatPanelInterfaceInteraction? - func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { + func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { return 0.0 } } diff --git a/TelegramUI/ChatTitleView.swift b/TelegramUI/ChatTitleView.swift index 9fff43278e..e6294a3add 100644 --- a/TelegramUI/ChatTitleView.swift +++ b/TelegramUI/ChatTitleView.swift @@ -6,15 +6,74 @@ import TelegramCore import SwiftSignalKit import LegacyComponents +enum ChatTitleContent { + case peer(PeerView) + case group([Peer]) +} + +private final class ChatTitleNetworkStatusNode: ASDisplayNode { + private var theme: PresentationTheme + + private let titleNode: ASTextNode + private let activityIndicator: ActivityIndicator + + var title: String = "" { + didSet { + if self.title != oldValue { + self.titleNode.attributedText = NSAttributedString(string: title, font: Font.bold(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor) + } + } + } + + init(theme: PresentationTheme) { + self.theme = theme + + self.titleNode = ASTextNode() + self.titleNode.isLayerBacked = true + self.titleNode.displaysAsynchronously = false + self.titleNode.maximumNumberOfLines = 1 + self.titleNode.truncationMode = .byTruncatingTail + self.titleNode.isOpaque = false + self.titleNode.isUserInteractionEnabled = false + + self.activityIndicator = ActivityIndicator(type: .custom(theme.rootController.navigationBar.secondaryTextColor, 22.0), speed: .slow) + let activityIndicatorSize = self.activityIndicator.measure(CGSize(width: 100.0, height: 100.0)) + self.activityIndicator.frame = CGRect(origin: CGPoint(), size: activityIndicatorSize) + + super.init() + + self.addSubnode(self.titleNode) + self.addSubnode(self.activityIndicator) + } + + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + let indicatorSize = self.activityIndicator.bounds.size + let indicatorPadding = indicatorSize.width + 6.0 + + let titleSize = self.titleNode.measure(CGSize(width: max(1.0, size.width - indicatorPadding), height: size.height)) + let combinedHeight = titleSize.height + + let titleFrame = CGRect(origin: CGPoint(x: indicatorPadding + floor((size.width - titleSize.width - indicatorPadding) / 2.0), y: floor((size.height - combinedHeight) / 2.0)), size: titleSize) + transition.updateFrame(node: self.titleNode, frame: titleFrame) + + transition.updateFrame(node: self.activityIndicator, frame: CGRect(origin: CGPoint(x: titleFrame.minX - indicatorSize.width - 6.0, y: titleFrame.minY - 1.0), size: indicatorSize)) + } +} + final class ChatTitleView: UIView, NavigationBarTitleView { + private let account: Account + private var theme: PresentationTheme private var strings: PresentationStrings + private let contentContainer: ASDisplayNode private let titleNode: ASTextNode private let infoNode: ASTextNode private let typingNode: ASTextNode private var typingIndicator: TGModernConversationTitleActivityIndicator? - private let button: HighlightTrackingButton + private let button: HighlightTrackingButtonNode + + private var networkStatusNode: ChatTitleNetworkStatusNode? private var presenceManager: PeerPresenceStatusManager? @@ -34,10 +93,22 @@ final class ChatTitleView: UIView, NavigationBarTitleView { } if peerId.namespace == Namespaces.Peer.CloudUser || peerId.namespace == Namespaces.Peer.SecretChat { switch mergedActivity { + case .typingText: + stringValue = strings.Conversation_typing + case .uploadingFile: + stringValue = strings.Activity_UploadingDocument case .recordingVoice: stringValue = strings.Activity_RecordingAudio - default: - stringValue = strings.Conversation_typing + case .uploadingPhoto: + stringValue = strings.Activity_UploadingPhoto + case .uploadingVideo: + stringValue = strings.Activity_UploadingVideo + case .playingGame: + stringValue = strings.Activity_PlayingGame + case .recordingInstantVideo: + stringValue = strings.Activity_RecordingVideoMessage + case .uploadingInstantVideo: + stringValue = strings.Activity_UploadingVideoMessage } } else { for (peer, _) in inputActivities { @@ -60,7 +131,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView { if self.typingIndicator == nil { let typingIndicator = TGModernConversationTitleActivityIndicator() typingIndicator.setColor(self.theme.rootController.navigationBar.accentTextColor) - self.addSubview(typingIndicator) + self.contentContainer.view.addSubview(typingIndicator) self.typingIndicator = typingIndicator } switch mergedActivity { @@ -76,6 +147,10 @@ final class ChatTitleView: UIView, NavigationBarTitleView { self.typingIndicator?.setAudioRecording() case .uploadingInstantVideo: self.typingIndicator?.setUploading() + case .uploadingPhoto: + self.typingIndicator?.setUploading() + case .uploadingVideo: + self.typingIndicator?.setUploading() } } else { self.typingNode.isHidden = true @@ -89,14 +164,63 @@ final class ChatTitleView: UIView, NavigationBarTitleView { } } + var networkState: AccountNetworkState = .online { + didSet { + if self.networkState != oldValue { + if case .online = self.networkState { + self.contentContainer.isHidden = false + if let networkStatusNode = self.networkStatusNode { + self.networkStatusNode = nil + networkStatusNode.removeFromSupernode() + } + } else { + self.contentContainer.isHidden = true + let statusNode: ChatTitleNetworkStatusNode + if let current = self.networkStatusNode { + statusNode = current + } else { + statusNode = ChatTitleNetworkStatusNode(theme: self.theme) + self.networkStatusNode = statusNode + self.insertSubview(statusNode.view, belowSubview: self.button.view) + } + switch self.networkState { + case .waitingForNetwork: + statusNode.title = self.strings.State_WaitingForNetwork + case let .connecting(toProxy): + statusNode.title = toProxy ? self.strings.State_ConnectingToProxy : self.strings.State_Connecting + case .updating: + statusNode.title = self.strings.State_Updating + case .online: + break + } + + } + + self.setNeedsLayout() + } + } + } + var pressed: (() -> Void)? - var peerView: PeerView? { + var titleContent: ChatTitleContent? { didSet { - if let peerView = self.peerView, let peer = peerViewMainPeer(peerView) { - let string = NSAttributedString(string: peer.displayTitle, font: Font.medium(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor) + if let titleContent = self.titleContent { + var string: NSAttributedString? + switch titleContent { + case let .peer(peerView): + if let peer = peerViewMainPeer(peerView) { + if peerView.peerId == self.account.peerId { + string = NSAttributedString(string: self.strings.Conversation_SavedMessages, font: Font.medium(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor) + } else { + string = NSAttributedString(string: peer.displayTitle, font: Font.medium(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor) + } + } + case .group: + string = NSAttributedString(string: "Feed", font: Font.medium(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor) + } - if self.titleNode.attributedText == nil || !self.titleNode.attributedText!.isEqual(to: string) { + if let string = string, self.titleNode.attributedText == nil || !self.titleNode.attributedText!.isEqual(to: string) { self.titleNode.attributedText = string self.setNeedsLayout() } @@ -108,86 +232,99 @@ final class ChatTitleView: UIView, NavigationBarTitleView { private func updateStatus() { var shouldUpdateLayout = false - if let peerView = self.peerView, let peer = peerViewMainPeer(peerView) { - if let user = peer as? TelegramUser { - if let _ = user.botInfo { - let string = NSAttributedString(string: self.strings.Bot_GenericBotStatus, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) - if self.infoNode.attributedText == nil || !self.infoNode.attributedText!.isEqual(to: string) { - self.infoNode.attributedText = string - shouldUpdateLayout = true - } - } else if let peer = peerViewMainPeer(peerView), let presence = peerView.peerPresences[peer.id] as? TelegramUserPresence { - let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 - let (string, activity) = stringAndActivityForUserPresence(strings: self.strings, presence: presence, relativeTo: Int32(timestamp)) - let attributedString = NSAttributedString(string: string, font: Font.regular(13.0), textColor: activity ? self.theme.rootController.navigationBar.accentTextColor : self.theme.rootController.navigationBar.secondaryTextColor) - if self.infoNode.attributedText == nil || !self.infoNode.attributedText!.isEqual(to: attributedString) { - self.infoNode.attributedText = attributedString - shouldUpdateLayout = true - } - - self.presenceManager?.reset(presence: presence) - } else { - let string = NSAttributedString(string: strings.Presence_offline, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) - if self.infoNode.attributedText == nil || !self.infoNode.attributedText!.isEqual(to: string) { - self.infoNode.attributedText = string - shouldUpdateLayout = true - } - } - } else if let group = peer as? TelegramGroup { - var onlineCount = 0 - if let cachedGroupData = peerView.cachedData as? CachedGroupData, let participants = cachedGroupData.participants { - let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 - for participant in participants.participants { - if let presence = peerView.peerPresences[participant.peerId] as? TelegramUserPresence { - let relativeStatus = relativeUserPresenceStatus(presence, relativeTo: Int32(timestamp)) - switch relativeStatus { - case .online: - onlineCount += 1 - default: - break + if let titleContent = self.titleContent { + switch titleContent { + case let .peer(peerView): + if let peer = peerViewMainPeer(peerView) { + if peer.id == self.account.peerId { + let string = NSAttributedString(string: "", font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) + if self.infoNode.attributedText == nil || !self.infoNode.attributedText!.isEqual(to: string) { + self.infoNode.attributedText = string + shouldUpdateLayout = true + } + } else if let user = peer as? TelegramUser { + if let _ = user.botInfo { + let string = NSAttributedString(string: self.strings.Bot_GenericBotStatus, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) + if self.infoNode.attributedText == nil || !self.infoNode.attributedText!.isEqual(to: string) { + self.infoNode.attributedText = string + shouldUpdateLayout = true + } + } else if let peer = peerViewMainPeer(peerView), let presence = peerView.peerPresences[peer.id] as? TelegramUserPresence { + let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 + let (string, activity) = stringAndActivityForUserPresence(strings: self.strings, timeFormat: .regular, presence: presence, relativeTo: Int32(timestamp)) + let attributedString = NSAttributedString(string: string, font: Font.regular(13.0), textColor: activity ? self.theme.rootController.navigationBar.accentTextColor : self.theme.rootController.navigationBar.secondaryTextColor) + if self.infoNode.attributedText == nil || !self.infoNode.attributedText!.isEqual(to: attributedString) { + self.infoNode.attributedText = attributedString + shouldUpdateLayout = true + } + + self.presenceManager?.reset(presence: presence) + } else { + let string = NSAttributedString(string: strings.LastSeen_ALongTimeAgo, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) + if self.infoNode.attributedText == nil || !self.infoNode.attributedText!.isEqual(to: string) { + self.infoNode.attributedText = string + shouldUpdateLayout = true + } + } + } else if let group = peer as? TelegramGroup { + var onlineCount = 0 + if let cachedGroupData = peerView.cachedData as? CachedGroupData, let participants = cachedGroupData.participants { + let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 + for participant in participants.participants { + if let presence = peerView.peerPresences[participant.peerId] as? TelegramUserPresence { + let relativeStatus = relativeUserPresenceStatus(presence, relativeTo: Int32(timestamp)) + switch relativeStatus { + case .online: + onlineCount += 1 + default: + break + } + } + } + } + if onlineCount > 1 { + let string = NSMutableAttributedString() + + string.append(NSAttributedString(string: "\(strings.Conversation_StatusMembers(Int32(group.participantCount))), ", font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)) + string.append(NSAttributedString(string: strings.Conversation_StatusOnline(Int32(onlineCount)), font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)) + if self.infoNode.attributedText == nil || !self.infoNode.attributedText!.isEqual(to: string) { + self.infoNode.attributedText = string + shouldUpdateLayout = true + } + } else { + let string = NSAttributedString(string: strings.Conversation_StatusMembers(Int32(group.participantCount)), font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) + if self.infoNode.attributedText == nil || !self.infoNode.attributedText!.isEqual(to: string) { + self.infoNode.attributedText = string + shouldUpdateLayout = true + } + } + } else if let channel = peer as? TelegramChannel { + if let cachedChannelData = peerView.cachedData as? CachedChannelData, let memberCount = cachedChannelData.participantsSummary.memberCount { + let string = NSAttributedString(string: strings.Conversation_StatusMembers(memberCount), font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) + if self.infoNode.attributedText == nil || !self.infoNode.attributedText!.isEqual(to: string) { + self.infoNode.attributedText = string + shouldUpdateLayout = true + } + } else { + switch channel.info { + case .group: + let string = NSAttributedString(string: strings.Group_Status, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) + if self.infoNode.attributedText == nil || !self.infoNode.attributedText!.isEqual(to: string) { + self.infoNode.attributedText = string + shouldUpdateLayout = true + } + case .broadcast: + let string = NSAttributedString(string: strings.Channel_Status, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) + if self.infoNode.attributedText == nil || !self.infoNode.attributedText!.isEqual(to: string) { + self.infoNode.attributedText = string + shouldUpdateLayout = true + } + } } } } - } - if onlineCount > 1 { - let string = NSMutableAttributedString() - - string.append(NSAttributedString(string: "\(strings.Conversation_StatusMembers(Int32(group.participantCount))), ", font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)) - string.append(NSAttributedString(string: strings.Conversation_StatusOnline(Int32(onlineCount)), font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)) - if self.infoNode.attributedText == nil || !self.infoNode.attributedText!.isEqual(to: string) { - self.infoNode.attributedText = string - shouldUpdateLayout = true - } - } else { - let string = NSAttributedString(string: strings.Conversation_StatusMembers(Int32(group.participantCount)), font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) - if self.infoNode.attributedText == nil || !self.infoNode.attributedText!.isEqual(to: string) { - self.infoNode.attributedText = string - shouldUpdateLayout = true - } - } - } else if let channel = peer as? TelegramChannel { - if let cachedChannelData = peerView.cachedData as? CachedChannelData, let memberCount = cachedChannelData.participantsSummary.memberCount { - let string = NSAttributedString(string: strings.Conversation_StatusMembers(memberCount), font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) - if self.infoNode.attributedText == nil || !self.infoNode.attributedText!.isEqual(to: string) { - self.infoNode.attributedText = string - shouldUpdateLayout = true - } - } else { - switch channel.info { - case .group: - let string = NSAttributedString(string: strings.Group_Status, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) - if self.infoNode.attributedText == nil || !self.infoNode.attributedText!.isEqual(to: string) { - self.infoNode.attributedText = string - shouldUpdateLayout = true - } - case .broadcast: - let string = NSAttributedString(string: strings.Channel_Status, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) - if self.infoNode.attributedText == nil || !self.infoNode.attributedText!.isEqual(to: string) { - self.infoNode.attributedText = string - shouldUpdateLayout = true - } - } - } + case .group: + break } if shouldUpdateLayout { @@ -196,10 +333,13 @@ final class ChatTitleView: UIView, NavigationBarTitleView { } } - init(theme: PresentationTheme, strings: PresentationStrings) { + init(account: Account, theme: PresentationTheme, strings: PresentationStrings) { + self.account = account self.theme = theme self.strings = strings + self.contentContainer = ASDisplayNode() + self.titleNode = ASTextNode() self.titleNode.displaysAsynchronously = false self.titleNode.maximumNumberOfLines = 1 @@ -218,20 +358,21 @@ final class ChatTitleView: UIView, NavigationBarTitleView { self.typingNode.truncationMode = .byTruncatingTail self.typingNode.isOpaque = false - self.button = HighlightTrackingButton() + self.button = HighlightTrackingButtonNode() super.init(frame: CGRect()) - self.addSubnode(self.titleNode) - self.addSubnode(self.infoNode) - self.addSubnode(self.typingNode) - self.addSubview(self.button) + self.addSubnode(self.contentContainer) + self.contentContainer.addSubnode(self.titleNode) + self.contentContainer.addSubnode(self.infoNode) + self.contentContainer.addSubnode(self.typingNode) + self.addSubnode(self.button) self.presenceManager = PeerPresenceStatusManager(update: { [weak self] in self?.updateStatus() }) - self.button.addTarget(self, action: #selector(buttonPressed), for: [.touchUpInside]) + self.button.addTarget(self, action: #selector(buttonPressed), forControlEvents: [.touchUpInside]) self.button.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { if highlighted { @@ -261,8 +402,10 @@ final class ChatTitleView: UIView, NavigationBarTitleView { super.layoutSubviews() let size = self.bounds.size + let transition: ContainedViewLayoutTransition = .immediate self.button.frame = CGRect(origin: CGPoint(), size: size) + self.contentContainer.frame = CGRect(origin: CGPoint(), size: size) if size.height > 40.0 { let titleSize = self.titleNode.measure(size) @@ -270,14 +413,18 @@ final class ChatTitleView: UIView, NavigationBarTitleView { let typingSize = self.typingNode.measure(size) let titleInfoSpacing: CGFloat = 0.0 - let combinedHeight = titleSize.height + infoSize.height + titleInfoSpacing - - self.titleNode.frame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: floor((size.height - combinedHeight) / 2.0)), size: titleSize) - self.infoNode.frame = CGRect(origin: CGPoint(x: floor((size.width - infoSize.width) / 2.0), y: floor((size.height - combinedHeight) / 2.0) + titleSize.height + titleInfoSpacing), size: infoSize) - self.typingNode.frame = CGRect(origin: CGPoint(x: floor((size.width - typingSize.width + 14.0) / 2.0), y: floor((size.height - combinedHeight) / 2.0) + titleSize.height + titleInfoSpacing), size: typingSize) - - if let typingIndicator = self.typingIndicator { - typingIndicator.frame = CGRect(x: self.typingNode.frame.origin.x - 24.0, y: self.typingNode.frame.origin.y, width: 24.0, height: 16.0) + if infoSize.width.isZero && typingSize.width.isZero { + self.titleNode.frame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: floor((size.height - titleSize.height) / 2.0)), size: titleSize) + } else { + let combinedHeight = titleSize.height + infoSize.height + titleInfoSpacing + + self.titleNode.frame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: floor((size.height - combinedHeight) / 2.0)), size: titleSize) + self.infoNode.frame = CGRect(origin: CGPoint(x: floor((size.width - infoSize.width) / 2.0), y: floor((size.height - combinedHeight) / 2.0) + titleSize.height + titleInfoSpacing), size: infoSize) + self.typingNode.frame = CGRect(origin: CGPoint(x: floor((size.width - typingSize.width + 14.0) / 2.0), y: floor((size.height - combinedHeight) / 2.0) + titleSize.height + titleInfoSpacing), size: typingSize) + + if let typingIndicator = self.typingIndicator { + typingIndicator.frame = CGRect(x: self.typingNode.frame.origin.x - 24.0, y: self.typingNode.frame.origin.y, width: 24.0, height: 16.0) + } } } else { let titleSize = self.titleNode.measure(CGSize(width: floor(size.width / 2.0), height: size.height)) @@ -291,6 +438,11 @@ final class ChatTitleView: UIView, NavigationBarTitleView { self.infoNode.frame = CGRect(origin: CGPoint(x: floor((size.width - combinedWidth) / 2.0 + titleSize.width + titleInfoSpacing), y: floor((size.height - infoSize.height) / 2.0)), size: infoSize) self.typingNode.frame = CGRect(origin: CGPoint(x: floor((size.width - combinedWidth) / 2.0 + titleSize.width + titleInfoSpacing), y: floor((size.height - typingSize.height) / 2.0)), size: typingSize) } + + if let networkStatusNode = self.networkStatusNode { + transition.updateFrame(node: networkStatusNode, frame: CGRect(origin: CGPoint(), size: size)) + networkStatusNode.updateLayout(size: size, transition: transition) + } } @objc func buttonPressed() { diff --git a/TelegramUI/ChatToastAlertPanelNode.swift b/TelegramUI/ChatToastAlertPanelNode.swift index ebf7f60b06..ba7c96f09e 100644 --- a/TelegramUI/ChatToastAlertPanelNode.swift +++ b/TelegramUI/ChatToastAlertPanelNode.swift @@ -6,10 +6,18 @@ final class ChatToastAlertPanelNode: ChatTitleAccessoryPanelNode { private let separatorNode: ASDisplayNode private let titleNode: ASTextNode + private var textColor: UIColor = .black { + didSet { + if !self.textColor.isEqual(oldValue) { + self.titleNode.attributedText = NSAttributedString(string: self.text, font: Font.regular(14.0), textColor: self.textColor) + } + } + } + var text: String = "" { didSet { if self.text != oldValue { - self.titleNode.attributedText = NSAttributedString(string: text, font: Font.regular(14.0), textColor: UIColor.black) + self.titleNode.attributedText = NSAttributedString(string: text, font: Font.regular(14.0), textColor: self.textColor) self.setNeedsLayout() } } @@ -17,7 +25,6 @@ final class ChatToastAlertPanelNode: ChatTitleAccessoryPanelNode { override init() { self.separatorNode = ASDisplayNode() - self.separatorNode.backgroundColor = UIColor(red: 0.6953125, green: 0.6953125, blue: 0.6953125, alpha: 1.0) self.separatorNode.isLayerBacked = true self.titleNode = ASTextNode() @@ -26,25 +33,22 @@ final class ChatToastAlertPanelNode: ChatTitleAccessoryPanelNode { super.init() - self.backgroundColor = UIColor(rgb: 0xF5F6F8) - self.addSubnode(self.titleNode) self.addSubnode(self.separatorNode) } - override func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { let panelHeight: CGFloat = 40.0 + + self.textColor = interfaceState.theme.rootController.navigationBar.primaryTextColor + self.backgroundColor = interfaceState.theme.rootController.navigationBar.backgroundColor + self.separatorNode.backgroundColor = interfaceState.theme.rootController.navigationBar.separatorColor transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelHeight - UIScreenPixel), size: CGSize(width: width, height: UIScreenPixel))) - self.setNeedsLayout() + + let titleSize = self.titleNode.measure(CGSize(width: width - leftInset - rightInset - 20.0, height: 100.0)) + self.titleNode.frame = CGRect(origin: CGPoint(x: floor((width - titleSize.width) / 2.0), y: floor((panelHeight - titleSize.height) / 2.0)), size: titleSize) return panelHeight } - - override func layout() { - super.layout() - - let titleSize = self.titleNode.measure(CGSize(width: self.bounds.size.width - 20.0, height: 100.0)) - self.titleNode.frame = CGRect(origin: CGPoint(x: floor((self.bounds.size.width - titleSize.width) / 2.0), y: floor((self.bounds.size.height - titleSize.height) / 2.0)), size: titleSize) - } } diff --git a/TelegramUI/ChatUnblockInputPanelNode.swift b/TelegramUI/ChatUnblockInputPanelNode.swift index 0ac2749eef..1915c3463b 100644 --- a/TelegramUI/ChatUnblockInputPanelNode.swift +++ b/TelegramUI/ChatUnblockInputPanelNode.swift @@ -80,19 +80,19 @@ final class ChatUnblockInputPanelNode: ChatInputPanelNode { self.interfaceInteraction?.unblockPeer() } - override func updateLayout(width: CGFloat, maxHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { if self.presentationInterfaceState != interfaceState { self.presentationInterfaceState = interfaceState } - let buttonSize = self.button.measure(CGSize(width: width - 80.0, height: 100.0)) + let buttonSize = self.button.measure(CGSize(width: width - leftInset - rightInset - 80.0, height: 100.0)) let panelHeight: CGFloat = 47.0 - self.button.frame = CGRect(origin: CGPoint(x: floor((width - buttonSize.width) / 2.0), y: floor((panelHeight - buttonSize.height) / 2.0)), size: buttonSize) + self.button.frame = CGRect(origin: CGPoint(x: leftInset + floor((width - leftInset - rightInset - buttonSize.width) / 2.0), y: floor((panelHeight - buttonSize.height) / 2.0)), size: buttonSize) let indicatorSize = self.activityIndicator.bounds.size - self.activityIndicator.frame = CGRect(origin: CGPoint(x: width - indicatorSize.width - 12.0, y: floor((panelHeight - indicatorSize.height) / 2.0)), size: indicatorSize) + self.activityIndicator.frame = CGRect(origin: CGPoint(x: width - rightInset - indicatorSize.width - 12.0, y: floor((panelHeight - indicatorSize.height) / 2.0)), size: indicatorSize) return 47.0 } diff --git a/TelegramUI/ChatUnreadItem.swift b/TelegramUI/ChatUnreadItem.swift index 1ac0d1c590..5c8c035476 100644 --- a/TelegramUI/ChatUnreadItem.swift +++ b/TelegramUI/ChatUnreadItem.swift @@ -20,19 +20,35 @@ class ChatUnreadItem: ListViewItem { self.header = ChatMessageDateHeader(timestamp: index.timestamp, theme: theme, strings: strings) } - func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { async { let node = ChatUnreadItemNode() - node.layoutForWidth(width, item: self, previousItem: previousItem, nextItem: nextItem) + node.layoutForParams(params, item: self, previousItem: previousItem, nextItem: nextItem) completion(node, { return (nil, {}) }) } } - func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { - completion(ListViewItemNodeLayout(contentSize: node.contentSize, insets: node.insets), { - }) + public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + if let node = node as? ChatUnreadItemNode { + Queue.mainQueue().async { + let nodeLayout = node.asyncLayout() + + async { + let dateAtBottom = !chatItemsHaveCommonDateHeader(self, nextItem) + + let (layout, apply) = nodeLayout(self, params, dateAtBottom) + Queue.mainQueue().async { + completion(layout, { + apply() + }) + } + } + } + } else { + assertionFailure() + } } } @@ -75,32 +91,32 @@ class ChatUnreadItemNode: ListViewItemNode { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } - override func layoutForWidth(_ width: CGFloat, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { + override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { if let item = item as? ChatUnreadItem { let dateAtBottom = !chatItemsHaveCommonDateHeader(item, nextItem) - let (layout, apply) = self.asyncLayout()(item, width, dateAtBottom) + let (layout, apply) = self.asyncLayout()(item, params, dateAtBottom) apply() self.contentSize = layout.contentSize self.insets = layout.insets } } - func asyncLayout() -> (_ item: ChatUnreadItem, _ width: CGFloat, _ dateAtBottom: Bool) -> (ListViewItemNodeLayout, () -> Void) { + func asyncLayout() -> (_ item: ChatUnreadItem, _ params: ListViewItemLayoutParams, _ dateAtBottom: Bool) -> (ListViewItemNodeLayout, () -> Void) { let labelLayout = TextNode.asyncLayout(self.labelNode) let layoutConstants = self.layoutConstants let currentTheme = self.theme - return { item, width, dateAtBottom in + return { item, params, dateAtBottom in var updatedBackgroundImage: UIImage? if currentTheme !== item.theme { updatedBackgroundImage = PresentationResourcesChat.chatUnreadBarBackgroundImage(item.theme) } - let (size, apply) = labelLayout(NSAttributedString(string: item.strings.Conversation_UnreadMessages, font: titleFont, textColor: item.theme.chat.serviceMessage.unreadBarTextColor), nil, 1, .end, CGSize(width: width, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let (size, apply) = labelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.strings.Conversation_UnreadMessages, font: titleFont, textColor: item.theme.chat.serviceMessage.unreadBarTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let backgroundSize = CGSize(width: width, height: 25.0) + let backgroundSize = CGSize(width: params.width, height: 25.0) - return (ListViewItemNodeLayout(contentSize: CGSize(width: width, height: 25.0), insets: UIEdgeInsets(top: 6.0 + (dateAtBottom ? layoutConstants.timestampHeaderHeight : 0.0), left: 0.0, bottom: 5.0, right: 0.0)), { [weak self] in + return (ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 25.0), insets: UIEdgeInsets(top: 6.0 + (dateAtBottom ? layoutConstants.timestampHeaderHeight : 0.0), left: 0.0, bottom: 5.0, right: 0.0)), { [weak self] in if let strongSelf = self { strongSelf.item = item strongSelf.theme = item.theme @@ -112,7 +128,7 @@ class ChatUnreadItemNode: ListViewItemNode { let _ = apply() strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: backgroundSize) - strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - size.size.width) / 2.0), y: floorToScreenPixels((backgroundSize.height - size.size.height) / 2.0) - 1.0), size: size.size) + strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - size.size.width) / 2.0), y: floorToScreenPixels((backgroundSize.height - size.size.height) / 2.0)), size: size.size) } }) } diff --git a/TelegramUI/ChatVideoGalleryItem.swift b/TelegramUI/ChatVideoGalleryItem.swift deleted file mode 100644 index 3aa7cb57b3..0000000000 --- a/TelegramUI/ChatVideoGalleryItem.swift +++ /dev/null @@ -1,489 +0,0 @@ -import Foundation -import Display -import AsyncDisplayKit -import SwiftSignalKit -import Postbox -import TelegramCore - -class ChatVideoGalleryItem: GalleryItem { - let account: Account - let theme: PresentationTheme - let strings: PresentationStrings - let message: Message - let location: MessageHistoryEntryLocation? - - init(account: Account, theme: PresentationTheme, strings: PresentationStrings, message: Message, location: MessageHistoryEntryLocation?) { - self.account = account - self.theme = theme - self.strings = strings - self.message = message - self.location = location - } - - func node() -> GalleryItemNode { - let node = ChatVideoGalleryItemNode(account: self.account, theme: self.theme, strings: self.strings) - - for media in self.message.media { - if let file = media as? TelegramMediaFile, (file.isVideo || file.mimeType.hasPrefix("video/")) { - node.setFile(account: account, stableId: self.message.stableId, file: file, loopVideo: file.isAnimated || self.message.containsSecretMedia) - break - } else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { - if let file = content.file, (file.isVideo || file.mimeType.hasPrefix("video/")) { - node.setFile(account: account, stableId: self.message.stableId, file: file, loopVideo: file.isAnimated || self.message.containsSecretMedia) - break - } - } - } - - if let location = self.location { - node._title.set(.single("\(location.index + 1) of \(location.count)")) - } - node.setMessage(self.message) - - return node - } - - func updateNode(node: GalleryItemNode) { - if let node = node as? ChatVideoGalleryItemNode, let location = self.location { - node._title.set(.single("\(location.index + 1) of \(location.count)")) - node.setMessage(self.message) - } - } -} - -private let pictureInPictureButtonImage = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/PictureInPictureButton"), color: .white) - -final class ChatVideoGalleryItemNode: ZoomableContentGalleryItemNode { - fileprivate let _ready = Promise() - fileprivate let _title = Promise() - fileprivate let _titleView = Promise() - fileprivate let _rightBarButtonItem = Promise() - - private var videoNode: TelegramVideoNode? - private let scrubberView: ChatVideoGalleryItemScrubberView - - private let progressButtonNode: HighlightableButtonNode - private let progressNode: RadialProgressNode - - private var accountAndFile: (Account, TelegramMediaFile, Bool)? - private var message: Message? - - private var isCentral = false - - private let fetchStatusDisposable = MetaDisposable() - private let fetchDisposable = MetaDisposable() - private var resourceStatus: MediaResourceStatus? - - private let footerContentNode: ChatItemGalleryFooterContentNode - - init(account: Account, theme: PresentationTheme, strings: PresentationStrings) { - self.scrubberView = ChatVideoGalleryItemScrubberView() - - self.progressButtonNode = HighlightableButtonNode() - self.progressNode = RadialProgressNode(theme: RadialProgressTheme(backgroundColor: UIColor(white: 0.0, alpha: 0.6), foregroundColor: UIColor.white, icon: nil)) - - self.footerContentNode = ChatItemGalleryFooterContentNode(account: account, theme: theme, strings: strings) - - super.init() - - self._titleView.set(.single(self.scrubberView)) - self.scrubberView.seek = { [weak self] timestamp in - self?.videoNode?.seek(timestamp) - } - - self.progressButtonNode.addSubnode(self.progressNode) - self.progressButtonNode.addTarget(self, action: #selector(progressButtonPressed), forControlEvents: .touchUpInside) - } - - deinit { - self.fetchStatusDisposable.dispose() - self.fetchDisposable.dispose() - } - - override func ready() -> Signal { - return self._ready.get() - } - - override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { - super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) - - let progressDiameter: CGFloat = 50.0 - let progressFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - progressDiameter) / 2.0), y: floor((layout.size.height - progressDiameter) / 2.0)), size: CGSize(width: progressDiameter, height: progressDiameter)) - transition.updateFrame(node: self.progressButtonNode, frame: progressFrame) - transition.updateFrame(node: self.progressNode, frame: CGRect(origin: CGPoint(), size: progressFrame.size)) - } - - fileprivate func setMessage(_ message: Message) { - self.footerContentNode.setMessage(message) - - self.message = message - - var rightBarButtonItem: UIBarButtonItem? - for media in message.media { - if let file = media as? TelegramMediaFile { - if file.isVideo { - rightBarButtonItem = UIBarButtonItem(image: pictureInPictureButtonImage, style: .plain, target: self, action: #selector(self.pictureInPictureButtonPressed)) - break - } - } - } - self._rightBarButtonItem.set(.single(rightBarButtonItem)) - } - - func setFile(account: Account, stableId: UInt32, file: TelegramMediaFile, loopVideo: Bool) { - if self.accountAndFile == nil || !self.accountAndFile!.1.isEqual(file) || !self.accountAndFile!.2 != loopVideo { - if let videoNode = self.videoNode { - videoNode.pause() - videoNode.removeFromSupernode() - self.videoNode = nil - } - if let largestSize = file.dimensions { - let videoNode = TelegramVideoNode(manager: account.telegramApplicationContext.mediaManager, account: account, source: .messageMedia(stableId: stableId, file: file), priority: 0, withSound: true) - videoNode.setShouldAcquireContext(true) - self.videoNode = videoNode - self.scrubberView.setStatusSignal(videoNode.status) - self.zoomableContent = (largestSize, videoNode) - - self._ready.set(.single(Void())) - } else { - self._ready.set(.single(Void())) - } - - self.resourceStatus = nil - self.fetchStatusDisposable.set((account.postbox.mediaBox.resourceStatus(file.resource) |> deliverOnMainQueue).start(next: { [weak self] status in - if let strongSelf = self { - strongSelf.resourceStatus = status - switch status { - case let .Fetching(_, progress): - strongSelf.progressNode.state = .Fetching(progress: progress) - strongSelf.progressButtonNode.isHidden = false - case .Local: - strongSelf.progressNode.state = .Play - strongSelf.progressButtonNode.isHidden = strongSelf.videoNode != nil - case .Remote: - strongSelf.progressNode.state = .Remote - strongSelf.progressButtonNode.isHidden = false - } - } - })) - if self.progressButtonNode.supernode == nil { - self.addSubnode(self.progressButtonNode) - } - - let shouldPlayVideo = self.accountAndFile?.1 != file - self.accountAndFile = (account, file, loopVideo) - if shouldPlayVideo && self.isCentral { - self.progressButtonPressed() - } - } - } - - private func playVideo() { - if let videoNode = self.videoNode { - videoNode.play() - } else { - if let (account, file, loop) = self.accountAndFile, let message = self.message { - if let largestSize = file.dimensions { - let videoNode = TelegramVideoNode(manager: account.telegramApplicationContext.mediaManager, account: account, source: .messageMedia(stableId: message.stableId, file: file), priority: 0, withSound: true) - videoNode.setShouldAcquireContext(true) - self.scrubberView.setStatusSignal(videoNode.status) - self.videoNode = videoNode - self.zoomableContent = (largestSize, videoNode) - - self._ready.set(.single(Void())) - } else { - self.scrubberView.setStatusSignal(nil) - self._ready.set(.single(Void())) - } - - self.resourceStatus = nil - self.fetchStatusDisposable.set((account.postbox.mediaBox.resourceStatus(file.resource) |> deliverOnMainQueue).start(next: { [weak self] status in - if let strongSelf = self { - strongSelf.resourceStatus = status - switch status { - case let .Fetching(_, progress): - strongSelf.progressNode.state = .Fetching(progress: progress) - strongSelf.progressButtonNode.isHidden = false - case .Local: - strongSelf.progressNode.state = .Play - strongSelf.progressButtonNode.isHidden = strongSelf.videoNode != nil - case .Remote: - strongSelf.progressNode.state = .Remote - strongSelf.progressButtonNode.isHidden = false - } - } - })) - if self.progressButtonNode.supernode == nil { - self.addSubnode(self.progressButtonNode) - } - } - } - } - - private func stopVideo() { - if let videoNode = self.videoNode { - videoNode.pause() - self.progressButtonNode.isHidden = false - - self.videoNode = nil - self.zoomableContent = nil - } - } - - override func centralityUpdated(isCentral: Bool) { - super.centralityUpdated(isCentral: isCentral) - - if self.isCentral != isCentral { - self.isCentral = isCentral - if isCentral { - self.playVideo() - } else { - self.stopVideo() - } - } - } - - override func animateIn(from node: ASDisplayNode, addToTransitionSurface: (UIView) -> Void) { - guard let videoNode = self.videoNode else { - return - } - - if let node = node as? TelegramVideoNode, let account = self.accountAndFile?.0 { - var transformedFrame = node.view.convert(node.view.bounds, to: videoNode.view) - let transformedSuperFrame = node.view.convert(node.view.bounds, to: videoNode.view.superview) - - videoNode.layer.animatePosition(from: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), to: videoNode.layer.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) - - transformedFrame.origin = CGPoint() - - let transform = CATransform3DScale(videoNode.layer.transform, transformedFrame.size.width / videoNode.layer.bounds.size.width, transformedFrame.size.height / videoNode.layer.bounds.size.height, 1.0) - videoNode.layer.animate(from: NSValue(caTransform3D: transform), to: NSValue(caTransform3D: videoNode.layer.transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25) - - account.telegramApplicationContext.mediaManager.setOverlayVideoNode(nil) - } else { - var transformedFrame = node.view.convert(node.view.bounds, to: videoNode.view) - let transformedSuperFrame = node.view.convert(node.view.bounds, to: videoNode.view.superview) - - videoNode.layer.animatePosition(from: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), to: videoNode.layer.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) - - transformedFrame.origin = CGPoint() - - let transform = CATransform3DScale(videoNode.layer.transform, transformedFrame.size.width / videoNode.layer.bounds.size.width, transformedFrame.size.height / videoNode.layer.bounds.size.height, 1.0) - videoNode.layer.animate(from: NSValue(caTransform3D: transform), to: NSValue(caTransform3D: videoNode.layer.transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25) - } - } - - override func animateOut(to node: ASDisplayNode, addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) { - guard let videoNode = self.videoNode else { - completion() - return - } - - var transformedFrame = node.view.convert(node.view.bounds, to: videoNode.view) - let transformedSuperFrame = node.view.convert(node.view.bounds, to: videoNode.view.superview) - let transformedSelfFrame = node.view.convert(node.view.bounds, to: self.view) - let transformedCopyViewInitialFrame = videoNode.view.convert(videoNode.view.bounds, to: self.view) - - var positionCompleted = false - var boundsCompleted = false - var copyCompleted = false - - let copyView = node.view.snapshotContentTree()! - - self.view.insertSubview(copyView, belowSubview: self.scrollNode.view) - copyView.frame = transformedSelfFrame - - let intermediateCompletion = { [weak copyView] in - if positionCompleted && boundsCompleted && copyCompleted { - copyView?.removeFromSuperview() - completion() - } - } - - copyView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, removeOnCompletion: false) - - copyView.layer.animatePosition(from: CGPoint(x: transformedCopyViewInitialFrame.midX, y: transformedCopyViewInitialFrame.midY), to: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) - let scale = CGSize(width: transformedCopyViewInitialFrame.size.width / transformedSelfFrame.size.width, height: transformedCopyViewInitialFrame.size.height / transformedSelfFrame.size.height) - copyView.layer.animate(from: NSValue(caTransform3D: CATransform3DMakeScale(scale.width, scale.height, 1.0)), to: NSValue(caTransform3D: CATransform3DIdentity), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in - copyCompleted = true - intermediateCompletion() - }) - - videoNode.layer.animatePosition(from: videoNode.layer.position, to: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in - positionCompleted = true - intermediateCompletion() - }) - - videoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) - - self.progressNode.layer.animatePosition(from: self.progressNode.layer.position, to: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in - //positionCompleted = true - //intermediateCompletion() - }) - self.progressNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) - self.progressNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.25, removeOnCompletion: false) - - transformedFrame.origin = CGPoint() - - let transform = CATransform3DScale(videoNode.layer.transform, transformedFrame.size.width / videoNode.layer.bounds.size.width, transformedFrame.size.height / videoNode.layer.bounds.size.height, 1.0) - videoNode.layer.animate(from: NSValue(caTransform3D: videoNode.layer.transform), to: NSValue(caTransform3D: transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in - boundsCompleted = true - intermediateCompletion() - }) - } - - func animateOut(toOverlay node: ASDisplayNode, completion: @escaping () -> Void) { - guard let videoNode = self.videoNode else { - completion() - return - } - - var transformedFrame = node.view.convert(node.view.bounds, to: videoNode.view) - let transformedSuperFrame = node.view.convert(node.view.bounds, to: videoNode.view.superview) - let transformedSelfFrame = node.view.convert(node.view.bounds, to: self.view) - let transformedCopyViewInitialFrame = videoNode.view.convert(videoNode.view.bounds, to: self.view) - let transformedSelfTargetSuperFrame = videoNode.view.convert(videoNode.view.bounds, to: node.view.superview) - - var positionCompleted = false - var boundsCompleted = false - var copyCompleted = false - var nodeCompleted = false - - let copyView = node.view.snapshotContentTree()! - - //self.view.insertSubview(copyView, belowSubview: self.scrollView) - videoNode.isHidden = true - copyView.frame = transformedSelfFrame - - let intermediateCompletion = { [weak copyView] in - if positionCompleted && boundsCompleted && copyCompleted && nodeCompleted { - copyView?.removeFromSuperview() - completion() - } - } - - copyView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, removeOnCompletion: false) - - copyView.layer.animatePosition(from: CGPoint(x: transformedCopyViewInitialFrame.midX, y: transformedCopyViewInitialFrame.midY), to: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) - let scale = CGSize(width: transformedCopyViewInitialFrame.size.width / transformedSelfFrame.size.width, height: transformedCopyViewInitialFrame.size.height / transformedSelfFrame.size.height) - copyView.layer.animate(from: NSValue(caTransform3D: CATransform3DMakeScale(scale.width, scale.height, 1.0)), to: NSValue(caTransform3D: CATransform3DIdentity), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in - copyCompleted = true - intermediateCompletion() - }) - - videoNode.layer.animatePosition(from: videoNode.layer.position, to: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in - positionCompleted = true - intermediateCompletion() - }) - - videoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) - - self.progressNode.layer.animatePosition(from: self.progressNode.layer.position, to: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in - //positionCompleted = true - //intermediateCompletion() - }) - self.progressNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) - self.progressNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.25, removeOnCompletion: false) - - transformedFrame.origin = CGPoint() - - let transform = CATransform3DScale(videoNode.layer.transform, transformedFrame.size.width / videoNode.layer.bounds.size.width, transformedFrame.size.height / videoNode.layer.bounds.size.height, 1.0) - videoNode.layer.animate(from: NSValue(caTransform3D: videoNode.layer.transform), to: NSValue(caTransform3D: transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in - boundsCompleted = true - intermediateCompletion() - }) - - //node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) - let nodeTransform = CATransform3DScale(node.layer.transform, videoNode.layer.bounds.size.width / transformedFrame.size.width, videoNode.layer.bounds.size.height / transformedFrame.size.height, 1.0) - node.layer.animatePosition(from: CGPoint(x: transformedSelfTargetSuperFrame.midX, y: transformedSelfTargetSuperFrame.midY), to: node.layer.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) - node.layer.animate(from: NSValue(caTransform3D: nodeTransform), to: NSValue(caTransform3D: node.layer.transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in - nodeCompleted = true - intermediateCompletion() - }) - } - - override func title() -> Signal { - return .single("") - } - - override func titleView() -> Signal { - return self._titleView.get() - } - - override func rightBarButtonItem() -> Signal { - return self._rightBarButtonItem.get() - } - - private func activateVideo() { - if let (account, file, _) = self.accountAndFile { - if let resourceStatus = self.resourceStatus { - switch resourceStatus { - case .Fetching: - break - case .Local: - self.playVideo() - case .Remote: - self.fetchDisposable.set(account.postbox.mediaBox.fetchedResource(file.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .video)).start()) - } - } - } - } - - @objc func progressButtonPressed() { - if let (account, file, _) = self.accountAndFile { - if let resourceStatus = self.resourceStatus { - switch resourceStatus { - case .Fetching: - account.postbox.mediaBox.cancelInteractiveResourceFetch(file.resource) - case .Local: - self.playVideo() - case .Remote: - self.fetchDisposable.set(account.postbox.mediaBox.fetchedResource(file.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .video)).start()) - } - } - } - } - - @objc func pictureInPictureButtonPressed() { - if let account = self.accountAndFile?.0, let message = self.message, let file = self.accountAndFile?.1 { - let overlayNode = TelegramVideoNode(manager: account.telegramApplicationContext.mediaManager, account: account, source: TelegramVideoNodeSource.messageMedia(stableId: message.stableId, file: file), priority: 1, withSound: true, withOverlayControls: true) - overlayNode.dismissed = { [weak account, weak overlayNode] in - if let account = account, let overlayNode = overlayNode { - if overlayNode.supernode != nil { - account.telegramApplicationContext.mediaManager.setOverlayVideoNode(nil) - } - } - } - let baseNavigationController = self.baseNavigationController() - overlayNode.unembed = { [weak account, weak overlayNode, weak baseNavigationController] in - if let account = account { - let gallery = GalleryController(account: account, messageId: message.id, replaceRootController: { controller, ready in - if let baseNavigationController = baseNavigationController { - baseNavigationController.replaceTopController(controller, animated: false, ready: ready) - } - }, baseNavigationController: baseNavigationController) - - (baseNavigationController?.topViewController as? ViewController)?.present(gallery, in: .window(.root), with: GalleryControllerPresentationArguments(transitionArguments: { _, _ in - if let overlayNode = overlayNode, let overlaySupernode = overlayNode.supernode { - return GalleryTransitionArguments(transitionNode: overlayNode, addToTransitionSurface: { _ in - }) - } - return nil - })) - } - } - overlayNode.setShouldAcquireContext(true) - account.telegramApplicationContext.mediaManager.setOverlayVideoNode(overlayNode) - if overlayNode.supernode != nil { - self.beginCustomDismiss() - self.animateOut(toOverlay: overlayNode, completion: { [weak self] in - self?.completeCustomDismiss() - }) - } - } - } - - override func footerContent() -> Signal { - return .single(self.footerContentNode) - } -} diff --git a/TelegramUI/ChatVideoGalleryItemScrubberView.swift b/TelegramUI/ChatVideoGalleryItemScrubberView.swift index 420d938f7a..6904338a85 100644 --- a/TelegramUI/ChatVideoGalleryItemScrubberView.swift +++ b/TelegramUI/ChatVideoGalleryItemScrubberView.swift @@ -32,7 +32,7 @@ final class ChatVideoGalleryItemScrubberView: UIView { var seek: (Double) -> Void = { _ in } override init(frame: CGRect) { - self.scrubberNode = MediaPlayerScrubbingNode(lineHeight: 3.0, lineCap: .round, scrubberHandle: true, backgroundColor: UIColor(white: 1.0, alpha: 0.42), foregroundColor: .white) + self.scrubberNode = MediaPlayerScrubbingNode(content: .standard(lineHeight: 3.0, lineCap: .round, scrubberHandle: .line, backgroundColor: UIColor(white: 1.0, alpha: 0.42), foregroundColor: .white)) self.leftTimestampNode = MediaPlayerTimeTextNode(textColor: .white) self.rightTimestampNode = MediaPlayerTimeTextNode(textColor: .white) diff --git a/TelegramUI/CheckNode.swift b/TelegramUI/CheckNode.swift new file mode 100644 index 0000000000..5134c31fb3 --- /dev/null +++ b/TelegramUI/CheckNode.swift @@ -0,0 +1,55 @@ +import Foundation +import AsyncDisplayKit + +import LegacyComponents + +enum CheckNodeStyle { + case plain + case overlay +} + +final class CheckNode: ASDisplayNode { + private let strokeColor: UIColor + private let fillColor: UIColor + private let foregroundColor: UIColor + private let checkStyle: CheckNodeStyle + + private var checkView: TGCheckButtonView? + + private(set) var isChecked: Bool = false + + init(strokeColor: UIColor, fillColor: UIColor, foregroundColor: UIColor, style: CheckNodeStyle) { + self.strokeColor = strokeColor + self.fillColor = fillColor + self.foregroundColor = foregroundColor + self.checkStyle = style + + super.init() + } + + override func didLoad() { + super.didLoad() + + let style: TGCheckButtonStyle + switch self.checkStyle { + case .plain: + style = TGCheckButtonStyleDefault + case .overlay: + style = TGCheckButtonStyleMedia + } + let checkView = TGCheckButtonView(style: style, pallete: TGCheckButtonPallete(defaultBackgroundColor: self.fillColor, accentBackgroundColor: self.fillColor, defaultBorderColor: self.strokeColor, mediaBorderColor: self.strokeColor, chatBorderColor: self.strokeColor, check: self.foregroundColor, blueColor: self.fillColor, barBackgroundColor: self.fillColor))! + checkView.setSelected(true, animated: false) + checkView.layoutSubviews() + checkView.setSelected(self.isChecked, animated: false) + self.checkView = checkView + self.view.addSubview(checkView) + checkView.frame = self.bounds + } + + func setIsChecked(_ isChecked: Bool, animated: Bool) { + if isChecked != self.isChecked { + self.isChecked = isChecked + self.checkView?.setSelected(isChecked, animated: animated) + } + } +} diff --git a/TelegramUI/CommandChatInputContextPanelNode.swift b/TelegramUI/CommandChatInputContextPanelNode.swift index b4ca1ae387..b8f874e325 100644 --- a/TelegramUI/CommandChatInputContextPanelNode.swift +++ b/TelegramUI/CommandChatInputContextPanelNode.swift @@ -19,13 +19,14 @@ private struct CommandChatInputContextPanelEntryStableId: Hashable { private struct CommandChatInputContextPanelEntry: Comparable, Identifiable { let index: Int let command: PeerCommand + let theme: PresentationTheme var stableId: CommandChatInputContextPanelEntryStableId { return CommandChatInputContextPanelEntryStableId(command: self.command) } static func ==(lhs: CommandChatInputContextPanelEntry, rhs: CommandChatInputContextPanelEntry) -> Bool { - return lhs.index == rhs.index && lhs.command == rhs.command + return lhs.index == rhs.index && lhs.command == rhs.command && lhs.theme === rhs.theme } static func <(lhs: CommandChatInputContextPanelEntry, rhs: CommandChatInputContextPanelEntry) -> Bool { @@ -33,7 +34,7 @@ private struct CommandChatInputContextPanelEntry: Comparable, Identifiable { } func item(account: Account, commandSelected: @escaping (PeerCommand, Bool) -> Void) -> ListViewItem { - return CommandChatInputPanelItem(account: account, command: self.command, commandSelected: commandSelected) + return CommandChatInputPanelItem(account: account, theme: self.theme, command: self.command, commandSelected: commandSelected) } } @@ -54,20 +55,24 @@ private func preparedTransition(from fromEntries: [CommandChatInputContextPanelE } final class CommandChatInputContextPanelNode: ChatInputContextPanelNode { + private var theme: PresentationTheme + private let listView: ListView private var currentEntries: [CommandChatInputContextPanelEntry]? private var enqueuedTransitions: [(CommandChatInputContextPanelTransition, Bool)] = [] - private var hasValidLayout = false + private var validLayout: (CGSize, CGFloat, CGFloat)? - override init(account: Account) { + override init(account: Account, theme: PresentationTheme, strings: PresentationStrings) { + self.theme = theme + self.listView = ListView() self.listView.isOpaque = false self.listView.stackFromBottom = true - self.listView.keepBottomItemOverscrollBackground = .white + self.listView.keepBottomItemOverscrollBackground = theme.list.plainBackgroundColor self.listView.limitHitTestToNodes = true - super.init(account: account) + super.init(account: account, theme: theme, strings: strings) self.isOpaque = false self.clipsToBounds = true @@ -80,7 +85,7 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode { var index = 0 var stableIds = Set() for command in results { - let entry = CommandChatInputContextPanelEntry(index: index, command: command) + let entry = CommandChatInputContextPanelEntry(index: index, command: command, theme: self.theme) if stableIds.contains(entry.stableId) { continue } @@ -96,7 +101,15 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode { interfaceInteraction.sendBotCommand(command.peer, "/" + command.command.text) } else { interfaceInteraction.updateTextInputState { textInputState in - if let (range, type, _) = textInputStateContextQueryRangeAndType(textInputState) { + var commandQueryRange: Range? + inner: for (range, type, _) in textInputStateContextQueryRangeAndType(textInputState) { + if type == [.command] { + commandQueryRange = range + break inner + } + } + + if let range = commandQueryRange { var inputText = textInputState.inputText let replacementText = command.command.text + " " @@ -125,7 +138,7 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode { private func enqueueTransition(_ transition: CommandChatInputContextPanelTransition, firstTime: Bool) { enqueuedTransitions.append((transition, firstTime)) - if self.hasValidLayout { + if self.validLayout != nil { while !self.enqueuedTransitions.isEmpty { self.dequeueTransition() } @@ -133,7 +146,7 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode { } private func dequeueTransition() { - if let (transition, firstTime) = self.enqueuedTransitions.first { + if let validLayout = self.validLayout, let (transition, firstTime) = self.enqueuedTransitions.first { self.enqueuedTransitions.remove(at: 0) var options = ListViewDeleteAndInsertOptions() @@ -146,7 +159,9 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode { } var insets = UIEdgeInsets() - insets.top = topInsetForLayout(size: self.listView.bounds.size) + insets.top = topInsetForLayout(size: validLayout.0) + insets.left = validLayout.1 + insets.right = validLayout.2 let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: self.listView.bounds.size, insets: insets, duration: 0.0, curve: .Default) @@ -174,9 +189,14 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode { return max(size.height - minimumItemHeights, 0.0) } - override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) { + override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) { + let hadValidLayout = self.validLayout != nil + self.validLayout = (size, leftInset, rightInset) + var insets = UIEdgeInsets() insets.top = self.topInsetForLayout(size: size) + insets.left = leftInset + insets.right = rightInset transition.updateFrame(node: self.listView, frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)) @@ -206,8 +226,7 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode { self.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) - if !hasValidLayout { - hasValidLayout = true + if !hadValidLayout { while !self.enqueuedTransitions.isEmpty { self.dequeueTransition() } diff --git a/TelegramUI/CommandChatInputPanelItem.swift b/TelegramUI/CommandChatInputPanelItem.swift index 9559a1447f..d7a2b89121 100644 --- a/TelegramUI/CommandChatInputPanelItem.swift +++ b/TelegramUI/CommandChatInputPanelItem.swift @@ -7,24 +7,26 @@ import Postbox final class CommandChatInputPanelItem: ListViewItem { fileprivate let account: Account + fileprivate let theme: PresentationTheme fileprivate let command: PeerCommand fileprivate let commandSelected: (PeerCommand, Bool) -> Void let selectable: Bool = true - public init(account: Account, command: PeerCommand, commandSelected: @escaping (PeerCommand, Bool) -> Void) { + public init(account: Account, theme: PresentationTheme, command: PeerCommand, commandSelected: @escaping (PeerCommand, Bool) -> Void) { self.account = account + self.theme = theme self.command = command self.commandSelected = commandSelected } - public func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { let configure = { () -> Void in let node = CommandChatInputPanelItemNode() let nodeLayout = node.asyncLayout() let (top, bottom) = (previousItem != nil, nextItem != nil) - let (layout, apply) = nodeLayout(self, width, top, bottom) + let (layout, apply) = nodeLayout(self, params, top, bottom) node.contentSize = layout.contentSize node.insets = layout.insets @@ -42,7 +44,7 @@ final class CommandChatInputPanelItem: ListViewItem { } } - public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { if let node = node as? CommandChatInputPanelItemNode { Queue.mainQueue().async { let nodeLayout = node.asyncLayout() @@ -50,7 +52,7 @@ final class CommandChatInputPanelItem: ListViewItem { async { let (top, bottom) = (previousItem != nil, nextItem != nil) - let (layout, apply) = nodeLayout(self, width, top, bottom) + let (layout, apply) = nodeLayout(self, params, top, bottom) Queue.mainQueue().async { completion(layout, { apply(animation) @@ -71,30 +73,6 @@ final class CommandChatInputPanelItem: ListViewItem { private let avatarFont: UIFont = UIFont(name: "ArialRoundedMTBold", size: 16.0)! private let textFont = Font.medium(14.0) private let descriptionFont = Font.regular(14.0) -private let descriptionColor = UIColor(rgb: 0x9099A2) - -private let arrowImage = generateImage(CGSize(width: 11.0, height: 11.0), contextGenerator: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.translateBy(x: size.width / 2.0, y: size.height / 2.0) - context.scaleBy(x: 1.0, y: -1.0) - context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) - - context.setStrokeColor(UIColor(rgb: 0xC7CCD0).cgColor) - context.setLineCap(.round) - context.setLineWidth(2.0) - context.setLineJoin(.round) - - context.beginPath() - context.move(to: CGPoint(x: 1.0, y: 2.0)) - context.addLine(to: CGPoint(x: 1.0, y: 10.0)) - context.addLine(to: CGPoint(x: 9.0, y: 10.0)) - context.strokePath() - - context.beginPath() - context.move(to: CGPoint(x: 1.0, y: 10.0)) - context.addLine(to: CGPoint(x: 10.0, y: 1.0)) - context.strokePath() -}) final class CommandChatInputPanelItemNode: ListViewItemNode { static let itemHeight: CGFloat = 42.0 @@ -112,24 +90,18 @@ final class CommandChatInputPanelItemNode: ListViewItemNode { self.textNode = TextNode() self.topSeparatorNode = ASDisplayNode() - self.topSeparatorNode.backgroundColor = UIColor(rgb: 0xC9CDD1) self.topSeparatorNode.isLayerBacked = true self.separatorNode = ASDisplayNode() - self.separatorNode.backgroundColor = UIColor(rgb: 0xD6D6DA) self.separatorNode.isLayerBacked = true self.highlightedBackgroundNode = ASDisplayNode() - self.highlightedBackgroundNode.backgroundColor = UIColor(rgb: 0xd9d9d9) self.highlightedBackgroundNode.isLayerBacked = true self.arrowNode = HighlightableButtonNode() - self.arrowNode.setImage(arrowImage, for: []) super.init(layerBacked: false, dynamicBounce: false) - self.backgroundColor = .white - self.addSubnode(self.topSeparatorNode) self.addSubnode(self.separatorNode) @@ -140,62 +112,72 @@ final class CommandChatInputPanelItemNode: ListViewItemNode { self.arrowNode.addTarget(self, action: #selector(self.arrowButtonPressed), forControlEvents: [.touchUpInside]) } - override public func layoutForWidth(_ width: CGFloat, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { + override public func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { if let item = item as? CommandChatInputPanelItem { let doLayout = self.asyncLayout() let merged = (top: previousItem != nil, bottom: nextItem != nil) - let (layout, apply) = doLayout(item, width, merged.top, merged.bottom) + let (layout, apply) = doLayout(item, params, merged.top, merged.bottom) self.contentSize = layout.contentSize self.insets = layout.insets apply(.None) } } - func asyncLayout() -> (_ item: CommandChatInputPanelItem, _ width: CGFloat, _ mergedTop: Bool, _ mergedBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { + func asyncLayout() -> (_ item: CommandChatInputPanelItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { let makeTextLayout = TextNode.asyncLayout(self.textNode) - return { [weak self] item, width, mergedTop, mergedBottom in - let leftInset: CGFloat = 55.0 - let rightInset: CGFloat = 10.0 + + return { [weak self] item, params, mergedTop, mergedBottom in + let leftInset: CGFloat = 55.0 + params.leftInset + let rightInset: CGFloat = 10.0 + params.rightInset let commandString = NSMutableAttributedString() - commandString.append(NSAttributedString(string: "/" + item.command.command.text, font: textFont, textColor: .black)) + commandString.append(NSAttributedString(string: "/" + item.command.command.text, font: textFont, textColor: item.theme.list.itemPrimaryTextColor)) if !item.command.command.description.isEmpty { - commandString.append(NSAttributedString(string: " " + item.command.command.description, font: descriptionFont, textColor: descriptionColor)) + commandString.append(NSAttributedString(string: " " + item.command.command.description, font: descriptionFont, textColor: item.theme.list.itemSecondaryTextColor)) } - let (textLayout, textApply) = makeTextLayout(commandString, nil, 1, .end, CGSize(width: width - leftInset - rightInset, height: 100.0), .natural, nil, UIEdgeInsets()) + let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: commandString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 40.0, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: width, height: HashtagChatInputPanelItemNode.itemHeight), insets: UIEdgeInsets()) + let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: HashtagChatInputPanelItemNode.itemHeight), insets: UIEdgeInsets()) + + let iconImage = PresentationResourcesChat.chatCommandPanelArrowImage(item.theme) return (nodeLayout, { _ in if let strongSelf = self { strongSelf.item = item + strongSelf.separatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor + strongSelf.topSeparatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor + strongSelf.backgroundColor = item.theme.list.plainBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor + + strongSelf.arrowNode.setImage(iconImage, for: []) + strongSelf.avatarNode.setPeer(account: item.account, peer: item.command.peer) - textApply() + let _ = textApply() - strongSelf.avatarNode.frame = CGRect(origin: CGPoint(x: 12.0, y: floor((nodeLayout.contentSize.height - 30.0) / 2.0)), size: CGSize(width: 30.0, height: 30.0)) + strongSelf.avatarNode.frame = CGRect(origin: CGPoint(x: params.leftInset + 12.0, y: floor((nodeLayout.contentSize.height - 30.0) / 2.0)), size: CGSize(width: 30.0, height: 30.0)) strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floor((nodeLayout.contentSize.height - textLayout.size.height) / 2.0)), size: textLayout.size) let arrowSize = CGSize(width: 42.0, height: nodeLayout.contentSize.height) - strongSelf.arrowNode.frame = CGRect(origin: CGPoint(x: nodeLayout.size.width - arrowSize.width, y: 0.0), size: arrowSize) + strongSelf.arrowNode.frame = CGRect(origin: CGPoint(x: nodeLayout.size.width - arrowSize.width - params.rightInset, y: 0.0), size: arrowSize) strongSelf.topSeparatorNode.isHidden = mergedTop strongSelf.separatorNode.isHidden = !mergedBottom - strongSelf.topSeparatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: UIScreenPixel)) - strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: nodeLayout.contentSize.height - UIScreenPixel), size: CGSize(width: width - leftInset, height: UIScreenPixel)) + strongSelf.topSeparatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.width, height: UIScreenPixel)) + strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: nodeLayout.contentSize.height - UIScreenPixel), size: CGSize(width: params.width - leftInset, height: UIScreenPixel)) - strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: nodeLayout.size.height + UIScreenPixel)) + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.width, height: nodeLayout.size.height + UIScreenPixel)) } }) } } - override func setHighlighted(_ highlighted: Bool, animated: Bool) { - super.setHighlighted(highlighted, animated: animated) + override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { + super.setHighlighted(highlighted, at: point, animated: animated) if highlighted { self.highlightedBackgroundNode.alpha = 1.0 diff --git a/TelegramUI/CompomentsThemes.swift b/TelegramUI/CompomentsThemes.swift index e3f4d38ac5..21a6178215 100644 --- a/TelegramUI/CompomentsThemes.swift +++ b/TelegramUI/CompomentsThemes.swift @@ -1,17 +1,17 @@ import Foundation import Display -extension TabBarControllerTheme { - convenience init(rootControllerTheme: PresentationTheme) { +public extension TabBarControllerTheme { + public convenience init(rootControllerTheme: PresentationTheme) { let theme = rootControllerTheme.rootController.tabBar - self.init(backgroundColor: rootControllerTheme.list.plainBackgroundColor, tabBarBackgroundColor: theme.backgroundColor, tabBarSeparatorColor: theme.separatorColor, tabBarTextColor: theme.textColor, tabBarSelectedTextColor: theme.selectedIconColor, tabBarBadgeBackgroundColor: theme.badgeBackgroundColor, tabBarBadgeTextColor: theme.badgeTextColor) + self.init(backgroundColor: rootControllerTheme.list.plainBackgroundColor, tabBarBackgroundColor: theme.backgroundColor, tabBarSeparatorColor: theme.separatorColor, tabBarTextColor: theme.textColor, tabBarSelectedTextColor: theme.selectedIconColor, tabBarBadgeBackgroundColor: theme.badgeBackgroundColor, tabBarBadgeStrokeColor: theme.badgeStrokeColor, tabBarBadgeTextColor: theme.badgeTextColor) } } -extension NavigationBarTheme { - convenience init(rootControllerTheme: PresentationTheme) { +public extension NavigationBarTheme { + public convenience init(rootControllerTheme: PresentationTheme) { let theme = rootControllerTheme.rootController.navigationBar - self.init(buttonColor: theme.buttonColor, primaryTextColor: theme.primaryTextColor, backgroundColor: theme.backgroundColor, separatorColor: theme.separatorColor) + self.init(buttonColor: theme.buttonColor, primaryTextColor: theme.primaryTextColor, backgroundColor: theme.backgroundColor, separatorColor: theme.separatorColor, badgeBackgroundColor: theme.badgeBackgroundColor, badgeStrokeColor: theme.badgeStrokeColor, badgeTextColor: theme.badgeTextColor) } } diff --git a/TelegramUI/ComposeController.swift b/TelegramUI/ComposeController.swift index 06436fc0c4..f8afc6ef4c 100644 --- a/TelegramUI/ComposeController.swift +++ b/TelegramUI/ComposeController.swift @@ -122,12 +122,14 @@ public class ComposeController: ViewController { strongSelf.createActionDisposable.set((createSecretChat(account: strongSelf.account, peerId: peerId) |> deliverOnMainQueue).start(next: { peerId in if let strongSelf = self, let controller = controller { controller.displayNavigationActivity = false - (controller.navigationController as? NavigationController)?.replaceAllButRootController(ChatController(account: strongSelf.account, peerId: peerId), animated: true) + (controller.navigationController as? NavigationController)?.replaceAllButRootController(ChatController(account: strongSelf.account, chatLocation: .peer(peerId)), animated: true) } }, error: { _ in - if let controller = controller { + if let strongSelf = self, let controller = controller { + let presentationData = strongSelf.account.telegramApplicationContext.currentPresentationData.with { $0 } + controller.displayNavigationActivity = false - controller.present(standardTextAlertController(title: nil, text: "An error occurred.", actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})]), in: .window(.root)) + controller.present(standardTextAlertController(title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) } })) } @@ -181,6 +183,6 @@ public class ComposeController: ViewController { } private func openPeer(peerId: PeerId) { - (self.navigationController as? NavigationController)?.replaceTopController(ChatController(account: self.account, peerId: peerId), animated: true) + (self.navigationController as? NavigationController)?.replaceTopController(ChatController(account: self.account, chatLocation: .peer(peerId)), animated: true) } } diff --git a/TelegramUI/ComposeControllerNode.swift b/TelegramUI/ComposeControllerNode.swift index 787d887199..6baccca16d 100644 --- a/TelegramUI/ComposeControllerNode.swift +++ b/TelegramUI/ComposeControllerNode.swift @@ -95,7 +95,7 @@ final class ComposeControllerNode: ASDisplayNode { var insets = layout.insets(options: [.input]) insets.top += navigationBarHeight - self.contactListNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: insets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight), transition: transition) + self.contactListNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: insets, safeInsets: layout.safeInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging), transition: transition) self.contactListNode.frame = CGRect(origin: CGPoint(), size: layout.size) @@ -121,7 +121,7 @@ final class ComposeControllerNode: ASDisplayNode { } if let placeholderNode = maybePlaceholderNode { - self.searchDisplayController = SearchDisplayController(theme: self.presentationData.theme, strings: self.presentationData.strings, contentNode: ContactsSearchContainerNode(account: self.account, openPeer: { [weak self] peerId in + self.searchDisplayController = SearchDisplayController(theme: self.presentationData.theme, strings: self.presentationData.strings, contentNode: ContactsSearchContainerNode(account: self.account, onlyWriteable: false, openPeer: { [weak self] peerId in if let requestOpenPeerFromSearch = self?.requestOpenPeerFromSearch { requestOpenPeerFromSearch(peerId) } diff --git a/TelegramUI/ContactListActionItem.swift b/TelegramUI/ContactListActionItem.swift index c17b2f2880..5be7ef7965 100644 --- a/TelegramUI/ContactListActionItem.swift +++ b/TelegramUI/ContactListActionItem.swift @@ -16,10 +16,10 @@ class ContactListActionItem: ListViewItem { self.action = action } - func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { async { let node = ContactListActionItemNode() - let (layout, apply) = node.asyncLayout()(self, width) + let (layout, apply) = node.asyncLayout()(self, params) node.contentSize = layout.contentSize node.insets = layout.insets @@ -30,13 +30,13 @@ class ContactListActionItem: ListViewItem { } } - func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { if let node = node as? ContactListActionItemNode { Queue.mainQueue().async { let makeLayout = node.asyncLayout() async { - let (layout, apply) = makeLayout(self, width) + let (layout, apply) = makeLayout(self, params) Queue.mainQueue().async { completion(layout, { apply() @@ -98,22 +98,22 @@ class ContactListActionItemNode: ListViewItemNode { self.addSubnode(self.titleNode) } - func asyncLayout() -> (_ item: ContactListActionItem, _ width: CGFloat) -> (ListViewItemNodeLayout, () -> Void) { + func asyncLayout() -> (_ item: ContactListActionItem, _ params: ListViewItemLayoutParams) -> (ListViewItemNodeLayout, () -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let currentTheme = self.theme - return { item, width in + return { item, params in var updatedTheme: PresentationTheme? if currentTheme !== item.theme { updatedTheme = item.theme } - let leftInset: CGFloat = 65.0 + let leftInset: CGFloat = 65.0 + params.leftInset - let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: item.title, font: titleFont, textColor: item.theme.list.itemAccentColor), nil, 1, .end, CGSize(width: width - 10.0 - leftInset, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.theme.list.itemAccentColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - 10.0 - leftInset - params.rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let contentSize = CGSize(width: width, height: 48.0) + let contentSize = CGSize(width: params.width, height: 48.0) let insets = UIEdgeInsets() let separatorHeight = UIScreenPixel @@ -124,9 +124,9 @@ class ContactListActionItemNode: ListViewItemNode { strongSelf.theme = item.theme if let _ = updatedTheme { - strongSelf.topStripeNode.backgroundColor = item.theme.list.itemSeparatorColor - strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemSeparatorColor - strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBackgroundColor + strongSelf.topStripeNode.backgroundColor = item.theme.list.itemPlainSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemPlainSeparatorColor + strongSelf.backgroundNode.backgroundColor = item.theme.list.plainBackgroundColor strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor } @@ -135,7 +135,7 @@ class ContactListActionItemNode: ListViewItemNode { strongSelf.iconNode.image = item.icon if let image = item.icon { - strongSelf.iconNode.frame = CGRect(origin: CGPoint(x: floor((leftInset - image.size.width) / 2.0), y: floor((contentSize.height - image.size.height) / 2.0)), size: image.size) + strongSelf.iconNode.frame = CGRect(origin: CGPoint(x: params.leftInset + floor((leftInset - params.leftInset - image.size.width) / 2.0), y: floor((contentSize.height - image.size.height) / 2.0)), size: image.size) } if strongSelf.backgroundNode.supernode == nil { @@ -150,18 +150,18 @@ class ContactListActionItemNode: ListViewItemNode { strongSelf.topStripeNode.isHidden = true - strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height), size: CGSize(width: width - leftInset, height: separatorHeight)) + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height), size: CGSize(width: params.width - leftInset, height: separatorHeight)) strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floor((contentSize.height - titleLayout.size.height) / 2.0)), size: titleLayout.size) - strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: width, height: 48.0 + UIScreenPixel + UIScreenPixel)) + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: 48.0 + UIScreenPixel + UIScreenPixel)) } }) } } - override func setHighlighted(_ highlighted: Bool, animated: Bool) { - super.setHighlighted(highlighted, animated: animated) + override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { + super.setHighlighted(highlighted, at: point, animated: animated) if highlighted { self.highlightedBackgroundNode.alpha = 1.0 diff --git a/TelegramUI/ContactListNameIndexHeader.swift b/TelegramUI/ContactListNameIndexHeader.swift index 8b1c2b4b0f..13f20c0fd7 100644 --- a/TelegramUI/ContactListNameIndexHeader.swift +++ b/TelegramUI/ContactListNameIndexHeader.swift @@ -44,7 +44,8 @@ final class ContactListNameIndexHeaderNode: ListViewItemHeaderNode { self.addSubnode(self.sectionHeaderNode) } - override func layout() { - self.sectionHeaderNode.frame = CGRect(origin: CGPoint(), size: self.bounds.size) + override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) { + self.sectionHeaderNode.frame = CGRect(origin: CGPoint(), size: size) + self.sectionHeaderNode.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset) } } diff --git a/TelegramUI/ContactListNode.swift b/TelegramUI/ContactListNode.swift index bda8570833..99774d09de 100644 --- a/TelegramUI/ContactListNode.swift +++ b/TelegramUI/ContactListNode.swift @@ -7,7 +7,6 @@ import TelegramCore private enum ContactListNodeEntryId: Hashable { case search - case vcard case option(index: Int) case peerId(Int64) @@ -15,8 +14,6 @@ private enum ContactListNodeEntryId: Hashable { switch self { case .search: return 0 - case .vcard: - return 1 case let .option(index): return (index + 2).hashValue case let .peerId(peerId): @@ -37,13 +34,6 @@ private enum ContactListNodeEntryId: Hashable { default: return false } - case .vcard: - switch rhs { - case .vcard: - return true - default: - return false - } case let .option(index): if case .option(index) = rhs { return true @@ -73,7 +63,6 @@ private final class ContactListNodeInteraction { private enum ContactListNodeEntry: Comparable, Identifiable { case search(PresentationTheme, PresentationStrings) - case vcard(Peer, PresentationTheme, PresentationStrings) case option(Int, ContactListAdditionalOption, PresentationTheme, PresentationStrings) case peer(Int, Peer, PeerPresence?, ContactListNameIndexHeader?, ContactsPeerItemSelection, PresentationTheme, PresentationStrings) @@ -81,8 +70,6 @@ private enum ContactListNodeEntry: Comparable, Identifiable { switch self { case .search: return .search - case .vcard: - return .vcard case let .option(index, _, _, _): return .option(index: index) case let .peer(_, peer, _, _, _, _, _): @@ -96,10 +83,6 @@ private enum ContactListNodeEntry: Comparable, Identifiable { return ChatListSearchItem(theme: theme, placeholder: strings.Common_Search, activate: { interaction.activateSearch() }) - case let .vcard(peer, theme, strings): - return ContactsVCardItem(theme: theme, strings: strings, account: account, peer: peer, action: { peer in - interaction.openPeer(peer) - }) case let .option(_, option, theme, strings): return ContactListActionItem(theme: theme, title: option.title, icon: option.icon, action: option.action) case let .peer(_, peer, presence, header, selection, theme, strings): @@ -109,7 +92,7 @@ private enum ContactListNodeEntry: Comparable, Identifiable { } else { status = .none } - return ContactsPeerItem(theme: theme, strings: strings, account: account, peer: peer, chatPeer: peer, status: status, selection: selection, hasActiveRevealControls: false, index: nil, header: header, action: { _ in + return ContactsPeerItem(theme: theme, strings: strings, account: account, peer: peer, chatPeer: peer, status: status, enabled: true, selection: selection, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, action: { _ in interaction.openPeer(peer) }) } @@ -123,12 +106,6 @@ private enum ContactListNodeEntry: Comparable, Identifiable { } else { return false } - case let .vcard(lhsPeer, lhsTheme, lhsStrings): - if case let .vcard(rhsPeer, rhsTheme, rhsStrings) = rhs, arePeersEqual(lhsPeer, rhsPeer), lhsTheme === rhsTheme, lhsStrings === rhsStrings { - return true - } else { - return false - } case let .option(lhsIndex, lhsOption, lhsTheme, lhsStrings): if case let .option(rhsIndex, rhsOption, rhsTheme, rhsStrings) = rhs, lhsIndex == rhsIndex, lhsOption == rhsOption, lhsTheme === rhsTheme, lhsStrings === rhsStrings { return true @@ -174,16 +151,9 @@ private enum ContactListNodeEntry: Comparable, Identifiable { switch lhs { case .search: return true - case .vcard: - switch rhs { - case .search, .vcard: - return false - case .peer, .option: - return true - } case let .option(lhsIndex, _, _, _): switch rhs { - case .search, .vcard: + case .search: return false case let .option(rhsIndex, _, _, _): return lhsIndex < rhsIndex @@ -192,7 +162,7 @@ private enum ContactListNodeEntry: Comparable, Identifiable { } case let .peer(lhsIndex, _, _, _, _, _, _): switch rhs { - case .search, .vcard, .option: + case .search, .option: return false case let .peer(rhsIndex, _, _, _, _, _, _): return lhsIndex < rhsIndex @@ -244,13 +214,8 @@ private func contactListNodeEntries(accountPeer: Peer?, peers: [Peer], presences var headers: [PeerId: ContactListNameIndexHeader] = [:] switch presentation { - case let .orderedByPresence(displayVCard): + case .orderedByPresence: entries.append(.search(theme, strings)) - if displayVCard { - if let peer = accountPeer { - entries.append(.vcard(peer, theme, strings)) - } - } orderedPeers = peers.sorted(by: { lhs, rhs in let lhsPresence = presences[lhs.id] let rhsPresence = presences[rhs.id] @@ -318,7 +283,7 @@ private func contactListNodeEntries(accountPeer: Peer?, peers: [Peer], presences removeIndices.append(i) } case let .personName(first, last, _, _): - if first.isEmpty || last.isEmpty { + if first.isEmpty && last.isEmpty { removeIndices.append(i) } } @@ -359,18 +324,18 @@ private struct ContactsListNodeTransition { let animated: Bool } -struct ContactListAdditionalOption: Equatable { - let title: String - let icon: UIImage? - let action: () -> Void +public struct ContactListAdditionalOption: Equatable { + public let title: String + public let icon: UIImage? + public let action: () -> Void - static func ==(lhs: ContactListAdditionalOption, rhs: ContactListAdditionalOption) -> Bool { + public static func ==(lhs: ContactListAdditionalOption, rhs: ContactListAdditionalOption) -> Bool { return lhs.title == rhs.title && lhs.icon === rhs.icon } } enum ContactListPresentation { - case orderedByPresence(displayVCard: Bool) + case orderedByPresence case natural(displaySearch: Bool, options: [ContactListAdditionalOption]) case search(Signal) } @@ -562,7 +527,9 @@ final class ContactListNode: ASDisplayNode { } func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { - let insets = layout.insets(options: [.input]) + var insets = layout.insets(options: [.input]) + insets.left += layout.safeInsets.left + insets.right += layout.safeInsets.right self.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) self.listNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0) diff --git a/TelegramUI/ContactMultiselectionControllerNode.swift b/TelegramUI/ContactMultiselectionControllerNode.swift index 885e7626fd..508a0e082d 100644 --- a/TelegramUI/ContactMultiselectionControllerNode.swift +++ b/TelegramUI/ContactMultiselectionControllerNode.swift @@ -100,7 +100,7 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { var insets = layout.insets(options: [.input]) insets.top += navigationBarHeight insets.top += strongSelf.tokenListNode.bounds.size.height - searchResultsNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: insets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight), transition: .immediate) + searchResultsNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: insets, safeInsets: layout.safeInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging), transition: .immediate) searchResultsNode.frame = CGRect(origin: CGPoint(), size: layout.size) } @@ -143,17 +143,17 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { var insets = layout.insets(options: [.input]) insets.top += navigationBarHeight - let tokenListHeight = self.tokenListNode.updateLayout(tokens: self.editableTokens, width: layout.size.width, transition: transition) + let tokenListHeight = self.tokenListNode.updateLayout(tokens: self.editableTokens, width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: transition) transition.updateFrame(node: self.tokenListNode, frame: CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: tokenListHeight))) insets.top += tokenListHeight - self.contactListNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: insets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight), transition: transition) + self.contactListNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: insets, safeInsets: layout.safeInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging), transition: transition) self.contactListNode.frame = CGRect(origin: CGPoint(), size: layout.size) if let searchResultsNode = self.searchResultsNode { - searchResultsNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: insets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight), transition: transition) + searchResultsNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: insets, safeInsets: layout.safeInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging), transition: transition) searchResultsNode.frame = CGRect(origin: CGPoint(), size: layout.size) } } diff --git a/TelegramUI/ContactSelectionController.swift b/TelegramUI/ContactSelectionController.swift index e44a856586..2924e42cb6 100644 --- a/TelegramUI/ContactSelectionController.swift +++ b/TelegramUI/ContactSelectionController.swift @@ -14,6 +14,7 @@ public class ContactSelectionController: ViewController { private let index: PeerNameIndex = .lastNameFirst private let titleProducer: (PresentationStrings) -> String + private let options: [ContactListAdditionalOption] private var _ready = Promise() override public var ready: Promise { @@ -37,7 +38,7 @@ public class ContactSelectionController: ViewController { didSet { if self.displayNavigationActivity != oldValue { if self.displayNavigationActivity { - self.navigationItem.setRightBarButton(UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode()), animated: false) + self.navigationItem.setRightBarButton(UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: self.presentationData.theme.rootController.navigationBar.accentTextColor)), animated: false) } else { self.navigationItem.setRightBarButton(nil, animated: false) } @@ -45,9 +46,10 @@ public class ContactSelectionController: ViewController { } } - public init(account: Account, title: @escaping (PresentationStrings) -> String, confirmation: @escaping (PeerId) -> Signal = { _ in .single(true) }) { + public init(account: Account, title: @escaping (PresentationStrings) -> String, options: [ContactListAdditionalOption] = [], confirmation: @escaping (PeerId) -> Signal = { _ in .single(true) }) { self.account = account self.titleProducer = title + self.options = options self.confirmation = confirmation self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } @@ -112,7 +114,7 @@ public class ContactSelectionController: ViewController { } override public func loadDisplayNode() { - self.displayNode = ContactSelectionControllerNode(account: self.account) + self.displayNode = ContactSelectionControllerNode(account: self.account, options: self.options) self._ready.set(self.contactsNode.contactListNode.ready) self.contactsNode.navigationBar = self.navigationBar @@ -146,7 +148,7 @@ public class ContactSelectionController: ViewController { if let presentationArguments = self.presentationArguments as? ViewControllerPresentationArguments { switch presentationArguments.presentationAnimation { case .modalSheet: - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Cancel", style: .plain, target: self, action: #selector(cancelPressed)) + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(cancelPressed)) case .none: break } diff --git a/TelegramUI/ContactSelectionControllerNode.swift b/TelegramUI/ContactSelectionControllerNode.swift index d1704fa1e2..ec3b3b3d36 100644 --- a/TelegramUI/ContactSelectionControllerNode.swift +++ b/TelegramUI/ContactSelectionControllerNode.swift @@ -22,10 +22,10 @@ final class ContactSelectionControllerNode: ASDisplayNode { var presentationData: PresentationData var presentationDataDisposable: Disposable? - init(account: Account) { + init(account: Account, options: [ContactListAdditionalOption]) { self.account = account - self.contactListNode = ContactListNode(account: account, presentation: .natural(displaySearch: true, options: [])) + self.contactListNode = ContactListNode(account: account, presentation: .natural(displaySearch: true, options: options)) self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } @@ -66,7 +66,7 @@ final class ContactSelectionControllerNode: ASDisplayNode { var insets = layout.insets(options: [.input]) insets.top += navigationBarHeight - self.contactListNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: insets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight), transition: transition) + self.contactListNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: insets, safeInsets: layout.safeInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging), transition: transition) self.contactListNode.frame = CGRect(origin: CGPoint(), size: layout.size) @@ -92,7 +92,7 @@ final class ContactSelectionControllerNode: ASDisplayNode { } if let placeholderNode = maybePlaceholderNode { - self.searchDisplayController = SearchDisplayController(theme: self.presentationData.theme, strings: self.presentationData.strings, contentNode: ContactsSearchContainerNode(account: self.account, openPeer: { [weak self] peerId in + self.searchDisplayController = SearchDisplayController(theme: self.presentationData.theme, strings: self.presentationData.strings, contentNode: ContactsSearchContainerNode(account: self.account, onlyWriteable: false, openPeer: { [weak self] peerId in if let requestOpenPeerFromSearch = self?.requestOpenPeerFromSearch { requestOpenPeerFromSearch(peerId) } diff --git a/TelegramUI/ContactsController.swift b/TelegramUI/ContactsController.swift index c09b323f7c..8bddbbdf9b 100644 --- a/TelegramUI/ContactsController.swift +++ b/TelegramUI/ContactsController.swift @@ -34,7 +34,7 @@ public class ContactsController: ViewController { self.title = self.presentationData.strings.Contacts_Title self.tabBarItem.title = self.presentationData.strings.Contacts_Title self.tabBarItem.image = UIImage(bundleImageName: "Chat List/Tabs/IconContacts") - self.tabBarItem.selectedImage = UIImage(bundleImageName: "Chat List/Tabs/IconContactsSelected") + self.tabBarItem.selectedImage = UIImage(bundleImageName: "Chat List/Tabs/IconContacts") self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) @@ -87,7 +87,7 @@ public class ContactsController: ViewController { self.contactsNode.requestOpenPeerFromSearch = { [weak self] peerId in if let strongSelf = self { - (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, peerId: peerId)) + (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, chatLocation: .peer(peerId))) } } @@ -98,7 +98,7 @@ public class ContactsController: ViewController { self.contactsNode.contactListNode.openPeer = { [weak self] peer in if let strongSelf = self { strongSelf.contactsNode.contactListNode.listNode.clearHighlightAnimated(true) - (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, peerId: peer.id)) + (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, chatLocation: .peer(peer.id))) } } diff --git a/TelegramUI/ContactsControllerNode.swift b/TelegramUI/ContactsControllerNode.swift index bf64de3cf2..abeadba7b5 100644 --- a/TelegramUI/ContactsControllerNode.swift +++ b/TelegramUI/ContactsControllerNode.swift @@ -23,7 +23,7 @@ final class ContactsControllerNode: ASDisplayNode { init(account: Account) { self.account = account - self.contactListNode = ContactListNode(account: account, presentation: .orderedByPresence(displayVCard: true)) + self.contactListNode = ContactListNode(account: account, presentation: .orderedByPresence) self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } @@ -75,7 +75,7 @@ final class ContactsControllerNode: ASDisplayNode { } } - self.contactListNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: insets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight), transition: transition) + self.contactListNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: insets, safeInsets: layout.safeInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging), transition: transition) self.contactListNode.frame = CGRect(origin: CGPoint(), size: layout.size) } @@ -97,7 +97,7 @@ final class ContactsControllerNode: ASDisplayNode { } if let placeholderNode = maybePlaceholderNode { - self.searchDisplayController = SearchDisplayController(theme: self.presentationData.theme, strings: self.presentationData.strings, contentNode: ContactsSearchContainerNode(account: self.account, openPeer: { [weak self] peerId in + self.searchDisplayController = SearchDisplayController(theme: self.presentationData.theme, strings: self.presentationData.strings, contentNode: ContactsSearchContainerNode(account: self.account, onlyWriteable: false, openPeer: { [weak self] peerId in if let requestOpenPeerFromSearch = self?.requestOpenPeerFromSearch { requestOpenPeerFromSearch(peerId) } diff --git a/TelegramUI/ContactsPeerItem.swift b/TelegramUI/ContactsPeerItem.swift index de508d35b5..f7eb9e25bd 100644 --- a/TelegramUI/ContactsPeerItem.swift +++ b/TelegramUI/ContactsPeerItem.swift @@ -16,7 +16,7 @@ private let selectableImage = UIImage(bundleImageName: "Contact List/SelectionUn enum ContactsPeerItemStatus { case none case presence(PeerPresence) - case addressName + case addressName(String) } enum ContactsPeerItemSelection: Equatable { @@ -41,6 +41,25 @@ enum ContactsPeerItemSelection: Equatable { } } +struct ContactsPeerItemEditing: Equatable { + let editable: Bool + let editing: Bool + let revealed: Bool + + static func ==(lhs: ContactsPeerItemEditing, rhs: ContactsPeerItemEditing) -> Bool { + if lhs.editable != rhs.editable { + return false + } + if lhs.editing != rhs.editing { + return false + } + if lhs.revealed != rhs.revealed { + return false + } + return true + } +} + class ContactsPeerItem: ListViewItem { let theme: PresentationTheme let strings: PresentationStrings @@ -48,56 +67,60 @@ class ContactsPeerItem: ListViewItem { let peer: Peer? let chatPeer: Peer? let status: ContactsPeerItemStatus + let enabled: Bool let selection: ContactsPeerItemSelection - let hasActiveRevealControls: Bool + let editing: ContactsPeerItemEditing let action: (Peer) -> Void let setPeerIdWithRevealedOptions: ((PeerId?, PeerId?) -> Void)? let deletePeer: ((PeerId) -> Void)? - let selectable: Bool = true + let selectable: Bool let headerAccessoryItem: ListViewAccessoryItem? let header: ListViewItemHeader? - init(theme: PresentationTheme, strings: PresentationStrings, account: Account, peer: Peer?, chatPeer: Peer?, status: ContactsPeerItemStatus, selection: ContactsPeerItemSelection, hasActiveRevealControls: Bool, index: PeerNameIndex?, header: ListViewItemHeader?, action: @escaping (Peer) -> Void, setPeerIdWithRevealedOptions: ((PeerId?, PeerId?) -> Void)? = nil, deletePeer: ((PeerId) -> Void)? = nil) { + init(theme: PresentationTheme, strings: PresentationStrings, account: Account, peer: Peer?, chatPeer: Peer?, status: ContactsPeerItemStatus, enabled: Bool, selection: ContactsPeerItemSelection, editing: ContactsPeerItemEditing, index: PeerNameIndex?, header: ListViewItemHeader?, action: @escaping (Peer) -> Void, setPeerIdWithRevealedOptions: ((PeerId?, PeerId?) -> Void)? = nil, deletePeer: ((PeerId) -> Void)? = nil) { self.theme = theme self.strings = strings self.account = account self.peer = peer self.chatPeer = chatPeer self.status = status + self.enabled = enabled self.selection = selection - self.hasActiveRevealControls = hasActiveRevealControls + self.editing = editing self.action = action self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions self.deletePeer = deletePeer self.header = header + self.selectable = self.enabled + if let index = index { var letter: String = "#" if let user = peer as? TelegramUser { switch index { case .firstNameFirst: if let firstName = user.firstName, !firstName.isEmpty { - letter = firstName.substring(to: firstName.index(after: firstName.startIndex)).uppercased() + letter = String(firstName.prefix(1)).uppercased() } else if let lastName = user.lastName, !lastName.isEmpty { - letter = lastName.substring(to: lastName.index(after: lastName.startIndex)).uppercased() + letter = String(lastName.prefix(1)).uppercased() } case .lastNameFirst: if let lastName = user.lastName, !lastName.isEmpty { - letter = lastName.substring(to: lastName.index(after: lastName.startIndex)).uppercased() + letter = String(lastName.prefix(1)).uppercased() } else if let firstName = user.firstName, !firstName.isEmpty { - letter = firstName.substring(to: firstName.index(after: firstName.startIndex)).uppercased() + letter = String(firstName.prefix(1)).uppercased() } } } else if let group = peer as? TelegramGroup { if !group.title.isEmpty { - letter = group.title.substring(to: group.title.index(after: group.title.startIndex)).uppercased() + letter = String(group.title.prefix(1)).uppercased() } } else if let channel = peer as? TelegramChannel { if !channel.title.isEmpty { - letter = channel.title.substring(to: channel.title.index(after: channel.title.startIndex)).uppercased() + letter = String(channel.title.prefix(1)).uppercased() } } self.headerAccessoryItem = ContactsSectionHeaderAccessoryItem(sectionHeader: .letter(letter), theme: theme) @@ -106,12 +129,12 @@ class ContactsPeerItem: ListViewItem { } } - func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { async { let node = ContactsPeerItemNode() let makeLayout = node.asyncLayout() let (first, last, firstWithHeader) = ContactsPeerItem.mergeType(item: self, previousItem: previousItem, nextItem: nextItem) - let (nodeLayout, nodeApply) = makeLayout(self, width, first, last, firstWithHeader) + let (nodeLayout, nodeApply) = makeLayout(self, params, first, last, firstWithHeader) node.contentSize = nodeLayout.contentSize node.insets = nodeLayout.insets @@ -124,13 +147,13 @@ class ContactsPeerItem: ListViewItem { } } - func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { if let node = node as? ContactsPeerItemNode { Queue.mainQueue().async { let layout = node.asyncLayout() async { let (first, last, firstWithHeader) = ContactsPeerItem.mergeType(item: self, previousItem: previousItem, nextItem: nextItem) - let (nodeLayout, apply) = layout(self, width, first, last, firstWithHeader) + let (nodeLayout, apply) = layout(self, params, first, last, firstWithHeader) Queue.mainQueue().async { completion(nodeLayout, { apply().1(animation.isAnimated) @@ -196,7 +219,7 @@ class ContactsPeerItemNode: ItemListRevealOptionsItemNode { private var avatarState: (Account, Peer?)? private var peerPresenceManager: PeerPresenceStatusManager? - private var layoutParams: (ContactsPeerItem, CGFloat, Bool, Bool, Bool)? + private var layoutParams: (ContactsPeerItem, ListViewItemLayoutParams, Bool, Bool, Bool)? var peer: Peer? { return self.layoutParams?.0.peer } @@ -236,20 +259,20 @@ class ContactsPeerItemNode: ItemListRevealOptionsItemNode { }) } - override func layoutForWidth(_ width: CGFloat, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { + override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { if let (item, _, _, _, _) = self.layoutParams { let (first, last, firstWithHeader) = ContactsPeerItem.mergeType(item: item, previousItem: previousItem, nextItem: nextItem) - self.layoutParams = (item, width, first, last, firstWithHeader) + self.layoutParams = (item, params, first, last, firstWithHeader) let makeLayout = self.asyncLayout() - let (nodeLayout, nodeApply) = makeLayout(item, width, first, last, firstWithHeader) + let (nodeLayout, nodeApply) = makeLayout(item, params, first, last, firstWithHeader) self.contentSize = nodeLayout.contentSize self.insets = nodeLayout.insets let _ = nodeApply() } } - override func setHighlighted(_ highlighted: Bool, animated: Bool) { - super.setHighlighted(highlighted, animated: animated) + override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { + super.setHighlighted(highlighted, at: point, animated: animated) if highlighted && self.selectionNode == nil { self.highlightedBackgroundNode.alpha = 1.0 @@ -274,21 +297,21 @@ class ContactsPeerItemNode: ItemListRevealOptionsItemNode { } } - func asyncLayout() -> (_ item: ContactsPeerItem, _ width: CGFloat, _ first: Bool, _ last: Bool, _ firstWithHeader: Bool) -> (ListViewItemNodeLayout, () -> (Signal?, (Bool) -> Void)) { + func asyncLayout() -> (_ item: ContactsPeerItem, _ params: ListViewItemLayoutParams, _ first: Bool, _ last: Bool, _ firstWithHeader: Bool) -> (ListViewItemNodeLayout, () -> (Signal?, (Bool) -> Void)) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeStatusLayout = TextNode.asyncLayout(self.statusNode) let currentSelectionNode = self.selectionNode let currentItem = self.layoutParams?.0 - return { [weak self] item, width, first, last, firstWithHeader in + return { [weak self] item, params, first, last, firstWithHeader in var updatedTheme: PresentationTheme? if currentItem?.theme !== item.theme { updatedTheme = item.theme } - var leftInset: CGFloat = 65.0 - let rightInset: CGFloat = 10.0 + var leftInset: CGFloat = 65.0 + params.leftInset + let rightInset: CGFloat = 10.0 + params.rightInset let updatedSelectionNode: ASImageNode? var updatedSelectionImage: UIImage? @@ -332,7 +355,9 @@ class ContactsPeerItemNode: ItemListRevealOptionsItemNode { textColor = item.theme.list.itemPrimaryTextColor } if let user = peer as? TelegramUser { - if let firstName = user.firstName, let lastName = user.lastName, !firstName.isEmpty, !lastName.isEmpty { + if peer.id == item.account.peerId { + titleAttributedString = NSAttributedString(string: item.strings.DialogList_SavedMessages, font: titleBoldFont, textColor: textColor) + } else if let firstName = user.firstName, let lastName = user.lastName, !firstName.isEmpty, !lastName.isEmpty { let string = NSMutableAttributedString() string.append(NSAttributedString(string: firstName, font: titleFont, textColor: textColor)) string.append(NSAttributedString(string: " ", font: titleFont, textColor: textColor)) @@ -358,12 +383,23 @@ class ContactsPeerItemNode: ItemListRevealOptionsItemNode { if let presence = presence as? TelegramUserPresence { userPresence = presence let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 - let (string, activity) = stringAndActivityForUserPresence(strings: item.strings, presence: presence, relativeTo: Int32(timestamp)) + let (string, activity) = stringAndActivityForUserPresence(strings: item.strings, timeFormat: .regular, presence: presence, relativeTo: Int32(timestamp)) statusAttributedString = NSAttributedString(string: string, font: statusFont, textColor: activity ? item.theme.list.itemAccentColor : item.theme.list.itemSecondaryTextColor) } - case .addressName: + case let .addressName(suffix): if let addressName = peer.addressName { - statusAttributedString = NSAttributedString(string: "@" + addressName, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor) + let addressNameString = NSAttributedString(string: "@" + addressName, font: statusFont, textColor: item.theme.list.itemAccentColor) + if !suffix.isEmpty { + let suffixString = NSAttributedString(string: suffix, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor) + let finalString = NSMutableAttributedString() + finalString.append(addressNameString) + finalString.append(suffixString) + statusAttributedString = finalString + } else { + statusAttributedString = addressNameString + } + } else if !suffix.isEmpty { + statusAttributedString = NSAttributedString(string: suffix, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor) } } } @@ -373,11 +409,11 @@ class ContactsPeerItemNode: ItemListRevealOptionsItemNode { additionalTitleInset += 3.0 + verificationIconImage.size.width } - let (titleLayout, titleApply) = makeTitleLayout(titleAttributedString, nil, 1, .end, CGSize(width: max(0.0, width - leftInset - rightInset - additionalTitleInset), height: CGFloat.infinity), .natural, nil, UIEdgeInsets()) + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(0.0, params.width - leftInset - rightInset - additionalTitleInset), height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let (statusLayout, statusApply) = makeStatusLayout(statusAttributedString, nil, 1, .end, CGSize(width: max(0.0, width - leftInset - rightInset), height: CGFloat.infinity), .natural, nil, UIEdgeInsets()) + let (statusLayout, statusApply) = makeStatusLayout(TextNodeLayoutArguments(attributedString: statusAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(0.0, params.width - leftInset - rightInset), height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: width, height: 48.0), insets: UIEdgeInsets(top: firstWithHeader ? 29.0 : 0.0, left: 0.0, bottom: 0.0, right: 0.0)) + let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 48.0), insets: UIEdgeInsets(top: firstWithHeader ? 29.0 : 0.0, left: 0.0, bottom: 0.0, right: 0.0)) let titleFrame: CGRect if statusAttributedString != nil { @@ -389,12 +425,16 @@ class ContactsPeerItemNode: ItemListRevealOptionsItemNode { return (nodeLayout, { [weak self] in if let strongSelf = self { if let peer = item.peer { - strongSelf.avatarNode.setPeer(account: item.account, peer: peer) + var overrideImage: AvatarNodeImageOverride? + if peer.id == item.account.peerId { + overrideImage = .savedMessagesIcon + } + strongSelf.avatarNode.setPeer(account: item.account, peer: peer, overrideImage: overrideImage) } return (strongSelf.avatarNode.ready, { [weak strongSelf] animated in if let strongSelf = strongSelf { - strongSelf.layoutParams = (item, width, first, last, firstWithHeader) + strongSelf.layoutParams = (item, params, first, last, firstWithHeader) let transition: ContainedViewLayoutTransition if animated { @@ -406,8 +446,8 @@ class ContactsPeerItemNode: ItemListRevealOptionsItemNode { let revealOffset = strongSelf.revealOffset if let _ = updatedTheme { - strongSelf.separatorNode.backgroundColor = item.theme.list.itemSeparatorColor - strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBackgroundColor + strongSelf.separatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor + strongSelf.backgroundNode.backgroundColor = item.theme.list.plainBackgroundColor strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor } @@ -416,6 +456,9 @@ class ContactsPeerItemNode: ItemListRevealOptionsItemNode { let _ = titleApply() transition.updateFrame(node: strongSelf.titleNode, frame: titleFrame.offsetBy(dx: revealOffset, dy: 0.0)) + strongSelf.titleNode.alpha = item.enabled ? 1.0 : 0.4 + strongSelf.statusNode.alpha = item.enabled ? 1.0 : 0.4 + let _ = statusApply() transition.updateFrame(node: strongSelf.statusNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset, y: 25.0), size: statusLayout.size)) @@ -448,7 +491,7 @@ class ContactsPeerItemNode: ItemListRevealOptionsItemNode { updatedSelectionNode.image = updatedSelectionImage } if let updatedSelectionImage = updatedSelectionImage { - updatedSelectionNode.frame = CGRect(origin: CGPoint(x: 10.0, y: floor((nodeLayout.contentSize.height - updatedSelectionImage.size.height) / 2.0)), size: updatedSelectionImage.size) + updatedSelectionNode.frame = CGRect(origin: CGPoint(x: params.leftInset + 10.0, y: floor((nodeLayout.contentSize.height - updatedSelectionImage.size.height) / 2.0)), size: updatedSelectionImage.size) } } else if let selectionNode = strongSelf.selectionNode { selectionNode.removeFromSupernode() @@ -458,15 +501,22 @@ class ContactsPeerItemNode: ItemListRevealOptionsItemNode { let topHighlightInset: CGFloat = (first || !nodeLayout.insets.top.isZero) ? 0.0 : separatorHeight strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: nodeLayout.contentSize.width, height: nodeLayout.contentSize.height)) strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -nodeLayout.insets.top - topHighlightInset), size: CGSize(width: nodeLayout.size.width, height: nodeLayout.size.height + topHighlightInset)) - strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: nodeLayout.contentSize.height - separatorHeight), size: CGSize(width: max(0.0, nodeLayout.size.width - 65.0), height: separatorHeight)) + strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: nodeLayout.contentSize.height - separatorHeight), size: CGSize(width: max(0.0, nodeLayout.size.width - leftInset), height: separatorHeight)) strongSelf.separatorNode.isHidden = last if let userPresence = userPresence { strongSelf.peerPresenceManager?.reset(presence: userPresence) } - strongSelf.setRevealOptions([ItemListRevealOption(key: 0, title: item.strings.Common_Delete, icon: nil, color: UIColor(rgb: 0xff3824))]) - strongSelf.setRevealOptionsOpened(item.hasActiveRevealControls, animated: animated) + strongSelf.updateLayout(size: nodeLayout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) + + + if item.editing.editable { + strongSelf.setRevealOptions([ItemListRevealOption(key: 0, title: item.strings.Common_Delete, icon: nil, color: item.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.theme.list.itemDisclosureActions.destructive.foregroundColor)]) + strongSelf.setRevealOptionsOpened(item.editing.revealed, animated: animated) + } else { + strongSelf.setRevealOptions([]) + } } }) } else { diff --git a/TelegramUI/ContactsSearchContainerNode.swift b/TelegramUI/ContactsSearchContainerNode.swift index 8bd8fef6bb..fa5cf45c57 100644 --- a/TelegramUI/ContactsSearchContainerNode.swift +++ b/TelegramUI/ContactsSearchContainerNode.swift @@ -22,7 +22,7 @@ final class ContactsSearchContainerNode: SearchDisplayControllerContentNode { private var presentationData: PresentationData private let themeAndStringsPromise: Promise<(PresentationTheme, PresentationStrings)> - init(account: Account, openPeer: @escaping (PeerId) -> Void) { + init(account: Account, onlyWriteable: Bool, openPeer: @escaping (PeerId) -> Void) { self.account = account self.openPeer = openPeer @@ -70,7 +70,12 @@ final class ContactsSearchContainerNode: SearchDisplayControllerContentNode { for item in items { switch item { case let .peer(peer, theme, strings): - listItems.append(ContactsPeerItem(theme: theme, strings: strings, account: account, peer: peer, chatPeer: peer, status: .none, selection: .none, hasActiveRevealControls: false, index: nil, header: nil, action: { [weak self] peer in + var enabled = true + if onlyWriteable { + enabled = canSendMessagesToPeer(peer) + } + + listItems.append(ContactsPeerItem(theme: theme, strings: strings, account: account, peer: peer, chatPeer: peer, status: .none, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: nil, action: { [weak self] peer in if let openPeer = self?.openPeer { self?.listNode.clearHighlightAnimated(true) openPeer(peer.id) diff --git a/TelegramUI/ContactsSectionHeaderAccessoryItem.swift b/TelegramUI/ContactsSectionHeaderAccessoryItem.swift index 1ca2b8ee9b..b58111bee2 100644 --- a/TelegramUI/ContactsSectionHeaderAccessoryItem.swift +++ b/TelegramUI/ContactsSectionHeaderAccessoryItem.swift @@ -68,7 +68,8 @@ private final class ContactsSectionHeaderAccessoryItemNode: ListViewAccessoryIte self.addSubnode(self.sectionHeaderNode) } - override func layout() { - self.sectionHeaderNode.frame = CGRect(origin: CGPoint(), size: self.bounds.size) + override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) { + self.sectionHeaderNode.frame = CGRect(origin: CGPoint(), size: size) + self.sectionHeaderNode.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset) } } diff --git a/TelegramUI/ContactsVCardItem.swift b/TelegramUI/ContactsVCardItem.swift deleted file mode 100644 index 6a0a73eae6..0000000000 --- a/TelegramUI/ContactsVCardItem.swift +++ /dev/null @@ -1,214 +0,0 @@ -import Foundation -import UIKit -import AsyncDisplayKit -import Postbox -import Display -import SwiftSignalKit -import TelegramCore - -private let titleFont = Font.regular(20.0) -private let statusFont = Font.regular(14.0) - -class ContactsVCardItem: ListViewItem { - let theme: PresentationTheme - let strings: PresentationStrings - let account: Account - let peer: Peer - let action: (Peer) -> Void - let selectable: Bool = true - - init(theme: PresentationTheme, strings: PresentationStrings, account: Account, peer: Peer, action: @escaping (Peer) -> Void) { - self.theme = theme - self.strings = strings - self.account = account - self.peer = peer - self.action = action - } - - func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { - async { - let node = ContactsVCardItemNode() - let makeLayout = node.asyncLayout() - let (nodeLayout, nodeApply) = makeLayout(self.account, self.peer, self, width, previousItem != nil, nextItem != nil) - node.contentSize = nodeLayout.contentSize - node.insets = nodeLayout.insets - - completion(node, { - return (nil, { nodeApply() }) - }) - } - } - - func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { - if let node = node as? ContactsVCardItemNode { - Queue.mainQueue().async { - let layout = node.asyncLayout() - async { - let first = previousItem == nil - let last = nextItem == nil - - let (nodeLayout, apply) = layout(self.account, self.peer, self, width, first, last) - Queue.mainQueue().async { - completion(nodeLayout, { - apply() - }) - } - } - } - } - } - - func selected(listView: ListView) { - self.action(self.peer) - } -} - -private let separatorHeight = 1.0 / UIScreen.main.scale - -private let avatarFont: UIFont = UIFont(name: "ArialRoundedMTBold", size: 15.0)! - -class ContactsVCardItemNode: ListViewItemNode { - private let separatorNode: ASDisplayNode - private let highlightedBackgroundNode: ASDisplayNode - - private let avatarNode: AvatarNode - private let titleNode: TextNode - private let statusNode: TextNode - - private var account: Account? - private var peer: Peer? - private var avatarState: (Account, Peer)? - private var item: ContactsVCardItem? - - required init() { - self.separatorNode = ASDisplayNode() - self.separatorNode.isLayerBacked = true - - self.highlightedBackgroundNode = ASDisplayNode() - self.highlightedBackgroundNode.isLayerBacked = true - - self.avatarNode = AvatarNode(font: avatarFont) - self.avatarNode.isLayerBacked = true - - self.titleNode = TextNode() - self.statusNode = TextNode() - - super.init(layerBacked: false, dynamicBounce: false) - - self.addSubnode(self.separatorNode) - self.addSubnode(self.avatarNode) - self.addSubnode(self.titleNode) - self.addSubnode(self.statusNode) - } - - override func layoutForWidth(_ width: CGFloat, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { - let makeLayout = self.asyncLayout() - let (nodeLayout, nodeApply) = makeLayout(self.account, self.peer, item as! ContactsVCardItem, width, previousItem != nil, nextItem != nil) - self.contentSize = nodeLayout.contentSize - self.insets = nodeLayout.insets - nodeApply() - } - - private func updateBackgroundAndSeparatorsLayout(layout: ListViewItemNodeLayout) { - self.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -layout.insets.top - separatorHeight), size: CGSize(width: layout.size.width, height: layout.size.height + separatorHeight)) - self.separatorNode.frame = CGRect(origin: CGPoint(x: 65.0, y: layout.size.height - separatorHeight), size: CGSize(width: max(0.0, layout.size.width - 65.0), height: separatorHeight)) - } - - override func setHighlighted(_ highlighted: Bool, animated: Bool) { - super.setHighlighted(highlighted, animated: animated) - - if highlighted { - self.highlightedBackgroundNode.alpha = 1.0 - if self.highlightedBackgroundNode.supernode == nil { - self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: self.separatorNode) - } - } else { - if self.highlightedBackgroundNode.supernode != nil { - if animated { - self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in - if let strongSelf = self { - if completed { - strongSelf.highlightedBackgroundNode.removeFromSupernode() - } - } - }) - self.highlightedBackgroundNode.alpha = 0.0 - } else { - self.highlightedBackgroundNode.removeFromSupernode() - } - } - } - } - - func asyncLayout() -> (_ account: Account?, _ peer: Peer?, _ item: ContactsVCardItem, _ width: CGFloat, _ first: Bool, _ last: Bool) -> (ListViewItemNodeLayout, () -> Void) { - let makeTitleLayout = TextNode.asyncLayout(self.titleNode) - let makeStatusLayout = TextNode.asyncLayout(self.statusNode) - - let currentItem = self.item - - return { [weak self] account, peer, item, width, first, last in - var updatedTheme: PresentationTheme? - - if currentItem?.theme !== item.theme { - updatedTheme = item.theme - } - - let leftInset: CGFloat = 91.0 - let rightInset: CGFloat = 10.0 - - var titleAttributedString: NSAttributedString? - var statusAttributedString: NSAttributedString? - - if let peer = peer { - if let user = peer as? TelegramUser { - titleAttributedString = NSAttributedString(string: user.displayTitle, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor) - - if let phone = user.phone { - statusAttributedString = NSAttributedString(string: formatPhoneNumber(phone), font: statusFont, textColor: item.theme.list.itemSecondaryTextColor) - } - } else if let group = peer as? TelegramGroup { - titleAttributedString = NSAttributedString(string: group.title, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor) - statusAttributedString = NSAttributedString(string: item.strings.Group_Status, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor) - } else if let channel = peer as? TelegramChannel { - titleAttributedString = NSAttributedString(string: channel.title, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor) - statusAttributedString = NSAttributedString(string: item.strings.Channel_Status, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor) - } - } - - let (titleLayout, titleApply) = makeTitleLayout(titleAttributedString, nil, 1, .end, CGSize(width: max(0.0, width - leftInset - rightInset), height: CGFloat.infinity), .natural, nil, UIEdgeInsets()) - - let (statusLayout, statusApply) = makeStatusLayout(statusAttributedString, nil, 1, .end, CGSize(width: max(0.0, width - leftInset - rightInset), height: CGFloat.infinity), .natural, nil, UIEdgeInsets()) - - let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: width, height: 78.0), insets: UIEdgeInsets()) - - return (nodeLayout, { [weak self] in - if let strongSelf = self { - strongSelf.item = item - strongSelf.peer = peer - strongSelf.account = account - - if let _ = updatedTheme { - strongSelf.separatorNode.backgroundColor = item.theme.list.itemSeparatorColor - //strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBackgroundColor - strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor - } - - if let peer = peer, let account = account, strongSelf.avatarState == nil || strongSelf.avatarState!.0 !== account || !strongSelf.avatarState!.1.isEqual(peer) { - strongSelf.avatarNode.setPeer(account: account, peer: peer) - } - - strongSelf.avatarNode.frame = CGRect(origin: CGPoint(x: 14.0, y: 6.0), size: CGSize(width: 60.0, height: 60.0)) - - let _ = titleApply() - strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 15.0), size: titleLayout.size) - - let _ = statusApply() - strongSelf.statusNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 40.0), size: statusLayout.size) - - strongSelf.updateBackgroundAndSeparatorsLayout(layout: nodeLayout) - strongSelf.separatorNode.isHidden = true - } - }) - } - } -} diff --git a/TelegramUI/ConvertToSupergroupController.swift b/TelegramUI/ConvertToSupergroupController.swift index 17db8470d2..c1d706b1dd 100644 --- a/TelegramUI/ConvertToSupergroupController.swift +++ b/TelegramUI/ConvertToSupergroupController.swift @@ -18,9 +18,9 @@ private enum ConvertToSupergroupSection: Int32 { } private enum ConvertToSupergroupEntry: ItemListNodeEntry { - case info - case action - case actionInfo + case info(PresentationTheme, String) + case action(PresentationTheme, String) + case actionInfo(PresentationTheme, String) var section: ItemListSectionId { switch self { @@ -43,7 +43,26 @@ private enum ConvertToSupergroupEntry: ItemListNodeEntry { } static func ==(lhs: ConvertToSupergroupEntry, rhs: ConvertToSupergroupEntry) -> Bool { - return lhs.stableId == rhs.stableId + switch lhs { + case let .info(lhsTheme, lhsText): + if case let .info(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .action(lhsTheme, lhsText): + if case let .action(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .actionInfo(lhsTheme, lhsText): + if case let .actionInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + } } static func <(lhs: ConvertToSupergroupEntry, rhs: ConvertToSupergroupEntry) -> Bool { @@ -52,14 +71,14 @@ private enum ConvertToSupergroupEntry: ItemListNodeEntry { func item(_ arguments: ConvertToSupergroupArguments) -> ListViewItem { switch self { - case .info: - return ItemListTextItem(text: .plain("In supergroups:\n• New members can see the full message history\n• Deleted messages will disappear for all members\n• Admins can pin important messages\n• Creator can set a public link for the group"), sectionId: self.section) - case .action: - return ItemListActionItem(title: "Convert to Supergroup", kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { + case let .info(theme, text): + return ItemListTextItem(theme: theme, text: .markdown(text), sectionId: self.section) + case let .action(theme, title): + return ItemListActionItem(theme: theme, title: title, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.convert() }) - case .actionInfo: - return ItemListTextItem(text: .plain("Note: this action can't be undone"), sectionId: self.section) + case let .actionInfo(theme, text): + return ItemListTextItem(theme: theme, text: .markdown(text), sectionId: self.section) } } } @@ -83,12 +102,12 @@ private struct ConvertToSupergroupState: Equatable { } } -private func convertToSupergroupEntries() -> [ConvertToSupergroupEntry] { +private func convertToSupergroupEntries(presentationData: PresentationData) -> [ConvertToSupergroupEntry] { var entries: [ConvertToSupergroupEntry] = [] - entries.append(.info) - entries.append(.action) - entries.append(.actionInfo) + entries.append(.info(presentationData.theme, "\(presentationData.strings.ConvertToSupergroup_HelpTitle)\n\(presentationData.strings.ConvertToSupergroup_HelpText)")) + entries.append(.action(presentationData.theme, presentationData.strings.GroupInfo_ConvertToSupergroup)) + entries.append(.actionInfo(presentationData.theme, presentationData.strings.ConvertToSupergroup_Note)) return entries } @@ -118,7 +137,7 @@ public func convertToSupergroupController(account: Account, peerId: PeerId) -> V if !alreadyConverting { convertDisposable.set((convertGroupToSupergroup(account: account, peerId: peerId) |> deliverOnMainQueue).start(next: { createdPeerId in - replaceControllerImpl?(ChatController(account: account, peerId: createdPeerId)) + replaceControllerImpl?(ChatController(account: account, chatLocation: .peer(createdPeerId))) })) } }) @@ -132,8 +151,8 @@ public func convertToSupergroupController(account: Account, peerId: PeerId) -> V rightNavigationButton = ItemListNavigationButton(title: "", style: .activity, enabled: true, action: {}) } - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text("Supergroup"), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: "Back")) - let listState = ItemListNodeState(entries: convertToSupergroupEntries(), style: .blocks) + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.ConvertToSupergroup_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) + let listState = ItemListNodeState(entries: convertToSupergroupEntries(presentationData: presentationData), style: .blocks) return (controllerState, (listState, arguments)) } diff --git a/TelegramUI/CountryList.swift b/TelegramUI/CountryList.swift new file mode 100644 index 0000000000..2c701b849d --- /dev/null +++ b/TelegramUI/CountryList.swift @@ -0,0 +1,63 @@ +import Foundation + +private func loadCountriesInfo() -> [(Int, String, String)] { + guard let filePath = Bundle.main.path(forResource: "PhoneCountries", ofType: "txt") else { + return [] + } + guard let stringData = try? Data(contentsOf: URL(fileURLWithPath: filePath)) else { + return [] + } + guard let data = String(data: stringData, encoding: .utf8) else { + return [] + } + + let delimiter = ";" + let endOfLine = "\n" + + var array: [(Int, String, String)] = [] + + var currentLocation = data.startIndex + while true { + guard let codeRange = data.range(of: delimiter, options: [], range: currentLocation ..< data.endIndex, locale: nil) else { + break + } + + guard let countryCode = Int(data[currentLocation ..< codeRange.lowerBound]) else { + break + } + + guard let idRange = data.range(of: delimiter, options: [], range: codeRange.upperBound ..< data.endIndex) else { + break + } + + let countryId = String(data[codeRange.upperBound ..< idRange.lowerBound]) + + let countryName: String + let nameRange = data.range(of: endOfLine, options: [], range: idRange.upperBound ..< data.endIndex) + if let nameRange = nameRange { + countryName = String(data[idRange.upperBound ..< nameRange.lowerBound]) + currentLocation = nameRange.upperBound + } else { + countryName = String(data[idRange.upperBound ..< data.index(data.endIndex, offsetBy: -1)]) + } + + array.append((countryCode, countryId, countryName)) + + if nameRange == nil { + break + } + } + return array +} + +let phoneCountriesInfo = loadCountriesInfo() + +let countryCodeToIdAndName: [Int: (String, String)] = { + var dict: [Int: (String, String)] = [:] + for (code, id, name) in phoneCountriesInfo { + if dict[code] == nil { + dict[code] = (id, name) + } + } + return dict +}() diff --git a/TelegramUI/CreateChannelController.swift b/TelegramUI/CreateChannelController.swift index aed5db3f8e..1d2438bc08 100644 --- a/TelegramUI/CreateChannelController.swift +++ b/TelegramUI/CreateChannelController.swift @@ -17,6 +17,25 @@ private enum CreateChannelSection: Int32 { case description } +private enum CreateChannelEntryTag: ItemListItemTag { + case info + + func isEqual(to other: ItemListItemTag) -> Bool { + if let other = other as? CreateChannelEntryTag { + switch self { + case .info: + if case .info = other { + return true + } else { + return false + } + } + } else { + return false + } + } +} + private enum CreateChannelEntry: ItemListNodeEntry { case channelInfo(PresentationTheme, PresentationStrings, Peer?, ItemListAvatarAndNameInfoItemState) case setProfilePhoto(PresentationTheme, String) @@ -98,16 +117,16 @@ private enum CreateChannelEntry: ItemListNodeEntry { func item(_ arguments: CreateChannelArguments) -> ListViewItem { switch self { case let .channelInfo(theme, strings, peer, state): - return ItemListAvatarAndNameInfoItem(account: arguments.account, theme: theme, strings: strings, peer: peer, presence: nil, cachedData: nil, state: state, sectionId: ItemListSectionId(self.section), style: .blocks(withTopInset: false), editingNameUpdated: { editingName in + return ItemListAvatarAndNameInfoItem(account: arguments.account, theme: theme, strings: strings, mode: .generic, peer: peer, presence: nil, cachedData: nil, state: state, sectionId: ItemListSectionId(self.section), style: .blocks(withTopInset: false), editingNameUpdated: { editingName in arguments.updateEditingName(editingName) }, avatarTapped: { - }) + }, tag: CreateChannelEntryTag.info) case let .setProfilePhoto(theme, text): return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: ItemListSectionId(self.section), style: .blocks, action: { }) case let .descriptionSetup(theme, text, value): - return ItemListMultilineInputItem(theme: theme, text: value, placeholder: text, sectionId: self.section, style: .blocks, textUpdated: { updatedText in + return ItemListMultilineInputItem(theme: theme, text: value, placeholder: text, maxLength: 1000, sectionId: self.section, style: .blocks, textUpdated: { updatedText in arguments.updateEditingDescriptionText(updatedText) }, action: { @@ -131,7 +150,7 @@ private struct CreateChannelState: Equatable { init() { self.creating = false - self.editingName = .title(title: "") + self.editingName = .title(title: "", type: .channel) self.editingDescriptionText = "" } @@ -219,13 +238,13 @@ public func createChannelController(account: Account) -> ViewController { if state.creating { rightNavigationButton = ItemListNavigationButton(title: "", style: .activity, enabled: true, action: {}) } else { - rightNavigationButton = ItemListNavigationButton(title: "Next", style: .bold, enabled: !state.editingName.composedTitle.isEmpty, action: { + rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Next, style: .bold, enabled: !state.editingName.composedTitle.isEmpty, action: { arguments.done() }) } - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text("Create Channel"), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: "Back")) - let listState = ItemListNodeState(entries: CreateChannelEntries(presentationData: presentationData, state: state), style: .blocks) + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.ChannelIntro_CreateChannel), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) + let listState = ItemListNodeState(entries: CreateChannelEntries(presentationData: presentationData, state: state), style: .blocks, focusItemTag: CreateChannelEntryTag.info) return (controllerState, (listState, arguments)) } |> afterDisposed { diff --git a/TelegramUI/CreateGroupController.swift b/TelegramUI/CreateGroupController.swift index 10266480ad..a1c14c4d91 100644 --- a/TelegramUI/CreateGroupController.swift +++ b/TelegramUI/CreateGroupController.swift @@ -16,6 +16,25 @@ private enum CreateGroupSection: Int32 { case members } +private enum CreateGroupEntryTag: ItemListItemTag { + case info + + func isEqual(to other: ItemListItemTag) -> Bool { + if let other = other as? CreateGroupEntryTag { + switch self { + case .info: + if case .info = other { + return true + } else { + return false + } + } + } else { + return false + } + } +} + private enum CreateGroupEntry: ItemListNodeEntry { case groupInfo(PresentationTheme, PresentationStrings, Peer?, ItemListAvatarAndNameInfoItemState) case setProfilePhoto(PresentationTheme, String) @@ -107,10 +126,10 @@ private enum CreateGroupEntry: ItemListNodeEntry { func item(_ arguments: CreateGroupArguments) -> ListViewItem { switch self { case let .groupInfo(theme, strings, peer, state): - return ItemListAvatarAndNameInfoItem(account: arguments.account, theme: theme, strings: strings, peer: peer, presence: nil, cachedData: nil, state: state, sectionId: ItemListSectionId(self.section), style: .blocks(withTopInset: false), editingNameUpdated: { editingName in + return ItemListAvatarAndNameInfoItem(account: arguments.account, theme: theme, strings: strings, mode: .generic, peer: peer, presence: nil, cachedData: nil, state: state, sectionId: ItemListSectionId(self.section), style: .blocks(withTopInset: false), editingNameUpdated: { editingName in arguments.updateEditingName(editingName) }, avatarTapped: { - }) + }, tag: CreateGroupEntryTag.info) case let .setProfilePhoto(theme, text): return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: ItemListSectionId(self.section), style: .blocks, action: { @@ -182,7 +201,7 @@ private func createGroupEntries(presentationData: PresentationData, state: Creat } public func createGroupController(account: Account, peerIds: [PeerId]) -> ViewController { - let initialState = CreateGroupState(creating: false, editingName: .title(title: "")) + let initialState = CreateGroupState(creating: false, editingName: .title(title: "", type: .group)) let statePromise = ValuePromise(initialState, ignoreRepeated: true) let stateValue = Atomic(value: initialState) let updateState: ((CreateGroupState) -> CreateGroupState) -> Void = { f in @@ -214,7 +233,7 @@ public func createGroupController(account: Account, peerIds: [PeerId]) -> ViewCo } }).start(next: { peerId in if let peerId = peerId { - let controller = ChatController(account: account, peerId: peerId) + let controller = ChatController(account: account, chatLocation: .peer(peerId)) replaceControllerImpl?(controller) } })) @@ -228,13 +247,13 @@ public func createGroupController(account: Account, peerIds: [PeerId]) -> ViewCo if state.creating { rightNavigationButton = ItemListNavigationButton(title: "", style: .activity, enabled: true, action: {}) } else { - rightNavigationButton = ItemListNavigationButton(title: "Create", style: .bold, enabled: !state.editingName.composedTitle.isEmpty, action: { + rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Compose_Create, style: .bold, enabled: !state.editingName.composedTitle.isEmpty, action: { arguments.done() }) } - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text("Create Group"), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: "Back")) - let listState = ItemListNodeState(entries: createGroupEntries(presentationData: presentationData, state: state, peerIds: peerIds, view: view), style: .blocks) + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.Compose_NewGroup), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) + let listState = ItemListNodeState(entries: createGroupEntries(presentationData: presentationData, state: state, peerIds: peerIds, view: view), style: .blocks, focusItemTag: CreateGroupEntryTag.info) return (controllerState, (listState, arguments)) } |> afterDisposed { diff --git a/TelegramUI/DataAndStorageSettingsController.swift b/TelegramUI/DataAndStorageSettingsController.swift index b16c62a8ae..eeeddf571b 100644 --- a/TelegramUI/DataAndStorageSettingsController.swift +++ b/TelegramUI/DataAndStorageSettingsController.swift @@ -20,18 +20,22 @@ private enum AutomaticDownloadPeers { private final class DataAndStorageControllerArguments { let openStorageUsage: () -> Void let openNetworkUsage: () -> Void + let openProxy: () -> Void let toggleAutomaticDownload: (AutomaticDownloadCategory, AutomaticDownloadPeers, Bool) -> Void let openVoiceUseLessData: () -> Void let toggleSaveIncomingPhotos: (Bool) -> Void let toggleSaveEditedPhotos: (Bool) -> Void + let toggleAutoplayGifs: (Bool) -> Void - init(openStorageUsage: @escaping () -> Void, openNetworkUsage: @escaping () -> Void, toggleAutomaticDownload: @escaping (AutomaticDownloadCategory, AutomaticDownloadPeers, Bool) -> Void, openVoiceUseLessData: @escaping () -> Void, toggleSaveIncomingPhotos: @escaping (Bool) -> Void, toggleSaveEditedPhotos: @escaping (Bool) -> Void) { + init(openStorageUsage: @escaping () -> Void, openNetworkUsage: @escaping () -> Void, openProxy: @escaping () -> Void, toggleAutomaticDownload: @escaping (AutomaticDownloadCategory, AutomaticDownloadPeers, Bool) -> Void, openVoiceUseLessData: @escaping () -> Void, toggleSaveIncomingPhotos: @escaping (Bool) -> Void, toggleSaveEditedPhotos: @escaping (Bool) -> Void, toggleAutoplayGifs: @escaping (Bool) -> Void) { self.openStorageUsage = openStorageUsage self.openNetworkUsage = openNetworkUsage + self.openProxy = openProxy self.toggleAutomaticDownload = toggleAutomaticDownload self.openVoiceUseLessData = openVoiceUseLessData self.toggleSaveIncomingPhotos = toggleSaveIncomingPhotos self.toggleSaveEditedPhotos = toggleSaveEditedPhotos + self.toggleAutoplayGifs = toggleAutoplayGifs } } @@ -42,6 +46,7 @@ private enum DataAndStorageSection: Int32 { case automaticInstantVideoDownload case voiceCalls case other + case connection } private enum DataAndStorageEntry: ItemListNodeEntry { @@ -61,6 +66,9 @@ private enum DataAndStorageEntry: ItemListNodeEntry { case otherHeader(PresentationTheme, String) case saveIncomingPhotos(PresentationTheme, String, Bool) case saveEditedPhotos(PresentationTheme, String, Bool) + case autoplayGifs(PresentationTheme, String, Bool) + case connectionHeader(PresentationTheme, String) + case connectionProxy(PresentationTheme, String, String) var section: ItemListSectionId { switch self { @@ -74,8 +82,10 @@ private enum DataAndStorageEntry: ItemListNodeEntry { return DataAndStorageSection.automaticInstantVideoDownload.rawValue case .voiceCallsHeader, .useLessVoiceData: return DataAndStorageSection.voiceCalls.rawValue - case .otherHeader, .saveIncomingPhotos, .saveEditedPhotos: + case .otherHeader, .saveIncomingPhotos, .saveEditedPhotos, .autoplayGifs: return DataAndStorageSection.other.rawValue + case .connectionHeader, .connectionProxy: + return DataAndStorageSection.connection.rawValue } } @@ -113,6 +123,12 @@ private enum DataAndStorageEntry: ItemListNodeEntry { return 14 case .saveEditedPhotos: return 15 + case .autoplayGifs: + return 16 + case .connectionHeader: + return 17 + case .connectionProxy: + return 18 } } @@ -214,6 +230,24 @@ private enum DataAndStorageEntry: ItemListNodeEntry { } else { return false } + case let .autoplayGifs(lhsTheme, lhsText, lhsValue): + if case let .autoplayGifs(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + case let .connectionHeader(lhsTheme, lhsText): + if case let .connectionHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .connectionProxy(lhsTheme, lhsText, lhsValue): + if case let .connectionProxy(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } } } @@ -277,6 +311,16 @@ private enum DataAndStorageEntry: ItemListNodeEntry { return ItemListSwitchItem(theme: theme, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in arguments.toggleSaveEditedPhotos(value) }) + case let .autoplayGifs(theme, text, value): + return ItemListSwitchItem(theme: theme, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in + arguments.toggleAutoplayGifs(value) + }) + case let .connectionHeader(theme, text): + return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + case let .connectionProxy(theme, text, value): + return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, action: { + arguments.openProxy() + }) } } } @@ -291,15 +335,17 @@ private struct DataAndStorageData: Equatable { let automaticMediaDownloadSettings: AutomaticMediaDownloadSettings let generatedMediaStoreSettings: GeneratedMediaStoreSettings let voiceCallSettings: VoiceCallSettings + let proxySettings: ProxySettings? - init(automaticMediaDownloadSettings: AutomaticMediaDownloadSettings, generatedMediaStoreSettings: GeneratedMediaStoreSettings, voiceCallSettings: VoiceCallSettings) { + init(automaticMediaDownloadSettings: AutomaticMediaDownloadSettings, generatedMediaStoreSettings: GeneratedMediaStoreSettings, voiceCallSettings: VoiceCallSettings, proxySettings: ProxySettings?) { self.automaticMediaDownloadSettings = automaticMediaDownloadSettings self.generatedMediaStoreSettings = generatedMediaStoreSettings self.voiceCallSettings = voiceCallSettings + self.proxySettings = proxySettings } static func ==(lhs: DataAndStorageData, rhs: DataAndStorageData) -> Bool { - return lhs.automaticMediaDownloadSettings == rhs.automaticMediaDownloadSettings && lhs.generatedMediaStoreSettings == rhs.generatedMediaStoreSettings && lhs.voiceCallSettings == rhs.voiceCallSettings + return lhs.automaticMediaDownloadSettings == rhs.automaticMediaDownloadSettings && lhs.generatedMediaStoreSettings == rhs.generatedMediaStoreSettings && lhs.voiceCallSettings == rhs.voiceCallSettings && lhs.proxySettings == rhs.proxySettings } } @@ -340,15 +386,24 @@ private func dataAndStorageControllerEntries(state: DataAndStorageControllerStat entries.append(.useLessVoiceData(presentationData.theme, presentationData.strings.CallSettings_UseLessData, stringForUseLessDataSetting(strings: presentationData.strings, settings: data.voiceCallSettings))) entries.append(.otherHeader(presentationData.theme, presentationData.strings.ChatSettings_Other)) - entries.append(.saveIncomingPhotos(presentationData.theme, presentationData.strings.Settings_SaveIncomingPhotos, data.automaticMediaDownloadSettings.saveIncomingPhotos)) + //entries.append(.saveIncomingPhotos(presentationData.theme, presentationData.strings.Settings_SaveIncomingPhotos, data.automaticMediaDownloadSettings.saveIncomingPhotos)) entries.append(.saveEditedPhotos(presentationData.theme, presentationData.strings.Settings_SaveEditedPhotos, data.generatedMediaStoreSettings.storeEditedPhotos)) + entries.append(.autoplayGifs(presentationData.theme, presentationData.strings.ChatSettings_AutoPlayAnimations, data.automaticMediaDownloadSettings.categories.gif.privateChats)) + + let proxyValue: String + if let _ = data.proxySettings { + proxyValue = presentationData.strings.ChatSettings_ConnectionType_UseSocks5 + } else { + proxyValue = presentationData.strings.GroupInfo_SharedMediaNone + } + entries.append(.connectionHeader(presentationData.theme, presentationData.strings.ChatSettings_ConnectionType_Title.uppercased())) + entries.append(.connectionProxy(presentationData.theme, presentationData.strings.SocksProxySetup_Title, proxyValue)) return entries } func dataAndStorageController(account: Account) -> ViewController { let initialState = DataAndStorageControllerState() - let statePromise = ValuePromise(initialState, ignoreRepeated: true) var pushControllerImpl: ((ViewController) -> Void)? @@ -356,7 +411,8 @@ func dataAndStorageController(account: Account) -> ViewController { let actionsDisposable = DisposableSet() let dataAndStorageDataPromise = Promise() - dataAndStorageDataPromise.set(account.postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.automaticMediaDownloadSettings, ApplicationSpecificPreferencesKeys.generatedMediaStoreSettings, ApplicationSpecificPreferencesKeys.voiceCallSettings]) + dataAndStorageDataPromise.set(account.postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.automaticMediaDownloadSettings, ApplicationSpecificPreferencesKeys.generatedMediaStoreSettings, ApplicationSpecificPreferencesKeys.voiceCallSettings, + PreferencesKeys.proxySettings]) |> map { view -> DataAndStorageData in let automaticMediaDownloadSettings: AutomaticMediaDownloadSettings if let value = view.values[ApplicationSpecificPreferencesKeys.automaticMediaDownloadSettings] as? AutomaticMediaDownloadSettings { @@ -379,13 +435,24 @@ func dataAndStorageController(account: Account) -> ViewController { voiceCallSettings = VoiceCallSettings.defaultSettings } - return DataAndStorageData(automaticMediaDownloadSettings: automaticMediaDownloadSettings, generatedMediaStoreSettings: generatedMediaStoreSettings, voiceCallSettings: voiceCallSettings) + var proxySettings: ProxySettings? + if let value = view.values[PreferencesKeys.proxySettings] as? ProxySettings { + proxySettings = value + } + + return DataAndStorageData(automaticMediaDownloadSettings: automaticMediaDownloadSettings, generatedMediaStoreSettings: generatedMediaStoreSettings, voiceCallSettings: voiceCallSettings, proxySettings: proxySettings) }) let arguments = DataAndStorageControllerArguments(openStorageUsage: { pushControllerImpl?(storageUsageController(account: account)) }, openNetworkUsage: { pushControllerImpl?(networkUsageStatsController(account: account)) + }, openProxy: { + let _ = (account.postbox.modify { modifier -> ProxySettings? in + return modifier.getPreferencesEntry(key: PreferencesKeys.proxySettings) as? ProxySettings + } |> deliverOnMainQueue).start(next: { settings in + pushControllerImpl?(proxySettingsController(account: account, currentSettings: settings)) + }) }, toggleAutomaticDownload: { category, peers, value in let _ = updateMediaDownloadSettingsInteractively(postbox: account.postbox, { current in switch category { @@ -429,6 +496,12 @@ func dataAndStorageController(account: Account) -> ViewController { let _ = updateGeneratedMediaStoreSettingsInteractively(postbox: account.postbox, { current in return current.withUpdatedStoreEditedPhotos(value) }).start() + }, toggleAutoplayGifs: { value in + let _ = updateMediaDownloadSettingsInteractively(postbox: account.postbox, { current in + var updated = current.withUpdatedCategories(current.categories.withUpdatedGif(current.categories.gif.withUpdatedPrivateChats(value))) + updated = updated.withUpdatedCategories(updated.categories.withUpdatedGif(updated.categories.gif.withUpdatedGroupsAndChannels(value))) + return updated + }).start() }) let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get(), dataAndStorageDataPromise.get()) |> deliverOnMainQueue diff --git a/TelegramUI/DateFormat.swift b/TelegramUI/DateFormat.swift new file mode 100644 index 0000000000..81e806f21f --- /dev/null +++ b/TelegramUI/DateFormat.swift @@ -0,0 +1,72 @@ +import Foundation + +func stringForShortTimestamp(hours: Int32, minutes: Int32, timeFormat: PresentationTimeFormat) -> String { + switch timeFormat { + case .regular: + let hourString: String + if hours == 0 { + hourString = "12" + } else if hours > 12 { + hourString = "\(hours - 12)" + } else { + hourString = "\(hours)" + } + + let periodString: String + if hours >= 12 { + periodString = "PM" + } else { + periodString = "AM" + } + if minutes >= 10 { + return "\(hourString):\(minutes) \(periodString)" + } else { + return "\(hourString):0\(minutes) \(periodString)" + } + case .military: + return String(format: "%02d:%02d", arguments: [Int(hours), Int(minutes)]) + } +} + +func stringForMessageTimestamp(timestamp: Int32, timeFormat: PresentationTimeFormat) -> String { + var t = Int(timestamp) + var timeinfo = tm() + localtime_r(&t, &timeinfo) + + return stringForShortTimestamp(hours: timeinfo.tm_hour, minutes: timeinfo.tm_min, timeFormat: timeFormat) +} + +func stringForFullDate(timestamp: Int32, strings: PresentationStrings, timeFormat: PresentationTimeFormat) -> String { + var t: time_t = Int(timestamp) + var timeinfo = tm() + localtime_r(&t, &timeinfo); + + switch timeinfo.tm_mon + 1 { + case 1: + return strings.Time_PreciseDate_m1("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), timeFormat: timeFormat)).0 + case 2: + return strings.Time_PreciseDate_m2("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), timeFormat: timeFormat)).0 + case 3: + return strings.Time_PreciseDate_m3("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), timeFormat: timeFormat)).0 + case 4: + return strings.Time_PreciseDate_m4("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), timeFormat: timeFormat)).0 + case 5: + return strings.Time_PreciseDate_m5("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), timeFormat: timeFormat)).0 + case 6: + return strings.Time_PreciseDate_m6("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), timeFormat: timeFormat)).0 + case 7: + return strings.Time_PreciseDate_m7("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), timeFormat: timeFormat)).0 + case 8: + return strings.Time_PreciseDate_m8("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), timeFormat: timeFormat)).0 + case 9: + return strings.Time_PreciseDate_m9("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), timeFormat: timeFormat)).0 + case 10: + return strings.Time_PreciseDate_m10("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), timeFormat: timeFormat)).0 + case 11: + return strings.Time_PreciseDate_m11("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), timeFormat: timeFormat)).0 + case 12: + return strings.Time_PreciseDate_m12("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), timeFormat: timeFormat)).0 + default: + return "" + } +} diff --git a/TelegramUI/DebugAccountsController.swift b/TelegramUI/DebugAccountsController.swift index 9c275ca54f..3ca6d7417b 100644 --- a/TelegramUI/DebugAccountsController.swift +++ b/TelegramUI/DebugAccountsController.swift @@ -25,8 +25,8 @@ private enum DebugAccountsControllerSection: Int32 { } private enum DebugAccountsControllerEntry: ItemListNodeEntry { - case record(AccountRecord, Bool) - case loginNewAccount + case record(PresentationTheme, AccountRecord, Bool) + case loginNewAccount(PresentationTheme) var section: ItemListSectionId { switch self { @@ -39,7 +39,7 @@ private enum DebugAccountsControllerEntry: ItemListNodeEntry { var stableId: Int64 { switch self { - case let .record(record, _): + case let .record(_, record, _): return record.id.int64 case .loginNewAccount: return Int64.max @@ -48,14 +48,14 @@ private enum DebugAccountsControllerEntry: ItemListNodeEntry { static func ==(lhs: DebugAccountsControllerEntry, rhs: DebugAccountsControllerEntry) -> Bool { switch lhs { - case let .record(record, current): - if case .record(record, current) = rhs { + case let .record(lhsTheme, lhsRecord, lhsCurrent): + if case let .record(rhsTheme, rhsRecord, rhsCurrent) = rhs, lhsTheme === rhsTheme, lhsRecord == rhsRecord, lhsCurrent == rhsCurrent { return true } else { return false } - case .loginNewAccount: - if case .loginNewAccount = rhs { + case let .loginNewAccount(lhsTheme): + if case let .loginNewAccount(rhsTheme) = rhs, lhsTheme === rhsTheme { return true } else { return false @@ -69,28 +69,28 @@ private enum DebugAccountsControllerEntry: ItemListNodeEntry { func item(_ arguments: DebugAccountsControllerArguments) -> ListViewItem { switch self { - case let .record(record, current): - return ItemListCheckboxItem(title: "\(UInt64(bitPattern: record.id.int64))", checked: current, zeroSeparatorInsets: false, sectionId: self.section, action: { + case let .record(theme, record, current): + return ItemListCheckboxItem(theme: theme, title: "\(UInt64(bitPattern: record.id.int64))", checked: current, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.switchAccount(record.id) }) - case .loginNewAccount: - return ItemListActionItem(title: "Login to another account", kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { + case let .loginNewAccount(theme): + return ItemListActionItem(theme: theme, title: "Login to another account", kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.loginNewAccount() }) } } } -private func debugAccountsControllerEntries(view: AccountRecordsView) -> [DebugAccountsControllerEntry] { +private func debugAccountsControllerEntries(view: AccountRecordsView, presentationData: PresentationData) -> [DebugAccountsControllerEntry] { var entries: [DebugAccountsControllerEntry] = [] for entry in view.records.sorted(by: { $0.id < $1.id }) { - entries.append(.record(entry, entry.id == view.currentRecord?.id)) + entries.append(.record(presentationData.theme, entry, entry.id == view.currentRecord?.id)) } - entries.append(.loginNewAccount) + entries.append(.loginNewAccount(presentationData.theme)) return entries } @@ -113,8 +113,8 @@ public func debugAccountsController(account: Account, accountManager: AccountMan let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, accountManager.accountRecords()) |> map { presentationData, view -> (ItemListControllerState, (ItemListNodeState, DebugAccountsControllerEntry.ItemGenerationArguments)) in - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text("Accounts"), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: "Back")) - let listState = ItemListNodeState(entries: debugAccountsControllerEntries(view: view), style: .blocks) + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text("Accounts"), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) + let listState = ItemListNodeState(entries: debugAccountsControllerEntries(view: view, presentationData: presentationData), style: .blocks) return (controllerState, (listState, arguments)) } diff --git a/TelegramUI/DebugController.swift b/TelegramUI/DebugController.swift index 2756d15aff..d5a10db4f1 100644 --- a/TelegramUI/DebugController.swift +++ b/TelegramUI/DebugController.swift @@ -21,12 +21,15 @@ private final class DebugControllerArguments { private enum DebugControllerSection: Int32 { case logs case payments + case logging } private enum DebugControllerEntry: ItemListNodeEntry { case sendLogs(PresentationTheme) case accounts(PresentationTheme) case clearPaymentData(PresentationTheme) + case logToFile(PresentationTheme, Bool) + case logToConsole(PresentationTheme, Bool) var section: ItemListSectionId { switch self { @@ -36,6 +39,8 @@ private enum DebugControllerEntry: ItemListNodeEntry { return DebugControllerSection.logs.rawValue case .clearPaymentData: return DebugControllerSection.payments.rawValue + case .logToFile, .logToConsole: + return DebugControllerSection.logging.rawValue } } @@ -47,6 +52,10 @@ private enum DebugControllerEntry: ItemListNodeEntry { return 1 case .clearPaymentData: return 2 + case .logToFile: + return 3 + case .logToConsole: + return 4 } } @@ -70,6 +79,18 @@ private enum DebugControllerEntry: ItemListNodeEntry { } else { return false } + case let .logToFile(lhsTheme, lhsValue): + if case let .logToFile(rhsTheme, rhsValue) = rhs, lhsTheme === rhsTheme, lhsValue == rhsValue { + return true + } else { + return false + } + case let .logToConsole(lhsTheme, lhsValue): + if case let .logToConsole(rhsTheme, rhsValue) = rhs, lhsTheme === rhsTheme, lhsValue == rhsValue { + return true + } else { + return false + } } } @@ -91,7 +112,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { let messages = logs.map { (name, path) -> EnqueueMessage in let id = arc4random64() let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), resource: LocalFileReferenceMediaResource(localFilePath: path, randomId: id), previewRepresentations: [], mimeType: "application/text", size: nil, attributes: [.FileName(fileName: name)]) - return .message(text: "", attributes: [], media: file, replyToMessageId: nil) + return .message(text: "", attributes: [], media: file, replyToMessageId: nil, localGroupingKey: nil) } let _ = enqueueMessages(account: arguments.account, peerId: peerId, messages: messages).start() } @@ -104,20 +125,35 @@ private enum DebugControllerEntry: ItemListNodeEntry { arguments.pushController(debugAccountsController(account: arguments.account, accountManager: arguments.accountManager)) }) case let .clearPaymentData(theme): - return ItemListDisclosureItem(theme: theme, title: "Clear Payment Data", label: "", sectionId: self.section, style: .blocks, action: { + return ItemListDisclosureItem(theme: theme, title: "Clear Payment Password", label: "", sectionId: self.section, style: .blocks, action: { let _ = cacheTwoStepPasswordToken(postbox: arguments.account.postbox, token: nil).start() }) + case let .logToFile(theme, value): + return ItemListSwitchItem(theme: theme, title: "Log to File", value: value, sectionId: self.section, style: .blocks, updated: { value in + updateLoggingSettings(postbox: arguments.account.postbox, { + $0.withUpdatedLogToFile(value) + }).start() + }) + case let .logToConsole(theme, value): + return ItemListSwitchItem(theme: theme, title: "Log to Console", value: value, sectionId: self.section, style: .blocks, updated: { value in + updateLoggingSettings(postbox: arguments.account.postbox, { + $0.withUpdatedLogToConsole(value) + }).start() + }) } } } -private func debugControllerEntries(presentationData: PresentationData) -> [DebugControllerEntry] { +private func debugControllerEntries(presentationData: PresentationData, loggingSettings: LoggingSettings) -> [DebugControllerEntry] { var entries: [DebugControllerEntry] = [] entries.append(.sendLogs(presentationData.theme)) entries.append(.accounts(presentationData.theme)) entries.append(.clearPaymentData(presentationData.theme)) + entries.append(.logToFile(presentationData.theme, loggingSettings.logToFile)) + entries.append(.logToConsole(presentationData.theme, loggingSettings.logToConsole)) + return entries } @@ -131,10 +167,17 @@ public func debugController(account: Account, accountManager: AccountManager) -> pushControllerImpl?(controller) }) - let signal = (account.applicationContext as! TelegramApplicationContext).presentationData - |> map { presentationData -> (ItemListControllerState, (ItemListNodeState, DebugControllerEntry.ItemGenerationArguments)) in - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text("Debug"), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: "Back")) - let listState = ItemListNodeState(entries: debugControllerEntries(presentationData: presentationData), style: .blocks) + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, account.postbox.preferencesView(keys: [PreferencesKeys.loggingSettings])) + |> map { presentationData, preferencesView -> (ItemListControllerState, (ItemListNodeState, DebugControllerEntry.ItemGenerationArguments)) in + let loggingSettings: LoggingSettings + if let value = preferencesView.values[PreferencesKeys.loggingSettings] as? LoggingSettings { + loggingSettings = value + } else { + loggingSettings = LoggingSettings.defaultSettings + } + + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text("Debug"), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) + let listState = ItemListNodeState(entries: debugControllerEntries(presentationData: presentationData, loggingSettings: loggingSettings), style: .blocks) return (controllerState, (listState, arguments)) } diff --git a/TelegramUI/DeclareEncodables.swift b/TelegramUI/DeclareEncodables.swift index ed7ea50aaa..7c268cf683 100644 --- a/TelegramUI/DeclareEncodables.swift +++ b/TelegramUI/DeclareEncodables.swift @@ -13,6 +13,9 @@ private var telegramUIDeclaredEncodables: Void = { declareEncodable(PresentationThemeSettings.self, f: { PresentationThemeSettings(decoder: $0) }) declareEncodable(TelegramWallpaper.self, f: { TelegramWallpaper(decoder: $0) }) declareEncodable(ApplicationSpecificBoolNotice.self, f: { ApplicationSpecificBoolNotice(decoder: $0) }) + declareEncodable(CallListSettings.self, f: { CallListSettings(decoder: $0) }) + declareEncodable(ExperimentalSettings.self, f: { ExperimentalSettings(decoder: $0) }) + declareEncodable(MusicPlaybackSettings.self, f: { MusicPlaybackSettings(decoder: $0) }) return }() diff --git a/TelegramUI/DefaultDarkAccentPresentationTheme.swift b/TelegramUI/DefaultDarkAccentPresentationTheme.swift new file mode 100644 index 0000000000..fb997ea8f8 --- /dev/null +++ b/TelegramUI/DefaultDarkAccentPresentationTheme.swift @@ -0,0 +1,304 @@ +import Foundation +import UIKit + +private let accentColor: UIColor = UIColor(rgb: 0x2EA6FF) +private let destructiveColor: UIColor = UIColor(rgb: 0xFF6767) +private let secretColor: UIColor = UIColor(rgb: 0x89DF9E) + +private let rootStatusBar = PresentationThemeRootNavigationStatusBar( + style: .white +) + +private let rootTabBar = PresentationThemeRootTabBar( + backgroundColor: UIColor(rgb: 0x213040), + separatorColor: UIColor(rgb: 0x131A23), + iconColor: UIColor(rgb: 0x7e929f), + selectedIconColor: accentColor, + textColor: UIColor(rgb: 0x7e929f), + selectedTextColor: accentColor, + badgeBackgroundColor: UIColor(rgb: 0xEF5B5B), + badgeStrokeColor: UIColor(rgb: 0xEF5B5B), + badgeTextColor: UIColor(rgb: 0xffffff) +) + +private let rootNavigationBar = PresentationThemeRootNavigationBar( + buttonColor: accentColor, + primaryTextColor: UIColor(rgb: 0xffffff), + secondaryTextColor: UIColor(rgb: 0x8B9197), + controlColor: accentColor, + accentTextColor: accentColor, + backgroundColor: UIColor(rgb: 0x213040), + separatorColor: UIColor(rgb: 0x131A23), + badgeBackgroundColor: UIColor(rgb: 0xEF5B5B), + badgeStrokeColor: UIColor(rgb: 0xEF5B5B), + badgeTextColor: UIColor(rgb: 0xffffff) +) + +private let activeNavigationSearchBar = PresentationThemeActiveNavigationSearchBar( + backgroundColor: UIColor(rgb: 0x213040), + accentColor: accentColor, + inputFillColor: UIColor(rgb: 0x182330), + inputTextColor: UIColor(rgb: 0xffffff), + inputPlaceholderTextColor: UIColor(rgb: 0x8B9197), + inputIconColor: UIColor(rgb: 0x8B9197), + inputClearButtonColor: UIColor(rgb: 0x8B9197), + separatorColor: UIColor(rgb: 0x18222C) +) + +private let rootController = PresentationThemeRootController( + statusBar: rootStatusBar, + tabBar: rootTabBar, + navigationBar: rootNavigationBar, + activeNavigationSearchBar: activeNavigationSearchBar +) + +private let switchColors = PresentationThemeSwitch( + frameColor: UIColor(rgb: 0xDBF5FF, alpha: 0.5), + handleColor: UIColor(rgb: 0x121212), + contentColor: accentColor +) + +private let list = PresentationThemeList( + blocksBackgroundColor: UIColor(rgb: 0x18222D), + plainBackgroundColor: UIColor(rgb: 0x18222D), + itemPrimaryTextColor: UIColor(rgb: 0xffffff), + itemSecondaryTextColor: UIColor(rgb: 0xDBF5FF, alpha: 0.5), + itemDisabledTextColor: UIColor(rgb: 0xDBF5FF, alpha: 0.5), //!!! + itemAccentColor: accentColor, + itemDestructiveColor: destructiveColor, + itemPlaceholderTextColor: UIColor(rgb: 0xDBF5FF, alpha: 0.5), //!!! + itemBlocksBackgroundColor: UIColor(rgb: 0x213040), + itemHighlightedBackgroundColor: UIColor(rgb: 0x10171F), + itemBlocksSeparatorColor: UIColor(rgb: 0x131A23), + itemPlainSeparatorColor: UIColor(rgb: 0x131A23), + disclosureArrowColor: UIColor(rgb: 0xDBF5FF, alpha: 0.5), //!!! + sectionHeaderTextColor: UIColor(rgb: 0x82888E), + freeTextColor: UIColor(rgb: 0x82888E), + freeTextErrorColor: destructiveColor, //!!! + freeTextSuccessColor: UIColor(rgb: 0x30cf30), //!!! + itemSwitchColors: switchColors, + itemDisclosureActions: PresentationThemeItemDisclosureActions( + neutral1: PresentationThemeItemDisclosureAction(fillColor: UIColor(rgb: 0x415A71), foregroundColor: .white), + neutral2: PresentationThemeItemDisclosureAction(fillColor: UIColor(rgb: 0x374F63), foregroundColor: .white), + destructive: PresentationThemeItemDisclosureAction(fillColor: destructiveColor, foregroundColor: .white) + ), + itemCheckColors: PresentationThemeCheck( + strokeColor: UIColor(rgb: 0xDBF5FF, alpha: 0.5), + fillColor: accentColor, + foregroundColor: .white + ), + controlSecondaryColor: UIColor(rgb: 0xDBF5FF, alpha: 0.5) +) + +private let chatList = PresentationThemeChatList( + backgroundColor: UIColor(rgb: 0x18222D), + itemSeparatorColor: UIColor(rgb: 0x131A23), + itemBackgroundColor: UIColor(rgb: 0x18222D), + pinnedItemBackgroundColor: UIColor(rgb: 0x213040), + itemHighlightedBackgroundColor: UIColor(rgb: 0x10171F), //!!! + titleColor: UIColor(rgb: 0xffffff), + secretTitleColor: secretColor, + dateTextColor: UIColor(rgb: 0xDBF5FF, alpha: 0.5), + authorNameColor: UIColor(rgb: 0xffffff), + messageTextColor: UIColor(rgb: 0xDBF5FF, alpha: 0.5), + messageDraftTextColor: UIColor(rgb: 0xdd4b39), //!!! + checkmarkColor: accentColor, + pendingIndicatorColor: UIColor(rgb: 0x8E8E93), + muteIconColor: UIColor(rgb: 0x8E8E93), + unreadBadgeActiveBackgroundColor: accentColor, + unreadBadgeActiveTextColor: UIColor(rgb: 0xffffff), + unreadBadgeInactiveBackgroundColor: UIColor(rgb: 0xDBF5FF, alpha: 0.4), + unreadBadgeInactiveTextColor: UIColor(rgb: 0x000000), + pinnedBadgeColor: UIColor(rgb: 0xDBF5FF, alpha: 0.5), + pinnedSearchBarColor: UIColor(rgb: 0x182330), + regularSearchBarColor: UIColor(rgb: 0x0F161E), + sectionHeaderFillColor: UIColor(rgb: 0x213040), + sectionHeaderTextColor: UIColor(rgb: 0xDBF5FF, alpha: 0.5), + searchBarKeyboardColor: .dark, + verifiedIconFillColor: accentColor, + verifiedIconForegroundColor: .white, + secretIconColor: secretColor +) + +private let bubble = PresentationThemeChatBubble( + incomingFillColor: UIColor(rgb: 0x213040), + incomingFillHighlightedColor: UIColor(rgb: 0x2D3A49), + incomingStrokeColor: UIColor(rgb: 0x213040), + outgoingFillColor: UIColor(rgb: 0x3D6A97), + outgoingFillHighlightedColor: UIColor(rgb: 0x5079A1), + outgoingStrokeColor: UIColor(rgb: 0x3D6A97), + freeformFillColor: UIColor(rgb: 0x213040), + freeformFillHighlightedColor: UIColor(rgb: 0x2A2A2A), //!!! + freeformStrokeColor: UIColor(rgb: 0x213040), + infoFillColor: UIColor(rgb: 0x213040), + infoStrokeColor: UIColor(rgb: 0x213040), + incomingPrimaryTextColor: UIColor(rgb: 0xffffff), + incomingSecondaryTextColor: UIColor(rgb: 0xDBF5FF, alpha: 0.5), + incomingLinkTextColor: accentColor, + incomingLinkHighlightColor: accentColor.withAlphaComponent(0.5), + outgoingPrimaryTextColor: UIColor(rgb: 0xffffff), + outgoingSecondaryTextColor: UIColor(rgb: 0xDBF5FF, alpha: 0.5), + outgoingLinkTextColor: accentColor, + outgoingLinkHighlightColor: accentColor.withAlphaComponent(0.5), + infoPrimaryTextColor: UIColor(rgb: 0xffffff), + infoLinkTextColor: accentColor, + incomingAccentTextColor: UIColor(rgb: 0xffffff), + outgoingAccentTextColor: UIColor(rgb: 0xffffff), + incomingAccentControlColor: UIColor(rgb: 0xffffff), + outgoingAccentControlColor: UIColor(rgb: 0xffffff), + incomingMediaActiveControlColor: UIColor(rgb: 0xffffff, alpha: 0.6), + outgoingMediaActiveControlColor: UIColor(rgb: 0xffffff, alpha: 0.6), + incomingMediaInactiveControlColor: UIColor(rgb: 0xffffff, alpha: 0.3), + outgoingMediaInactiveControlColor: UIColor(rgb: 0xffffff, alpha: 0.3), + outgoingCheckColor: UIColor(rgb: 0x64c0ff), + incomingPendingActivityColor: UIColor(rgb: 0xDBF5FF, alpha: 0.5), + outgoingPendingActivityColor: UIColor(rgb: 0xDBF5FF, alpha: 0.5), + mediaDateAndStatusFillColor: UIColor(white: 0.0, alpha: 0.5), + mediaDateAndStatusTextColor: .white, + incomingFileTitleColor: UIColor(rgb: 0xffffff), + outgoingFileTitleColor: UIColor(rgb: 0xffffff), + incomingFileDescriptionColor: UIColor(rgb: 0xDBF5FF, alpha: 0.5), + outgoingFileDescriptionColor: UIColor(rgb: 0xDBF5FF, alpha: 0.5), + incomingFileDurationColor: UIColor(rgb: 0xDBF5FF, alpha: 0.5), + outgoingFileDurationColor: UIColor(rgb: 0xDBF5FF, alpha: 0.5), + shareButtonFillColor: UIColor(rgb: 0x18222D, alpha: 0.5), + shareButtonStrokeColor: UIColor(rgb: 0x213040), + shareButtonForegroundColor: UIColor(rgb: 0xb2b2b2), //!!! + mediaOverlayControlBackgroundColor: UIColor(white: 0.0, alpha: 0.6), //!!! + mediaOverlayControlForegroundColor: UIColor(white: 1.0, alpha: 1.0), //!!! + actionButtonsIncomingFillColor: UIColor(rgb: 0x18222D, alpha: 0.5), + actionButtonsIncomingStrokeColor: UIColor(rgb: 0x213040), + actionButtonsIncomingTextColor: UIColor(rgb: 0xffffff), + actionButtonsOutgoingFillColor: UIColor(rgb: 0x18222D, alpha: 0.5), + actionButtonsOutgoingStrokeColor: UIColor(rgb: 0x213040), + actionButtonsOutgoingTextColor: UIColor(rgb: 0xffffff), + selectionControlBorderColor: .white, + selectionControlFillColor: accentColor, + selectionControlForegroundColor: .white +) + +private let serviceMessage = PresentationThemeServiceMessage( + serviceMessageFillColor: UIColor(rgb: 0x18222D, alpha: 1.0), + serviceMessagePrimaryTextColor: UIColor(rgb: 0xffffff), + serviceMessageLinkHighlightColor: UIColor(rgb: 0xffffff, alpha: 0.12), + unreadBarFillColor: UIColor(rgb: 0x18222D), + unreadBarStrokeColor: UIColor(rgb: 0x18222D), + unreadBarTextColor: UIColor(rgb: 0xffffff), + dateFillStaticColor: UIColor(rgb: 0x18222D, alpha: 1.0), + dateFillFloatingColor: UIColor(rgb: 0x18222D, alpha: 0.2), + dateTextColor: UIColor(rgb: 0xffffff) +) + +private let inputPanelMediaRecordingControl = PresentationThemeChatInputPanelMediaRecordingControl( + buttonColor: accentColor, + micLevelColor: accentColor.withAlphaComponent(0.2), + activeIconColor: .white, + panelControlFillColor: UIColor(rgb: 0x213040), + panelControlStrokeColor: UIColor(rgb: 0x213040), + panelControlContentPrimaryColor: UIColor(rgb: 0xDBF5FF, alpha: 0.5), //!!! + panelControlContentAccentColor: accentColor +) + +private let inputPanel = PresentationThemeChatInputPanel( + panelBackgroundColor: UIColor(rgb: 0x213040), + panelStrokeColor: UIColor(rgb: 0x131A23), + panelControlAccentColor: accentColor, + panelControlColor: UIColor(rgb: 0xDBF5FF, alpha: 0.5), + panelControlDisabledColor: UIColor(rgb: 0x90979F, alpha: 0.5), //!!! + panelControlDestructiveColor: destructiveColor, + inputBackgroundColor: UIColor(rgb: 0x131C26), + inputStrokeColor: UIColor(rgb: 0x131C26), + inputPlaceholderColor: UIColor(rgb: 0xDBF5FF, alpha: 0.4), + inputTextColor: UIColor(rgb: 0xffffff), + inputControlColor: UIColor(rgb: 0xDBF5FF, alpha: 0.4), + actionControlFillColor: accentColor, + actionControlForegroundColor: .white, + primaryTextColor: UIColor(rgb: 0xffffff), + secondaryTextColor: UIColor(rgb: 0xffffff, alpha: 0.5), + mediaRecordingDotColor: accentColor, + keyboardColor: .dark, + mediaRecordingControl: inputPanelMediaRecordingControl +) + +private let inputMediaPanel = PresentationThemeInputMediaPanel( + panelSerapatorColor: UIColor(rgb: 0x213040), + panelIconColor: UIColor(rgb: 0xDBF5FF, alpha: 0.5), + panelHighlightedIconBackgroundColor: UIColor(rgb: 0x131C26), //!!! + stickersBackgroundColor: UIColor(rgb: 0x131C26), + stickersSectionTextColor: UIColor(rgb: 0xDBF5FF, alpha: 0.5), + gifsBackgroundColor: UIColor(rgb: 0x131C26) +) + +private let inputButtonPanel = PresentationThemeInputButtonPanel( + panelSerapatorColor: UIColor(rgb: 0x213040), + panelBackgroundColor: UIColor(rgb: 0x161A20), + buttonFillColor: UIColor(rgb: 0x5B5F62), + buttonStrokeColor: UIColor(rgb: 0x0D1013), + buttonHighlightedFillColor: UIColor(rgb: 0x5B5F62, alpha: 0.7), + buttonHighlightedStrokeColor: UIColor(rgb: 0x0D1013), + buttonTextColor: UIColor(rgb: 0xffffff) +) + +private let historyNavigation = PresentationThemeChatHistoryNavigation( + fillColor: UIColor(rgb: 0x213040), + strokeColor: UIColor(rgb: 0x131A23), + foregroundColor: UIColor(rgb: 0xDBF5FF, alpha: 0.5), + badgeBackgroundColor: accentColor, + badgeStrokeColor: accentColor, + badgeTextColor: .white +) + +private let chat = PresentationThemeChat( + bubble: bubble, + serviceMessage: serviceMessage, + inputPanel: inputPanel, + inputMediaPanel: inputMediaPanel, + inputButtonPanel: inputButtonPanel, + historyNavigation: historyNavigation +) + +private let actionSheet = PresentationThemeActionSheet( + dimColor: UIColor(white: 0.0, alpha: 0.5), + backgroundType: .dark, + opaqueItemBackgroundColor: UIColor(rgb: 0x213040), + itemBackgroundColor: UIColor(rgb: 0x213040, alpha: 0.8), + opaqueItemHighlightedBackgroundColor: UIColor(rgb: 0x10171F), + itemHighlightedBackgroundColor: UIColor(rgb: 0x10171F, alpha: 0.2), //!!! + standardActionTextColor: accentColor, + opaqueItemSeparatorColor: UIColor(rgb: 0x18222D), + destructiveActionTextColor: destructiveColor, + disabledActionTextColor: UIColor(white: 1.0, alpha: 0.5), //!!! + primaryTextColor: .white, + secondaryTextColor: UIColor(white: 1.0, alpha: 0.5), //!!! + controlAccentColor: accentColor, + inputBackgroundColor: UIColor(rgb: 0x182330), //!!! + inputPlaceholderColor: UIColor(rgb: 0x8B9197), //!!! + inputTextColor: .white, + inputClearButtonColor: UIColor(rgb: 0x8B9197) +) + +private let inAppNotification = PresentationThemeInAppNotification( + fillColor: UIColor(rgb: 0x213040), + primaryTextColor: .white, + expandedNotification: PresentationThemeExpandedNotification( + backgroundType: .dark, + navigationBar: PresentationThemeExpandedNotificationNavigationBar( + backgroundColor: UIColor(rgb: 0x213040), + primaryTextColor: UIColor(rgb: 0xffffff), + controlColor: accentColor, + separatorColor: UIColor(rgb: 0x131A23) + ) + ) +) + +let defaultDarkAccentPresentationTheme = PresentationTheme( + name: .builtin(.nightAccent), + overallDarkAppearance: true, + allowsCustomWallpapers: false, + rootController: rootController, + list: list, + chatList: chatList, + chat: chat, + actionSheet: actionSheet, + inAppNotification: inAppNotification +) diff --git a/TelegramUI/DefaultDarkPresentationTheme.swift b/TelegramUI/DefaultDarkPresentationTheme.swift index 9e8ec6baf5..78107e2859 100644 --- a/TelegramUI/DefaultDarkPresentationTheme.swift +++ b/TelegramUI/DefaultDarkPresentationTheme.swift @@ -2,7 +2,8 @@ import Foundation import UIKit private let accentColor: UIColor = UIColor(rgb: 0xffffff) -private let destructiveColor: UIColor = .red +private let destructiveColor: UIColor = UIColor(rgb: 0xFF736B) +private let secretColor: UIColor = UIColor(rgb: 0x00B12C) private let rootStatusBar = PresentationThemeRootNavigationStatusBar( style: .white @@ -15,31 +16,33 @@ private let rootTabBar = PresentationThemeRootTabBar( selectedIconColor: accentColor, textColor: UIColor(rgb: 0x929292), selectedTextColor: accentColor, - badgeBackgroundColor: .red, //!!! - badgeTextColor: .white //!!! + badgeBackgroundColor: UIColor(rgb: 0xffffff), + badgeStrokeColor: UIColor(rgb: 0x1c1c1d), + badgeTextColor: UIColor(rgb: 0x000000) ) private let rootNavigationBar = PresentationThemeRootNavigationBar( buttonColor: accentColor, primaryTextColor: accentColor, - secondaryTextColor: UIColor(rgb: 0x5e5e5e), + secondaryTextColor: UIColor(rgb: 0xffffff, alpha: 0.5), controlColor: accentColor, accentTextColor: accentColor, backgroundColor: UIColor(rgb: 0x1c1c1d), separatorColor: UIColor(rgb: 0x000000), badgeBackgroundColor: UIColor(rgb: 0xffffff), + badgeStrokeColor: UIColor(rgb: 0x1c1c1d), badgeTextColor: UIColor(rgb: 0x1c1c1d) ) private let activeNavigationSearchBar = PresentationThemeActiveNavigationSearchBar( - backgroundColor: UIColor(rgb: 0x121212), + backgroundColor: UIColor(rgb: 0x1c1c1d), accentColor: accentColor, - inputFillColor: UIColor(rgb: 0x545454), + inputFillColor: UIColor(rgb: 0x272728), inputTextColor: accentColor, inputPlaceholderTextColor: UIColor(rgb: 0x5e5e5e), inputIconColor: UIColor(rgb: 0x5e5e5e), inputClearButtonColor: UIColor(rgb: 0x5e5e5e), - separatorColor: UIColor(rgb: 0x1a1a1a) + separatorColor: UIColor(rgb: 0x000000) ) private let rootController = PresentationThemeRootController( @@ -59,20 +62,32 @@ private let list = PresentationThemeList( blocksBackgroundColor: UIColor(rgb: 0x000000), plainBackgroundColor: UIColor(rgb: 0x000000), itemPrimaryTextColor: UIColor(rgb: 0xffffff), - itemSecondaryTextColor: UIColor(rgb: 0x545454), //!!! + itemSecondaryTextColor: UIColor(rgb: 0xffffff, alpha: 0.5), itemDisabledTextColor: UIColor(rgb: 0x4d4d4d), //!!! itemAccentColor: accentColor, itemDestructiveColor: destructiveColor, itemPlaceholderTextColor: UIColor(rgb: 0x4d4d4d), //!!! - itemBackgroundColor: UIColor(rgb: 0x1c1c1d), - itemHighlightedBackgroundColor: UIColor(rgb: 0x1b1b1b), //!!! - itemSeparatorColor: UIColor(rgb: 0x000000), + itemBlocksBackgroundColor: UIColor(rgb: 0x1c1c1d), + itemHighlightedBackgroundColor: UIColor(rgb: 0x191919), + itemBlocksSeparatorColor: UIColor(rgb: 0x000000), + itemPlainSeparatorColor: UIColor(rgb: 0x252525), disclosureArrowColor: UIColor(rgb: 0x545454), //!!! - sectionHeaderTextColor: UIColor(rgb: 0x8d8e93), + sectionHeaderTextColor: UIColor(rgb: 0xffffff), freeTextColor: UIColor(rgb: 0x8d8e93), freeTextErrorColor: UIColor(rgb: 0xcf3030), //!!! freeTextSuccessColor: UIColor(rgb: 0x30cf30), //!!! - itemSwitchColors: switchColors + itemSwitchColors: switchColors, + itemDisclosureActions: PresentationThemeItemDisclosureActions( + neutral1: PresentationThemeItemDisclosureAction(fillColor: UIColor(rgb: 0x666666), foregroundColor: .white), + neutral2: PresentationThemeItemDisclosureAction(fillColor: UIColor(rgb: 0x414141), foregroundColor: .white), + destructive: PresentationThemeItemDisclosureAction(fillColor: destructiveColor, foregroundColor: .white) + ), + itemCheckColors: PresentationThemeCheck( + strokeColor: UIColor(rgb: 0xffffff, alpha: 0.5), + fillColor: accentColor, + foregroundColor: UIColor(rgb: 0x000000) + ), + controlSecondaryColor: UIColor(rgb: 0xffffff, alpha: 0.5) ) private let chatList = PresentationThemeChatList( @@ -80,7 +95,7 @@ private let chatList = PresentationThemeChatList( itemSeparatorColor: UIColor(rgb: 0x252525), itemBackgroundColor: UIColor(rgb: 0x000000), pinnedItemBackgroundColor: UIColor(rgb: 0x1c1c1d), - itemHighlightedBackgroundColor: UIColor(rgb: 0x1b1b1b), //!!! + itemHighlightedBackgroundColor: UIColor(rgb: 0x191919), titleColor: UIColor(rgb: 0xffffff), secretTitleColor: UIColor(rgb: 0xb2b2b2), //!!! dateTextColor: UIColor(rgb: 0x8e8e93), @@ -97,52 +112,69 @@ private let chatList = PresentationThemeChatList( pinnedBadgeColor: UIColor(rgb: 0x767677), pinnedSearchBarColor: UIColor(rgb: 0x272728), regularSearchBarColor: UIColor(rgb: 0x272728), //!!! - sectionHeaderFillColor: UIColor(rgb: 0x000000), //!!! - sectionHeaderTextColor: UIColor(rgb: 0x545454), //!!! - searchBarKeyboardColor: .dark + sectionHeaderFillColor: UIColor(rgb: 0x1C1C1D), + sectionHeaderTextColor: UIColor(rgb: 0xffffff), + searchBarKeyboardColor: .dark, + verifiedIconFillColor: accentColor, + verifiedIconForegroundColor: .white, + secretIconColor: secretColor ) private let bubble = PresentationThemeChatBubble( incomingFillColor: UIColor(rgb: 0x1f1f1f), - incomingFillHighlightedColor: UIColor(rgb: 0x4b4b4b), //!!! - incomingStrokeColor: UIColor(rgb: 0x000000), //!!! + incomingFillHighlightedColor: UIColor(rgb: 0x2A2A2A), + incomingStrokeColor: UIColor(rgb: 0x1f1f1f), outgoingFillColor: UIColor(rgb: 0x313131), - outgoingFillHighlightedColor: UIColor(rgb: 0x4b4b4b), //!!! - outgoingStrokeColor: UIColor(rgb: 0x000000), + outgoingFillHighlightedColor: UIColor(rgb: 0x464646), + outgoingStrokeColor: UIColor(rgb: 0x313131), freeformFillColor: UIColor(rgb: 0x1f1f1f), - freeformFillHighlightedColor: UIColor(rgb: 0x4b4b4b), //!!! - freeformStrokeColor: UIColor(rgb: 0x000000), + freeformFillHighlightedColor: UIColor(rgb: 0x2A2A2A), + freeformStrokeColor: UIColor(rgb: 0x1f1f1f), infoFillColor: UIColor(rgb: 0x1f1f1f), infoStrokeColor: UIColor(rgb: 0x000000), incomingPrimaryTextColor: UIColor(rgb: 0xffffff), - incomingSecondaryTextColor: UIColor(rgb: 0xacacac), //!!! - incomingLinkTextColor: accentColor, //!!! - incomingLinkHighlightColor: accentColor.withAlphaComponent(0.5), //!!! - outgoingPrimaryTextColor: UIColor(rgb: 0xffffff), //!!! - outgoingSecondaryTextColor: UIColor(rgb: 0xacacac), //!!! - outgoingLinkTextColor: accentColor, //!!! - outgoingLinkHighlightColor: accentColor.withAlphaComponent(0.5), //!!! + incomingSecondaryTextColor: UIColor(rgb: 0xffffff, alpha: 0.5), + incomingLinkTextColor: accentColor, + incomingLinkHighlightColor: accentColor.withAlphaComponent(0.5), + outgoingPrimaryTextColor: UIColor(rgb: 0xffffff), + outgoingSecondaryTextColor: UIColor(rgb: 0xffffff, alpha: 0.5), + outgoingLinkTextColor: accentColor, + outgoingLinkHighlightColor: accentColor.withAlphaComponent(0.5), infoPrimaryTextColor: UIColor(rgb: 0xffffff), - infoLinkTextColor: accentColor, //!!! - incomingAccentColor: UIColor(rgb: 0xacacac), //!!! - outgoingAccentColor: UIColor(rgb: 0xacacac), - outgoingCheckColor: UIColor(rgb: 0xacacac), - incomingPendingActivityColor: UIColor(rgb: 0xacacac), //!!! - outgoingPendingActivityColor: UIColor(rgb: 0xacacac), - mediaDateAndStatusFillColor: UIColor(white: 0.0, alpha: 0.5), //!!! + infoLinkTextColor: accentColor, + incomingAccentTextColor: UIColor(rgb: 0xffffff), + outgoingAccentTextColor: UIColor(rgb: 0xffffff), + incomingAccentControlColor: UIColor(rgb: 0xffffff), + outgoingAccentControlColor: UIColor(rgb: 0xffffff), + incomingMediaActiveControlColor: UIColor(rgb: 0xffffff, alpha: 0.6), + outgoingMediaActiveControlColor: UIColor(rgb: 0xffffff, alpha: 0.6), + incomingMediaInactiveControlColor: UIColor(rgb: 0xffffff, alpha: 0.3), + outgoingMediaInactiveControlColor: UIColor(rgb: 0xffffff, alpha: 0.3), + outgoingCheckColor: UIColor(rgb: 0xffffff, alpha: 0.5), + incomingPendingActivityColor: UIColor(rgb: 0xffffff, alpha: 0.5), + outgoingPendingActivityColor: UIColor(rgb: 0xffffff, alpha: 0.5), + mediaDateAndStatusFillColor: UIColor(white: 0.0, alpha: 0.5), mediaDateAndStatusTextColor: .white, incomingFileTitleColor: UIColor(rgb: 0xffffff), outgoingFileTitleColor: UIColor(rgb: 0xffffff), - incomingFileDescriptionColor: UIColor(rgb: 0xacacac), //!!! - outgoingFileDescriptionColor: UIColor(rgb: 0xacacac), - incomingFileDurationColor: UIColor(rgb: 0xacacac), - outgoingFileDurationColor: UIColor(rgb: 0xacacac), - shareButtonFillColor: UIColor(rgb: 0xffffff, alpha: 0.2), //!!! + incomingFileDescriptionColor: UIColor(rgb: 0xffffff, alpha: 0.5), + outgoingFileDescriptionColor: UIColor(rgb: 0xffffff, alpha: 0.5), + incomingFileDurationColor: UIColor(rgb: 0xffffff, alpha: 0.5), + outgoingFileDurationColor: UIColor(rgb: 0xffffff, alpha: 0.5), + shareButtonFillColor: UIColor(rgb: 0x000000, alpha: 0.5), + shareButtonStrokeColor: UIColor(rgb: 0x1f1f1f), shareButtonForegroundColor: UIColor(rgb: 0xb2b2b2), //!!! mediaOverlayControlBackgroundColor: UIColor(white: 0.0, alpha: 0.6), //!!! - mediaOverlayControlForegroundColor: UIColor(white: 1.0, alpha: 0.6), //!!! - actionButtonsFillColor: UIColor(rgb: 0x1b1b1b), //!!! - actionButtonsTextColor: UIColor(rgb: 0xb2b2b2) //!!! + mediaOverlayControlForegroundColor: UIColor(white: 1.0, alpha: 1.0), //!!! + actionButtonsIncomingFillColor: UIColor(rgb: 0x000000, alpha: 0.5), + actionButtonsIncomingStrokeColor: UIColor(rgb: 0x1f1f1f), + actionButtonsIncomingTextColor: UIColor(rgb: 0xffffff), + actionButtonsOutgoingFillColor: UIColor(rgb: 0x000000, alpha: 0.5), + actionButtonsOutgoingStrokeColor: UIColor(rgb: 0x1f1f1f), + actionButtonsOutgoingTextColor: UIColor(rgb: 0xffffff), + selectionControlBorderColor: .white, + selectionControlFillColor: accentColor, + selectionControlForegroundColor: .black ) private let serviceMessage = PresentationThemeServiceMessage( @@ -178,8 +210,11 @@ private let inputPanel = PresentationThemeChatInputPanel( inputStrokeColor: UIColor(rgb: 0x060606), inputPlaceholderColor: UIColor(rgb: 0x7b7b7b), inputTextColor: UIColor(rgb: 0xffffff), - inputControlColor: UIColor(rgb: 0xb2b2b2, alpha: 0.5), //!!! + inputControlColor: UIColor(rgb: 0x7b7b7b), + actionControlFillColor: accentColor, + actionControlForegroundColor: .black, primaryTextColor: UIColor(rgb: 0xffffff), + secondaryTextColor: UIColor(rgb: 0xffffff, alpha: 0.5), mediaRecordingDotColor: .white, keyboardColor: .dark, mediaRecordingControl: inputPanelMediaRecordingControl @@ -187,28 +222,29 @@ private let inputPanel = PresentationThemeChatInputPanel( private let inputMediaPanel = PresentationThemeInputMediaPanel( panelSerapatorColor: UIColor(rgb: 0x000000), - panelIconColor: UIColor(rgb: 0x545454), - panelHighlightedIconBackgroundColor: UIColor(rgb: 0x4b4b4b), - stickersBackgroundColor: UIColor(rgb: 0x1b1b1b), - stickersSectionTextColor: UIColor(rgb: 0xb2b2b2), - gifsBackgroundColor: UIColor(rgb: 0x1b1b1b) + panelIconColor: UIColor(rgb: 0x808080), + panelHighlightedIconBackgroundColor: UIColor(rgb: 0x000000), //!!! + stickersBackgroundColor: UIColor(rgb: 0x000000), + stickersSectionTextColor: UIColor(rgb: 0x7b7b7b), + gifsBackgroundColor: UIColor(rgb: 0x000000) ) private let inputButtonPanel = PresentationThemeInputButtonPanel( panelSerapatorColor: UIColor(rgb: 0x000000), - panelBackgroundColor: UIColor(rgb: 0x1b1b1b), - buttonFillColor: UIColor(rgb: 0x1b1b1b), - buttonStrokeColor: UIColor(rgb: 0x000000), - buttonHighlightedFillColor: UIColor(rgb: 0x4b4b4b), - buttonHighlightedStrokeColor: UIColor(rgb: 0x000000), - buttonTextColor: UIColor(rgb: 0xb2b2b2) + panelBackgroundColor: UIColor(rgb: 0x141414), + buttonFillColor: UIColor(rgb: 0x5A5A5A), + buttonStrokeColor: UIColor(rgb: 0x0C0C0C), + buttonHighlightedFillColor: UIColor(rgb: 0x5A5A5A, alpha: 0.7), + buttonHighlightedStrokeColor: UIColor(rgb: 0x0C0C0C), + buttonTextColor: UIColor(rgb: 0xffffff) ) private let historyNavigation = PresentationThemeChatHistoryNavigation( - fillColor: .white, + fillColor: UIColor(rgb: 0x1C1C1D), strokeColor: UIColor(rgb: 0x000000), - foregroundColor: UIColor(rgb: 0x4b4b4b), + foregroundColor: UIColor(rgb: 0xffffff), badgeBackgroundColor: accentColor, + badgeStrokeColor: .black, badgeTextColor: .black ) @@ -224,20 +260,45 @@ private let chat = PresentationThemeChat( private let actionSheet = PresentationThemeActionSheet( dimColor: UIColor(white: 0.0, alpha: 0.5), backgroundType: .dark, - itemBackgroundColor: UIColor(rgb: 0x1c1c1d, alpha: 0.8), //!!! + opaqueItemBackgroundColor: UIColor(rgb: 0x1c1c1d), + itemBackgroundColor: UIColor(rgb: 0x1c1c1d, alpha: 0.8), + opaqueItemHighlightedBackgroundColor: UIColor(white: 0.0, alpha: 1.0), itemHighlightedBackgroundColor: UIColor(rgb: 0x000000, alpha: 0.5), //!!! standardActionTextColor: accentColor, + opaqueItemSeparatorColor: UIColor(white: 0.0, alpha: 1.0), destructiveActionTextColor: destructiveColor, disabledActionTextColor: UIColor(rgb: 0x4d4d4d), //!!! primaryTextColor: .white, secondaryTextColor: UIColor(rgb: 0x5e5e5e), //!!! - controlAccentColor: accentColor + controlAccentColor: accentColor, + inputBackgroundColor: UIColor(rgb: 0x545454), //!!! + inputPlaceholderColor: UIColor(rgb: 0xaaaaaa), //!!! + inputTextColor: .white, + inputClearButtonColor: UIColor(rgb: 0xaaaaaa) +) + +private let inAppNotification = PresentationThemeInAppNotification( + fillColor: UIColor(rgb: 0x1c1c1d), + primaryTextColor: .white, + expandedNotification: PresentationThemeExpandedNotification( + backgroundType: .dark, + navigationBar: PresentationThemeExpandedNotificationNavigationBar( + backgroundColor: UIColor(rgb: 0x1c1c1d), + primaryTextColor: accentColor, + controlColor: accentColor, + separatorColor: UIColor(rgb: 0x000000) + ) + ) ) let defaultDarkPresentationTheme = PresentationTheme( + name: .builtin(.nightGrayscale), + overallDarkAppearance: true, + allowsCustomWallpapers: false, rootController: rootController, list: list, chatList: chatList, chat: chat, - actionSheet: actionSheet + actionSheet: actionSheet, + inAppNotification: inAppNotification ) diff --git a/TelegramUI/DefaultPresentationStrings.swift b/TelegramUI/DefaultPresentationStrings.swift index 7b16d84eba..154187f342 100644 --- a/TelegramUI/DefaultPresentationStrings.swift +++ b/TelegramUI/DefaultPresentationStrings.swift @@ -1,3 +1,3 @@ import Foundation -let defaultPresentationStrings = PresentationStrings(languageCode: "en", dict: NSDictionary(contentsOf: URL(fileURLWithPath: Bundle.main.path(forResource: "Localizable", ofType: "strings", inDirectory: nil, forLocalization: "en")!)) as! [String : String]) +public let defaultPresentationStrings = PresentationStrings(languageCode: "en", dict: NSDictionary(contentsOf: URL(fileURLWithPath: Bundle.main.path(forResource: "Localizable", ofType: "strings", inDirectory: nil, forLocalization: "en")!)) as! [String : String]) diff --git a/TelegramUI/DefaultPresentationTheme.swift b/TelegramUI/DefaultPresentationTheme.swift index 4b41deea58..02d1efa518 100644 --- a/TelegramUI/DefaultPresentationTheme.swift +++ b/TelegramUI/DefaultPresentationTheme.swift @@ -3,6 +3,7 @@ import UIKit private let accentColor: UIColor = UIColor(rgb: 0x007ee5) private let destructiveColor: UIColor = UIColor(rgb: 0xff3b30) +private let secretColor: UIColor = UIColor(rgb: 0x00B12C) private let rootStatusBar = PresentationThemeRootNavigationStatusBar( style: .black @@ -11,11 +12,12 @@ private let rootStatusBar = PresentationThemeRootNavigationStatusBar( private let rootTabBar = PresentationThemeRootTabBar( backgroundColor: UIColor(rgb: 0xf7f7f7), separatorColor: UIColor(rgb: 0xa3a3a3), - iconColor: UIColor(rgb: 0x929292), + iconColor: UIColor(rgb: 0xA1A1A1), selectedIconColor: accentColor, - textColor: UIColor(rgb: 0x929292), + textColor: UIColor(rgb: 0xA1A1A1), selectedTextColor: accentColor, badgeBackgroundColor: UIColor(rgb: 0xff3b30), + badgeStrokeColor: UIColor(rgb: 0xff3b30), badgeTextColor: .white ) @@ -28,6 +30,7 @@ private let rootNavigationBar = PresentationThemeRootNavigationBar( backgroundColor: UIColor(red: 0.968626451, green: 0.968626451, blue: 0.968626451, alpha: 1.0), separatorColor: UIColor(red: 0.6953125, green: 0.6953125, blue: 0.6953125, alpha: 1.0), badgeBackgroundColor: UIColor(rgb: 0xff3b30), + badgeStrokeColor: UIColor(rgb: 0xff3b30), badgeTextColor: .white ) @@ -64,15 +67,27 @@ private let list = PresentationThemeList( itemAccentColor: accentColor, itemDestructiveColor: destructiveColor, itemPlaceholderTextColor: UIColor(rgb: 0xc8c8ce), - itemBackgroundColor: .white, + itemBlocksBackgroundColor: .white, itemHighlightedBackgroundColor: UIColor(rgb: 0xd9d9d9), - itemSeparatorColor: UIColor(rgb: 0xc8c7cc), + itemBlocksSeparatorColor: UIColor(rgb: 0xc8c7cc), + itemPlainSeparatorColor: UIColor(rgb: 0xc8c7cc), disclosureArrowColor: UIColor(rgb: 0xbab9be), sectionHeaderTextColor: UIColor(rgb: 0x6d6d72), freeTextColor: UIColor(rgb: 0x6d6d72), freeTextErrorColor: UIColor(rgb: 0xcf3030), freeTextSuccessColor: UIColor(rgb: 0x26972c), - itemSwitchColors: switchColors + itemSwitchColors: switchColors, + itemDisclosureActions: PresentationThemeItemDisclosureActions( + neutral1: PresentationThemeItemDisclosureAction(fillColor: UIColor(rgb: 0xbcbcc3), foregroundColor: .white), + neutral2: PresentationThemeItemDisclosureAction(fillColor: UIColor(rgb: 0xaaaab3), foregroundColor: .white), + destructive: PresentationThemeItemDisclosureAction(fillColor: UIColor(rgb: 0xff3824), foregroundColor: .white) + ), + itemCheckColors: PresentationThemeCheck( + strokeColor: UIColor(rgb: 0xC7C7CC), + fillColor: accentColor, + foregroundColor: .white + ), + controlSecondaryColor: UIColor(rgb: 0xdedede) ) private let chatList = PresentationThemeChatList( @@ -82,7 +97,7 @@ private let chatList = PresentationThemeChatList( pinnedItemBackgroundColor: UIColor(rgb: 0xf7f7f7), itemHighlightedBackgroundColor: UIColor(rgb: 0xd9d9d9), titleColor: .black, - secretTitleColor: UIColor(rgb: 0x00a629), + secretTitleColor: secretColor, dateTextColor: UIColor(rgb: 0x8e8e93), authorNameColor: .black, messageTextColor: UIColor(rgb: 0x8e8e93), @@ -99,7 +114,40 @@ private let chatList = PresentationThemeChatList( regularSearchBarColor: UIColor(rgb: 0xe9e9e9), sectionHeaderFillColor: UIColor(rgb: 0xf7f7f7), sectionHeaderTextColor: UIColor(rgb: 0x8e8e93), - searchBarKeyboardColor: .light + searchBarKeyboardColor: .light, + verifiedIconFillColor: accentColor, + verifiedIconForegroundColor: .white, + secretIconColor: secretColor +) + +private let chatListDay = PresentationThemeChatList( + backgroundColor: .white, + itemSeparatorColor: UIColor(rgb: 0xc8c7cc), + itemBackgroundColor: .white, + pinnedItemBackgroundColor: UIColor(rgb: 0xf7f7f7), + itemHighlightedBackgroundColor: UIColor(rgb: 0xd9d9d9), + titleColor: .black, + secretTitleColor: secretColor, + dateTextColor: UIColor(rgb: 0x8e8e93), + authorNameColor: .black, + messageTextColor: UIColor(rgb: 0x8e8e93), + messageDraftTextColor: UIColor(rgb: 0xdd4b39), + checkmarkColor: accentColor, + pendingIndicatorColor: UIColor(rgb: 0x8e8e93), + muteIconColor: UIColor(rgb: 0xa7a7ad), + unreadBadgeActiveBackgroundColor: accentColor, + unreadBadgeActiveTextColor: .white, + unreadBadgeInactiveBackgroundColor: UIColor(rgb: 0xb6b6bb), + unreadBadgeInactiveTextColor: .white, + pinnedBadgeColor: UIColor(rgb: 0x939399), + pinnedSearchBarColor: UIColor(rgb: 0xe5e5e5), + regularSearchBarColor: UIColor(rgb: 0xe9e9e9), + sectionHeaderFillColor: UIColor(rgb: 0xf7f7f7), + sectionHeaderTextColor: UIColor(rgb: 0x8e8e93), + searchBarKeyboardColor: .light, + verifiedIconFillColor: accentColor, + verifiedIconForegroundColor: .white, + secretIconColor: secretColor ) private let bubble = PresentationThemeChatBubble( @@ -124,8 +172,14 @@ private let bubble = PresentationThemeChatBubble( outgoingLinkHighlightColor: accentColor.withAlphaComponent(0.3), infoPrimaryTextColor: UIColor(rgb: 0x000000), infoLinkTextColor: UIColor(rgb: 0x004bad), - incomingAccentColor: UIColor(rgb: 0x3ca7fe), - outgoingAccentColor: UIColor(rgb: 0x00a700), + incomingAccentTextColor: UIColor(rgb: 0x3ca7fe), + outgoingAccentTextColor: UIColor(rgb: 0x00a700), + incomingAccentControlColor: UIColor(rgb: 0x3ca7fe), + outgoingAccentControlColor: UIColor(rgb: 0x3FC33B), + incomingMediaActiveControlColor: UIColor(rgb: 0x3ca7fe), + outgoingMediaActiveControlColor: UIColor(rgb: 0x3FC33B), + incomingMediaInactiveControlColor: UIColor(rgb: 0xcacaca), + outgoingMediaInactiveControlColor: UIColor(rgb: 0x93D987), outgoingCheckColor: UIColor(rgb: 0x19C700), incomingPendingActivityColor: UIColor(rgb: 0x525252, alpha: 0.6), outgoingPendingActivityColor: UIColor(rgb: 0x42b649), @@ -138,11 +192,76 @@ private let bubble = PresentationThemeChatBubble( incomingFileDurationColor: UIColor(rgb: 0x525252, alpha: 0.6), outgoingFileDurationColor: UIColor(rgb: 0x008c09, alpha: 0.8), shareButtonFillColor: UIColor(rgb: 0x748391, alpha: 0.45), + shareButtonStrokeColor: .clear, shareButtonForegroundColor: .white, mediaOverlayControlBackgroundColor: UIColor(white: 0.0, alpha: 0.6), mediaOverlayControlForegroundColor: UIColor(white: 1.0, alpha: 1.0), - actionButtonsFillColor: UIColor(rgb: 0x596E89), - actionButtonsTextColor: .white + actionButtonsIncomingFillColor: UIColor(rgb: 0x596E89, alpha: 0.35), + actionButtonsIncomingStrokeColor: .clear, + actionButtonsIncomingTextColor: .white, + actionButtonsOutgoingFillColor: UIColor(rgb: 0x596E89, alpha: 0.35), + actionButtonsOutgoingStrokeColor: .clear, + actionButtonsOutgoingTextColor: .white, + selectionControlBorderColor: UIColor(rgb: 0xC7C7CC), + selectionControlFillColor: accentColor, + selectionControlForegroundColor: .white +) + +private let bubbleDay = PresentationThemeChatBubble( + incomingFillColor: UIColor(rgb: 0xF1F1F4), + incomingFillHighlightedColor: UIColor(rgb: 0xDADADE), + incomingStrokeColor: UIColor(rgb: 0xF1F1F4), + outgoingFillColor: UIColor(rgb: 0x3996ee), + outgoingFillHighlightedColor: UIColor(rgb: 0x3387D6), + outgoingStrokeColor: UIColor(rgb: 0x3996ee, alpha: 1.0), + freeformFillColor: UIColor(rgb: 0xE5E5EA), + freeformFillHighlightedColor: UIColor(rgb: 0xDADADE), + freeformStrokeColor: UIColor(rgb: 0xE5E5EA), + infoFillColor: UIColor(rgb: 0xE5E5EA), + infoStrokeColor: UIColor(rgb: 0xE5E5EA, alpha: 1.0), + incomingPrimaryTextColor: UIColor(rgb: 0x000000), + incomingSecondaryTextColor: UIColor(rgb: 0x525252, alpha: 0.6), + incomingLinkTextColor: UIColor(rgb: 0x004bad), + incomingLinkHighlightColor: accentColor.withAlphaComponent(0.3), + outgoingPrimaryTextColor: UIColor(rgb: 0xffffff), + outgoingSecondaryTextColor: UIColor(rgb: 0xAFD5F8, alpha: 1.0), + outgoingLinkTextColor: UIColor(rgb: 0xffffff), + outgoingLinkHighlightColor: UIColor(rgb: 0xffffff, alpha: 0.3), + infoPrimaryTextColor: UIColor(rgb: 0x000000), + infoLinkTextColor: UIColor(rgb: 0x004bad), + incomingAccentTextColor: accentColor, + outgoingAccentTextColor: UIColor(white: 1.0, alpha: 0.7), + incomingAccentControlColor: accentColor, + outgoingAccentControlColor: UIColor(white: 1.0, alpha: 0.7), + incomingMediaActiveControlColor: accentColor, + outgoingMediaActiveControlColor: UIColor(white: 1.0, alpha: 1.0), + incomingMediaInactiveControlColor: UIColor(rgb: 0xcacaca), + outgoingMediaInactiveControlColor: UIColor(white: 1.0, alpha: 0.6), + outgoingCheckColor: UIColor(rgb: 0xAFD5F8), + incomingPendingActivityColor: UIColor(rgb: 0x525252, alpha: 0.6), + outgoingPendingActivityColor: UIColor(white: 1.0, alpha: 0.7), + mediaDateAndStatusFillColor: UIColor(white: 0.0, alpha: 0.5), + mediaDateAndStatusTextColor: .white, + incomingFileTitleColor: UIColor(rgb: 0x0b8bed), + outgoingFileTitleColor: UIColor(rgb: 0xffffff), + incomingFileDescriptionColor: UIColor(rgb: 0x999999), + outgoingFileDescriptionColor: UIColor(white: 1.0, alpha: 0.7), + incomingFileDurationColor: UIColor(rgb: 0x525252, alpha: 0.6), + outgoingFileDurationColor: UIColor(white: 1.0, alpha: 0.7), + shareButtonFillColor: UIColor(rgb: 0xffffff, alpha: 0.5), + shareButtonStrokeColor: UIColor(rgb: 0xE5E5EA), + shareButtonForegroundColor: accentColor, + mediaOverlayControlBackgroundColor: UIColor(white: 0.0, alpha: 0.6), + mediaOverlayControlForegroundColor: UIColor(white: 1.0, alpha: 1.0), + actionButtonsIncomingFillColor: UIColor(rgb: 0xffffff, alpha: 0.5), + actionButtonsIncomingStrokeColor: UIColor(rgb: 0x3996ee), + actionButtonsIncomingTextColor: UIColor(rgb: 0x3996ee), + actionButtonsOutgoingFillColor: UIColor(rgb: 0xffffff, alpha: 0.5), + actionButtonsOutgoingStrokeColor: UIColor(rgb: 0x3996ee), + actionButtonsOutgoingTextColor: UIColor(rgb: 0x3996ee), + selectionControlBorderColor: UIColor(rgb: 0xC7C7CC), + selectionControlFillColor: accentColor, + selectionControlForegroundColor: .white ) private let serviceMessage = PresentationThemeServiceMessage( @@ -157,6 +276,18 @@ private let serviceMessage = PresentationThemeServiceMessage( dateTextColor: .white ) +private let serviceMessageDay = PresentationThemeServiceMessage( + serviceMessageFillColor: UIColor(rgb: 0xffffff, alpha: 0.45), + serviceMessagePrimaryTextColor: UIColor(rgb: 0x8D8E93), + serviceMessageLinkHighlightColor: UIColor(rgb: 0x748391, alpha: 0.25), + unreadBarFillColor: UIColor(white: 1.0, alpha: 1.0), + unreadBarStrokeColor: UIColor(white: 1.0, alpha: 1.0), + unreadBarTextColor: UIColor(rgb: 0x8D8E93), + dateFillStaticColor: UIColor(rgb: 0xffffff, alpha: 0.45), + dateFillFloatingColor: UIColor(rgb: 0xffffff, alpha: 0.8), + dateTextColor: UIColor(rgb: 0x8D8E93) +) + private let inputPanelMediaRecordingControl = PresentationThemeChatInputPanelMediaRecordingControl( buttonColor: accentColor, micLevelColor: accentColor.withAlphaComponent(0.2), @@ -179,7 +310,10 @@ private let inputPanel = PresentationThemeChatInputPanel( inputPlaceholderColor: UIColor(rgb: 0xbebec0), inputTextColor: .black, inputControlColor: UIColor(rgb: 0x9099A2, alpha: 0.6), + actionControlFillColor: accentColor, + actionControlForegroundColor: .white, primaryTextColor: .black, + secondaryTextColor: UIColor(rgb: 0x5e5e5e), mediaRecordingDotColor: UIColor(rgb: 0xed2521), keyboardColor: .light, mediaRecordingControl: inputPanelMediaRecordingControl @@ -209,6 +343,7 @@ private let historyNavigation = PresentationThemeChatHistoryNavigation( strokeColor: UIColor(rgb: 0x000000, alpha: 0.15), foregroundColor: UIColor(rgb: 0x88888D), badgeBackgroundColor: accentColor, + badgeStrokeColor: accentColor, badgeTextColor: .white ) @@ -221,23 +356,69 @@ private let chat = PresentationThemeChat( historyNavigation: historyNavigation ) +private let chatDay = PresentationThemeChat( + bubble: bubbleDay, + serviceMessage: serviceMessageDay, + inputPanel: inputPanel, + inputMediaPanel: inputMediaPanel, + inputButtonPanel: inputButtonPanel, + historyNavigation: historyNavigation +) + private let actionSheet = PresentationThemeActionSheet( dimColor: UIColor(white: 0.0, alpha: 0.4), backgroundType: .light, + opaqueItemBackgroundColor: .white, itemBackgroundColor: UIColor(white: 1.0, alpha: 0.8), + opaqueItemHighlightedBackgroundColor: UIColor(white: 0.9, alpha: 1.0), itemHighlightedBackgroundColor: UIColor(white: 0.9, alpha: 0.7), standardActionTextColor: accentColor, + opaqueItemSeparatorColor: UIColor(white: 0.9, alpha: 1.0), destructiveActionTextColor: destructiveColor, disabledActionTextColor: UIColor(rgb: 0x4d4d4d), primaryTextColor: .black, secondaryTextColor: UIColor(rgb: 0x5e5e5e), - controlAccentColor: accentColor + controlAccentColor: accentColor, + inputBackgroundColor: UIColor(rgb: 0xe9e9e9), + inputPlaceholderColor: UIColor(rgb: 0x818086), + inputTextColor: .black, + inputClearButtonColor: UIColor(rgb: 0x7b7b81) ) -let defaultPresentationTheme = PresentationTheme( +private let inAppNotification = PresentationThemeInAppNotification( + fillColor: .white, + primaryTextColor: .black, + expandedNotification: PresentationThemeExpandedNotification( + backgroundType: .light, + navigationBar: PresentationThemeExpandedNotificationNavigationBar( + backgroundColor: .white, + primaryTextColor: .black, + controlColor: UIColor(rgb: 0x7e8791), + separatorColor: UIColor(red: 0.6953125, green: 0.6953125, blue: 0.6953125, alpha: 1.0) + ) + ) +) + +public let defaultPresentationTheme = PresentationTheme( + name: .builtin(.dayClassic), + overallDarkAppearance: false, + allowsCustomWallpapers: true, rootController: rootController, list: list, chatList: chatList, chat: chat, - actionSheet: actionSheet + actionSheet: actionSheet, + inAppNotification: inAppNotification +) + +let defaultDayPresentationTheme = PresentationTheme( + name: .builtin(.day), + overallDarkAppearance: false, + allowsCustomWallpapers: false, + rootController: rootController, + list: list, + chatList: chatListDay, + chat: chatDay, + actionSheet: actionSheet, + inAppNotification: inAppNotification ) diff --git a/TelegramUI/DeleteChatInputPanelNode.swift b/TelegramUI/DeleteChatInputPanelNode.swift index 05b9962724..a61413bef7 100644 --- a/TelegramUI/DeleteChatInputPanelNode.swift +++ b/TelegramUI/DeleteChatInputPanelNode.swift @@ -33,18 +33,18 @@ final class DeleteChatInputPanelNode: ChatInputPanelNode { self.interfaceInteraction?.deleteChat() } - override func updateLayout(width: CGFloat, maxHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { if self.presentationInterfaceState != interfaceState { self.presentationInterfaceState = interfaceState self.button.setAttributedTitle(NSAttributedString(string: interfaceState.strings.GroupInfo_DeleteAndExit, font: Font.regular(17.0), textColor: interfaceState.theme.chat.inputPanel.panelControlDestructiveColor), for: []) } - let buttonSize = self.button.measure(CGSize(width: width - 10.0, height: 100.0)) + let buttonSize = self.button.measure(CGSize(width: width - leftInset - rightInset - 10.0, height: 100.0)) let panelHeight: CGFloat = 47.0 - self.button.frame = CGRect(origin: CGPoint(x: floor((width - buttonSize.width) / 2.0), y: floor((panelHeight - buttonSize.height) / 2.0)), size: buttonSize) + self.button.frame = CGRect(origin: CGPoint(x: leftInset + floor((width - leftInset - rightInset - buttonSize.width) / 2.0), y: floor((panelHeight - buttonSize.height) / 2.0)), size: buttonSize) return panelHeight } diff --git a/TelegramUI/DeviceProximityManager.h b/TelegramUI/DeviceProximityManager.h new file mode 100644 index 0000000000..b9b9f23535 --- /dev/null +++ b/TelegramUI/DeviceProximityManager.h @@ -0,0 +1,14 @@ +#import + +@interface DeviceProximityManager : NSObject + ++ (DeviceProximityManager * _Nonnull)shared; + +- (bool)currentValue; + +- (void)setGloballyEnabled:(bool)value; + +- (NSInteger)add:(void (^ _Nonnull)(bool))f; +- (void)remove:(NSInteger)index; + +@end diff --git a/TelegramUI/DeviceProximityManager.m b/TelegramUI/DeviceProximityManager.m new file mode 100644 index 0000000000..e5ab4fc277 --- /dev/null +++ b/TelegramUI/DeviceProximityManager.m @@ -0,0 +1,107 @@ +#import "DeviceProximityManager.h" + +#import + +#import +#import + +@interface DeviceProximityManager () { + SBag *_subscribers; + bool _proximityState; + bool _globallyEnabled; +} + +@end + +@implementation DeviceProximityManager + ++ (DeviceProximityManager * _Nonnull)shared { + static DeviceProximityManager *instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [[DeviceProximityManager alloc] init]; + }); + return instance; +} + +- (bool)currentValue { + return _proximityState; +} + +- (instancetype)init { + self = [super init]; + if (self != nil) { + _subscribers = [[SBag alloc] init]; + + __weak DeviceProximityManager *weakSelf = self; + [[NSNotificationCenter defaultCenter] addObserverForName:UIDeviceProximityStateDidChangeNotification object:[UIDevice currentDevice] queue:[NSOperationQueue mainQueue] usingBlock:^(__unused NSNotification *notification) + { + __strong DeviceProximityManager *strongSelf = weakSelf; + if (strongSelf != nil) { + bool proximityState = [UIDevice currentDevice].proximityState; + if (strongSelf->_proximityState != proximityState) { + strongSelf->_proximityState = proximityState; + if (!strongSelf->_proximityState && [strongSelf->_subscribers isEmpty]) { + [UIDevice currentDevice].proximityMonitoringEnabled = false; + } + for (void (^f)(bool) in [strongSelf->_subscribers copyItems]) { + f(proximityState); + } + } else if (!strongSelf->_proximityState && [strongSelf->_subscribers isEmpty]) { + [UIDevice currentDevice].proximityMonitoringEnabled = false; + } + } + }]; + } + return self; +} + +- (void)setGloballyEnabled:(bool)value { + if (_globallyEnabled != value) { + _globallyEnabled = value; + + [self updateState:![_subscribers isEmpty] globallyEnabled:_globallyEnabled]; + } +} + +- (NSInteger)add:(void (^)(bool))f { + bool wasEmpty = [_subscribers isEmpty]; + NSInteger index = [_subscribers addItem:[f copy]]; + f(_proximityState); + if (wasEmpty) { + [self updateState:true globallyEnabled:_globallyEnabled]; + } + return index; +} + +- (void)remove:(NSInteger)index { + bool wasEmpty = [_subscribers isEmpty]; + [_subscribers removeItem:index]; + if ([_subscribers isEmpty] && !wasEmpty) { + [self updateState:false globallyEnabled:_globallyEnabled]; + } +} + +- (void)updateState:(bool)hasSubscribers globallyEnabled:(bool)globallyEnabled { + if (hasSubscribers && globallyEnabled) { + [UIDevice currentDevice].proximityMonitoringEnabled = true; + bool deviceProximityState = [UIDevice currentDevice].proximityState; + if (deviceProximityState != _proximityState) { + _proximityState = deviceProximityState; + for (void (^f)(bool) in [_subscribers copyItems]) { + f(_proximityState); + } + } + } else { + if (_proximityState) { + _proximityState = false; + for (void (^f)(bool) in [_subscribers copyItems]) { + f(_proximityState); + } + } else { + [UIDevice currentDevice].proximityMonitoringEnabled = false; + } + } +} + +@end diff --git a/TelegramUI/DirectionalPanGestureRecognizer.swift b/TelegramUI/DirectionalPanGestureRecognizer.swift new file mode 100644 index 0000000000..f62fed9d63 --- /dev/null +++ b/TelegramUI/DirectionalPanGestureRecognizer.swift @@ -0,0 +1,53 @@ +import Foundation +import UIKit + +class DirectionalPanGestureRecognizer: UIPanGestureRecognizer { + var validatedGesture = false + var firstLocation: CGPoint = CGPoint() + + override init(target: Any?, action: Selector?) { + super.init(target: target, action: action) + + self.maximumNumberOfTouches = 1 + } + + override func reset() { + super.reset() + + self.validatedGesture = false + } + + override func touchesBegan(_ touches: Set, with event: UIEvent) { + super.touchesBegan(touches, with: event) + + let touch = touches.first! + self.firstLocation = touch.location(in: self.view) + + if let target = self.view?.hitTest(self.firstLocation, with: event) { + if target == self.view { + self.validatedGesture = true + } + } + } + + override func touchesMoved(_ touches: Set, with event: UIEvent) { + let location = touches.first!.location(in: self.view) + let translation = CGPoint(x: location.x - self.firstLocation.x, y: location.y - self.firstLocation.y) + + let absTranslationX: CGFloat = abs(translation.x) + let absTranslationY: CGFloat = abs(translation.y) + + if !self.validatedGesture { + if absTranslationX > 4.0 && absTranslationX > absTranslationY * 2.0 { + self.state = .failed + } else if absTranslationY > 2.0 && absTranslationX * 2.0 < absTranslationY { + self.validatedGesture = true + } + } + + if self.validatedGesture { + super.touchesMoved(touches, with: event) + } + } +} + diff --git a/TelegramUI/EditAccessoryPanelNode.swift b/TelegramUI/EditAccessoryPanelNode.swift index a209dcaeef..9058e785bb 100644 --- a/TelegramUI/EditAccessoryPanelNode.swift +++ b/TelegramUI/EditAccessoryPanelNode.swift @@ -12,22 +12,26 @@ final class EditAccessoryPanelNode: AccessoryPanelNode { let lineNode: ASImageNode let titleNode: ASTextNode let textNode: ASTextNode - var activityIndicator: UIActivityIndicatorView? + let imageNode: TransformImageNode + + let activityIndicator: ActivityIndicator private let messageDisposable = MetaDisposable() private let editingMessageDisposable = MetaDisposable() + private var previousMedia: Media? + override var interfaceInteraction: ChatPanelInterfaceInteraction? { didSet { if let statuses = self.interfaceInteraction?.statuses { self.editingMessageDisposable.set(statuses.editingMessage.start(next: { [weak self] value in - if let strongSelf = self, let activityIndicator = strongSelf.activityIndicator { + if let strongSelf = self { if value { - activityIndicator.isHidden = false - activityIndicator.startAnimating() - } else { - activityIndicator.isHidden = true - activityIndicator.stopAnimating() + if strongSelf.activityIndicator.supernode == nil { + strongSelf.addSubnode(strongSelf.activityIndicator) + } + } else if strongSelf.activityIndicator.supernode != nil { + strongSelf.activityIndicator.removeFromSupernode() } } })) @@ -61,6 +65,12 @@ final class EditAccessoryPanelNode: AccessoryPanelNode { self.textNode.maximumNumberOfLines = 1 self.textNode.displaysAsynchronously = false + self.imageNode = TransformImageNode() + self.imageNode.contentAnimations = [.subsequentUpdates] + self.imageNode.isHidden = true + + self.activityIndicator = ActivityIndicator(type: .custom(theme.chat.inputPanel.panelControlAccentColor, 22.0)) + super.init() self.closeButton.addTarget(self, action: #selector(self.closePressed), forControlEvents: [.touchUpInside]) @@ -69,17 +79,94 @@ final class EditAccessoryPanelNode: AccessoryPanelNode { self.addSubnode(self.lineNode) self.addSubnode(self.titleNode) self.addSubnode(self.textNode) + self.addSubnode(self.imageNode) self.messageDisposable.set((account.postbox.messageAtId(messageId) |> deliverOnMainQueue).start(next: { [weak self] message in if let strongSelf = self { var text = "" - if let messageText = message?.text { - text = messageText + if let message = message { + text = descriptionStringForMessage(message, strings: strings, accountPeerId: account.peerId) } - strongSelf.titleNode.attributedText = NSAttributedString(string: "Edit Message", font: Font.medium(15.0), textColor: strongSelf.theme.chat.inputPanel.panelControlAccentColor) - strongSelf.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(15.0), textColor: strongSelf.theme.chat.inputPanel.primaryTextColor) + var updatedMedia: Media? + var imageDimensions: CGSize? + if let message = message, !message.containsSecretMedia { + for media in message.media { + if let image = media as? TelegramMediaImage { + updatedMedia = image + if let representation = largestRepresentationForPhoto(image) { + imageDimensions = representation.dimensions + } + break + } else if let file = media as? TelegramMediaFile { + updatedMedia = file + if !file.isInstantVideo, let representation = largestImageRepresentation(file.previewRepresentations), !file.isSticker { + imageDimensions = representation.dimensions + } + break + } + } + } + + let imageNodeLayout = strongSelf.imageNode.asyncLayout() + var applyImage: (() -> Void)? + if let imageDimensions = imageDimensions { + let boundingSize = CGSize(width: 35.0, height: 35.0) + applyImage = imageNodeLayout(TransformImageArguments(corners: ImageCorners(radius: 2.0), imageSize: imageDimensions.aspectFilled(boundingSize), boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets())) + } + + var mediaUpdated = false + if let updatedMedia = updatedMedia, let previousMedia = strongSelf.previousMedia { + mediaUpdated = !updatedMedia.isEqual(previousMedia) + } else if (updatedMedia != nil) != (strongSelf.previousMedia != nil) { + mediaUpdated = true + } + strongSelf.previousMedia = updatedMedia + + var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? + if mediaUpdated { + if let updatedMedia = updatedMedia, imageDimensions != nil { + if let image = updatedMedia as? TelegramMediaImage { + updateImageSignal = chatMessagePhotoThumbnail(account: account, photo: image) + } else if let file = updatedMedia as? TelegramMediaFile { + if file.isVideo { + updateImageSignal = chatMessageVideoThumbnail(account: account, file: file) + } else if let iconImageRepresentation = smallestImageRepresentation(file.previewRepresentations) { + let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [iconImageRepresentation], reference: nil) + updateImageSignal = chatWebpageSnippetPhoto(account: account, photo: tmpImage) + } + } + } else { + updateImageSignal = .single({ _ in return nil }) + } + } + + let isMedia: Bool + if let message = message { + switch messageContentKind(message, strings: strings, accountPeerId: account.peerId) { + case .text: + isMedia = false + default: + isMedia = true + } + } else { + isMedia = false + } + + strongSelf.titleNode.attributedText = NSAttributedString(string: strings.Conversation_EditingMessagePanelTitle, font: Font.medium(15.0), textColor: strongSelf.theme.chat.inputPanel.panelControlAccentColor) + strongSelf.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(15.0), textColor: isMedia ? strongSelf.theme.chat.inputPanel.secondaryTextColor : strongSelf.theme.chat.inputPanel.primaryTextColor) + + if let applyImage = applyImage { + applyImage() + strongSelf.imageNode.isHidden = false + } else { + strongSelf.imageNode.isHidden = true + } + + if let updateImageSignal = updateImageSignal { + strongSelf.imageNode.setSignal(updateImageSignal) + } strongSelf.setNeedsLayout() } @@ -111,15 +198,6 @@ final class EditAccessoryPanelNode: AccessoryPanelNode { } } - override func didLoad() { - super.didLoad() - - let activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: .gray) - self.activityIndicator = activityIndicator - self.view.addSubview(activityIndicator) - activityIndicator.isHidden = true - } - override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { return CGSize(width: constrainedSize.width, height: 45.0) } @@ -133,21 +211,25 @@ final class EditAccessoryPanelNode: AccessoryPanelNode { let rightInset: CGFloat = 55.0 let textRightInset: CGFloat = 20.0 - if let activityIndicator = self.activityIndicator { - let indicatorSize = activityIndicator.bounds.size - activityIndicator.frame = CGRect(origin: CGPoint(x: 18.0, y: 15.0), size: indicatorSize) - } + let indicatorSize = CGSize(width: 22.0, height: 22.0) + activityIndicator.frame = CGRect(origin: CGPoint(x: 18.0, y: 15.0), size: indicatorSize) let closeButtonSize = self.closeButton.measure(CGSize(width: 100.0, height: 100.0)) self.closeButton.frame = CGRect(origin: CGPoint(x: bounds.size.width - rightInset - closeButtonSize.width, y: 19.0), size: closeButtonSize) self.lineNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 8.0), size: CGSize(width: 2.0, height: bounds.size.height - 10.0)) - let titleSize = self.titleNode.measure(CGSize(width: bounds.size.width - leftInset - textLineInset - rightInset - textRightInset, height: bounds.size.height)) - self.titleNode.frame = CGRect(origin: CGPoint(x: leftInset + textLineInset, y: 7.0), size: titleSize) + var imageTextInset: CGFloat = 0.0 + if !self.imageNode.isHidden { + imageTextInset = 9.0 + 35.0 + } + self.imageNode.frame = CGRect(origin: CGPoint(x: leftInset + 9.0, y: 8.0), size: CGSize(width: 35.0, height: 35.0)) - let textSize = self.textNode.measure(CGSize(width: bounds.size.width - leftInset - textLineInset - rightInset - textRightInset, height: bounds.size.height)) - self.textNode.frame = CGRect(origin: CGPoint(x: leftInset + textLineInset, y: 25.0), size: textSize) + let titleSize = self.titleNode.measure(CGSize(width: bounds.size.width - leftInset - textLineInset - rightInset - textRightInset - imageTextInset, height: bounds.size.height)) + self.titleNode.frame = CGRect(origin: CGPoint(x: leftInset + textLineInset + imageTextInset, y: 7.0), size: titleSize) + + let textSize = self.textNode.measure(CGSize(width: bounds.size.width - leftInset - textLineInset - rightInset - textRightInset - imageTextInset, height: bounds.size.height)) + self.textNode.frame = CGRect(origin: CGPoint(x: leftInset + textLineInset + imageTextInset, y: 25.0), size: textSize) } @objc func closePressed() { diff --git a/TelegramUI/EditSettingsController.swift b/TelegramUI/EditSettingsController.swift new file mode 100644 index 0000000000..ed946a0514 --- /dev/null +++ b/TelegramUI/EditSettingsController.swift @@ -0,0 +1,502 @@ +import Foundation +import Display +import SwiftSignalKit +import Postbox +import TelegramCore +import LegacyComponents + +private struct EditSettingsItemArguments { + let account: Account + let accountManager: AccountManager + let avatarAndNameInfoContext: ItemListAvatarAndNameInfoItemContext + + let avatarTapAction: () -> Void + + let pushController: (ViewController) -> Void + let presentController: (ViewController) -> Void + let updateEditingName: (ItemListAvatarAndNameInfoItemName) -> Void + let updateBioText: (String, String) -> Void + let saveEditingState: () -> Void + let logout: () -> Void +} + +private enum SettingsSection: Int32 { + case info + case bio + case personalData + case logOut +} + +private enum SettingsEntry: ItemListNodeEntry { + case userInfo(PresentationTheme, PresentationStrings, Peer?, CachedPeerData?, ItemListAvatarAndNameInfoItemState, ItemListAvatarAndNameInfoItemUpdatingAvatar?) + case userInfoNotice(PresentationTheme, String) + + case bioText(PresentationTheme, String, String) + case bioInfo(PresentationTheme, String) + + case username(PresentationTheme, String, String) + case phoneNumber(PresentationTheme, String, String) + + case logOut(PresentationTheme, String) + + var section: ItemListSectionId { + switch self { + case .userInfo, .userInfoNotice: + return SettingsSection.info.rawValue + case .bioText, .bioInfo: + return SettingsSection.bio.rawValue + case .username, .phoneNumber: + return SettingsSection.personalData.rawValue + case .logOut: + return SettingsSection.logOut.rawValue + } + } + + var stableId: Int32 { + switch self { + case .userInfo: + return 0 + case .userInfoNotice: + return 1 + case .bioText: + return 2 + case .bioInfo: + return 3 + case .username: + return 4 + case .phoneNumber: + return 5 + case .logOut: + return 6 + } + } + + static func ==(lhs: SettingsEntry, rhs: SettingsEntry) -> Bool { + switch lhs { + case let .userInfo(lhsTheme, lhsStrings, lhsPeer, lhsCachedData, lhsEditingState, lhsUpdatingImage): + if case let .userInfo(rhsTheme, rhsStrings, rhsPeer, rhsCachedData, rhsEditingState, rhsUpdatingImage) = rhs { + if lhsTheme !== rhsTheme { + return false + } + if lhsStrings !== rhsStrings { + return false + } + if let lhsPeer = lhsPeer, let rhsPeer = rhsPeer { + if !lhsPeer.isEqual(rhsPeer) { + return false + } + } else if (lhsPeer != nil) != (rhsPeer != nil) { + return false + } + if let lhsCachedData = lhsCachedData, let rhsCachedData = rhsCachedData { + if !lhsCachedData.isEqual(to: rhsCachedData) { + return false + } + } else if (lhsCachedData != nil) != (rhsCachedData != nil) { + return false + } + if lhsEditingState != rhsEditingState { + return false + } + if lhsUpdatingImage != rhsUpdatingImage { + return false + } + return true + } else { + return false + } + case let .userInfoNotice(lhsTheme, lhsText): + if case let .userInfoNotice(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .bioText(lhsTheme, lhsCurrentText, lhsText): + if case let .bioText(rhsTheme, rhsCurrentText, rhsText) = rhs, lhsTheme === rhsTheme, lhsCurrentText == rhsCurrentText, lhsText == rhsText { + return true + } else { + return false + } + case let .bioInfo(lhsTheme, lhsText): + if case let .bioInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .username(lhsTheme, lhsText, lhsAddress): + if case let .username(rhsTheme, rhsText, rhsAddress) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsAddress == rhsAddress { + return true + } else { + return false + } + case let .phoneNumber(lhsTheme, lhsText, lhsNumber): + if case let .phoneNumber(rhsTheme, rhsText, rhsNumber) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsNumber == rhsNumber { + return true + } else { + return false + } + case let .logOut(lhsTheme, lhsText): + if case let .logOut(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + } + } + + static func <(lhs: SettingsEntry, rhs: SettingsEntry) -> Bool { + return lhs.stableId < rhs.stableId + } + + func item(_ arguments: EditSettingsItemArguments) -> ListViewItem { + switch self { + case let .userInfo(theme, strings, peer, cachedData, state, updatingImage): + return ItemListAvatarAndNameInfoItem(account: arguments.account, theme: theme, strings: strings, mode: .generic, peer: peer, presence: TelegramUserPresence(status: .present(until: Int32.max)), cachedData: cachedData, state: state, sectionId: ItemListSectionId(self.section), style: .blocks(withTopInset: false), editingNameUpdated: { editingName in + arguments.updateEditingName(editingName) + }, avatarTapped: { + arguments.avatarTapAction() + }, context: arguments.avatarAndNameInfoContext, updatingImage: updatingImage) + case let .userInfoNotice(theme, text): + return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + case let .bioText(theme, currentText, placeholder): + return ItemListMultilineInputItem(theme: theme, text: currentText, placeholder: placeholder, maxLength: 70, sectionId: self.section, style: .blocks, textUpdated: { updatedText in + arguments.updateBioText(currentText, updatedText) + }, action: { + + }) + case let .bioInfo(theme, text): + return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + case let .username(theme, text, address): + return ItemListDisclosureItem(theme: theme, title: text, label: address, sectionId: ItemListSectionId(self.section), style: .blocks, action: { + arguments.presentController(usernameSetupController(account: arguments.account)) + }) + case let .phoneNumber(theme, text, number): + return ItemListDisclosureItem(theme: theme, title: text, label: number, sectionId: ItemListSectionId(self.section), style: .blocks, action: { + arguments.pushController(ChangePhoneNumberIntroController(account: arguments.account, phoneNumber: number)) + }) + case let .logOut(theme, text): + return ItemListActionItem(theme: theme, title: text, kind: .destructive, alignment: .center, sectionId: ItemListSectionId(self.section), style: .blocks, action: { + arguments.logout() + }) + } + } +} + +private struct EditSettingsState: Equatable { + let updatingAvatar: ItemListAvatarAndNameInfoItemUpdatingAvatar? + let editingName: ItemListAvatarAndNameInfoItemName + let updatingName: ItemListAvatarAndNameInfoItemName? + let editingBioText: String + let updatingBioText: Bool + + init(updatingAvatar: ItemListAvatarAndNameInfoItemUpdatingAvatar? = nil, editingName: ItemListAvatarAndNameInfoItemName, updatingName: ItemListAvatarAndNameInfoItemName? = nil, editingBioText: String, updatingBioText: Bool = false) { + self.updatingAvatar = updatingAvatar + self.editingName = editingName + self.updatingName = updatingName + self.editingBioText = editingBioText + self.updatingBioText = updatingBioText + } + + func withUpdatedUpdatingAvatar(_ updatingAvatar: ItemListAvatarAndNameInfoItemUpdatingAvatar?) -> EditSettingsState { + return EditSettingsState(updatingAvatar: updatingAvatar, editingName: self.editingName, updatingName: self.updatingName, editingBioText: self.editingBioText, updatingBioText: self.updatingBioText) + } + + func withUpdatedEditingName(_ editingName: ItemListAvatarAndNameInfoItemName) -> EditSettingsState { + return EditSettingsState(updatingAvatar: self.updatingAvatar, editingName: editingName, updatingName: self.updatingName, editingBioText: self.editingBioText, updatingBioText: self.updatingBioText) + } + + func withUpdatedUpdatingName(_ updatingName: ItemListAvatarAndNameInfoItemName?) -> EditSettingsState { + return EditSettingsState(updatingAvatar: self.updatingAvatar, editingName: self.editingName, updatingName: updatingName, editingBioText: self.editingBioText, updatingBioText: self.updatingBioText) + } + + func withUpdatedEditingBioText(_ editingBioText: String) -> EditSettingsState { + return EditSettingsState(updatingAvatar: self.updatingAvatar, editingName: self.editingName, updatingName: self.updatingName, editingBioText: editingBioText, updatingBioText: self.updatingBioText) + } + + func withUpdatedUpdatingBioText(_ updatingBioText: Bool) -> EditSettingsState { + return EditSettingsState(updatingAvatar: self.updatingAvatar, editingName: self.editingName, updatingName: self.updatingName, editingBioText: self.editingBioText, updatingBioText: updatingBioText) + } + + static func ==(lhs: EditSettingsState, rhs: EditSettingsState) -> Bool { + if lhs.updatingAvatar != rhs.updatingAvatar { + return false + } + if lhs.editingName != rhs.editingName { + return false + } + if lhs.updatingName != rhs.updatingName { + return false + } + if lhs.editingBioText != rhs.editingBioText { + return false + } + if lhs.updatingBioText != rhs.updatingBioText { + return false + } + return true + } +} + +private func editSettingsEntries(presentationData: PresentationData, state: EditSettingsState, view: PeerView, wallpapers: [TelegramWallpaper]) -> [SettingsEntry] { + var entries: [SettingsEntry] = [] + + if let peer = peerViewMainPeer(view) as? TelegramUser { + let userInfoState = ItemListAvatarAndNameInfoItemState(editingName: state.editingName, updatingName: state.updatingName) + entries.append(.userInfo(presentationData.theme, presentationData.strings, peer, view.cachedData, userInfoState, state.updatingAvatar)) + entries.append(.userInfoNotice(presentationData.theme, presentationData.strings.Login_InfoHelp)) + + entries.append(.bioText(presentationData.theme, state.editingBioText, presentationData.strings.UserInfo_About_Placeholder)) + entries.append(.bioInfo(presentationData.theme, presentationData.strings.Settings_About_Help)) + + entries.append(.username(presentationData.theme, presentationData.strings.Settings_Username, peer.addressName == nil ? "" : ("@" + peer.addressName!))) + + if let phone = peer.phone { + entries.append(.phoneNumber(presentationData.theme, presentationData.strings.Settings_PhoneNumber, formatPhoneNumber(phone))) + } + + entries.append(.logOut(presentationData.theme, presentationData.strings.Settings_Logout)) + } + + return entries +} + +func editSettingsController(account: Account, currentName: ItemListAvatarAndNameInfoItemName, currentBioText: String, accountManager: AccountManager) -> ViewController { + let initialState = EditSettingsState(editingName: currentName, editingBioText: currentBioText) + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((EditSettingsState) -> EditSettingsState) -> Void = { f in + statePromise.set(stateValue.modify { f($0) }) + } + + var pushControllerImpl: ((ViewController) -> Void)? + var presentControllerImpl: ((ViewController, Any?) -> Void)? + var dismissImpl: (() -> Void)? + + let actionsDisposable = DisposableSet() + + let updateAvatarDisposable = MetaDisposable() + actionsDisposable.add(updateAvatarDisposable) + + let updatePeerNameDisposable = MetaDisposable() + actionsDisposable.add(updatePeerNameDisposable) + + let supportPeerDisposable = MetaDisposable() + actionsDisposable.add(supportPeerDisposable) + + let hiddenAvatarRepresentationDisposable = MetaDisposable() + actionsDisposable.add(hiddenAvatarRepresentationDisposable) + + let currentAvatarMixin = Atomic(value: nil) + + var avatarGalleryTransitionArguments: ((AvatarGalleryEntry) -> GalleryTransitionArguments?)? + let avatarAndNameInfoContext = ItemListAvatarAndNameInfoItemContext() + var updateHiddenAvatarImpl: (() -> Void)? + + let wallpapersPromise = Promise<[TelegramWallpaper]>() + wallpapersPromise.set(telegramWallpapers(account: account)) + + let changeProfilePhotoImpl: () -> Void = { + let _ = (account.postbox.modify { modifier -> Peer? in + return modifier.getPeer(account.peerId) + } |> deliverOnMainQueue).start(next: { peer in + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + + let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme) + legacyController.statusBar.statusBarStyle = .Ignore + + let emptyController = LegacyEmptyController(context: legacyController.context)! + let navigationController = makeLegacyNavigationController(rootController: emptyController) + navigationController.setNavigationBarHidden(true, animated: false) + navigationController.navigationBar.transform = CGAffineTransform(translationX: -1000.0, y: 0.0) + + legacyController.bind(controller: navigationController) + + presentControllerImpl?(legacyController, nil) + + var hasPhotos = false + if let peer = peer, !peer.profileImageRepresentations.isEmpty { + hasPhotos = true + } + + let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasDeleteButton: hasPhotos, personalPhoto: true, saveEditedPhotos: false, saveCapturedMedia: false)! + let _ = currentAvatarMixin.swap(mixin) + mixin.didFinishWithImage = { image in + if let image = image { + if let data = UIImageJPEGRepresentation(image, 0.6) { + let resource = LocalFileMediaResource(fileId: arc4random64()) + account.postbox.mediaBox.storeResourceData(resource.id, data: data) + let representation = TelegramMediaImageRepresentation(dimensions: CGSize(width: 640.0, height: 640.0), resource: resource) + updateState { + $0.withUpdatedUpdatingAvatar(.image(representation)) + } + updateAvatarDisposable.set((updateAccountPhoto(account: account, resource: resource) |> deliverOnMainQueue).start(next: { result in + switch result { + case .complete: + updateState { + $0.withUpdatedUpdatingAvatar(nil) + } + case .progress: + break + } + })) + } + } + } + mixin.didFinishWithDelete = { + let _ = currentAvatarMixin.swap(nil) + updateState { + if let profileImage = peer?.smallProfileImage { + return $0.withUpdatedUpdatingAvatar(.image(profileImage)) + } else { + return $0.withUpdatedUpdatingAvatar(.none) + } + } + updateAvatarDisposable.set((updateAccountPhoto(account: account, resource: nil) |> deliverOnMainQueue).start(next: { result in + switch result { + case .complete: + updateState { + $0.withUpdatedUpdatingAvatar(nil) + } + case .progress: + break + } + })) + } + mixin.didDismiss = { [weak legacyController] in + let _ = currentAvatarMixin.swap(nil) + legacyController?.dismiss() + } + let menuController = mixin.present() + if let menuController = menuController { + menuController.customRemoveFromParentViewController = { [weak legacyController] in + legacyController?.dismiss() + } + } + }) + } + + let arguments = EditSettingsItemArguments(account: account, accountManager: accountManager, avatarAndNameInfoContext: avatarAndNameInfoContext, avatarTapAction: { + var updating = false + updateState { + updating = $0.updatingAvatar != nil + return $0 + } + + if updating { + return + } + + changeProfilePhotoImpl() + }, pushController: { controller in + pushControllerImpl?(controller) + }, presentController: { controller in + presentControllerImpl?(controller, nil) + }, updateEditingName: { editingName in + updateState { state in + return state.withUpdatedEditingName(editingName) + } + }, updateBioText: { currentText, text in + updateState { state in + return state.withUpdatedEditingBioText(text) + } + }, saveEditingState: { + var updateName: ItemListAvatarAndNameInfoItemName? + var updateBio: String? + updateState { state in + if state.editingName != currentName { + updateName = state.editingName + } + if state.editingBioText != currentBioText { + updateBio = state.editingBioText + } + if updateName != nil || updateBio != nil { + return state.withUpdatedUpdatingName(state.editingName).withUpdatedUpdatingBioText(true) + } else { + return state + } + } + var updateNameSignal: Signal = .complete() + if let updateName = updateName, case let .personName(firstName, lastName) = updateName { + updateNameSignal = updateAccountPeerName(account: account, firstName: firstName, lastName: lastName) + } + var updateBioSignal: Signal = .complete() + if let updateBio = updateBio { + updateBioSignal = updateAbout(account: account, about: updateBio.isEmpty ? nil : updateBio) + |> `catch` { _ -> Signal in + return .complete() + } + } + updatePeerNameDisposable.set((combineLatest(updateNameSignal, updateBioSignal) |> deliverOnMainQueue).start(completed: { + dismissImpl?() + })) + }, logout: { + let alertController = standardTextAlertController(title: NSLocalizedString("Settings.LogoutConfirmationTitle", comment: ""), text: NSLocalizedString("Settings.LogoutConfirmationText", comment: ""), actions: [ + TextAlertAction(type: .genericAction, title: "Cancel", action: { + }), + TextAlertAction(type: .defaultAction, title: "OK", action: { + let _ = logoutFromAccount(id: account.id, accountManager: accountManager).start() + }) + ]) + presentControllerImpl?(alertController, nil) + }) + + let peerView = account.viewTracker.peerView(account.peerId) + + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get(), peerView, wallpapersPromise.get()) + |> map { presentationData, state, view, wallpapers -> (ItemListControllerState, (ItemListNodeState, SettingsEntry.ItemGenerationArguments)) in + let rightNavigationButton: ItemListNavigationButton + if state.updatingName != nil || state.updatingBioText { + rightNavigationButton = ItemListNavigationButton(title: "", style: .activity, enabled: true, action: {}) + } else { + rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Done, style: .bold, enabled: true, action: { + arguments.saveEditingState() + }) + } + + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.Common_Edit), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) + let listState = ItemListNodeState(entries: editSettingsEntries(presentationData: presentationData, state: state, view: view, wallpapers: wallpapers), style: .blocks) + + return (controllerState, (listState, arguments)) + } |> afterDisposed { + actionsDisposable.dispose() + } + + let controller = ItemListController(account: account, state: signal, tabBarItem: (account.applicationContext as! TelegramApplicationContext).presentationData |> map { presentationData in + return ItemListControllerTabBarItem(title: presentationData.strings.Settings_Title, image: PresentationResourcesRootController.tabSettingsIcon(presentationData.theme), selectedImage: PresentationResourcesRootController.tabSettingsSelectedIcon(presentationData.theme)) + }) + pushControllerImpl = { [weak controller] value in + (controller?.navigationController as? NavigationController)?.pushViewController(value) + } + presentControllerImpl = { [weak controller] value, arguments in + controller?.present(value, in: .window(.root), with: arguments ?? ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + } + dismissImpl = { [weak controller] in + (controller?.navigationController as? NavigationController)?.popViewController(animated: true) + } + avatarGalleryTransitionArguments = { [weak controller] entry in + if let controller = controller { + var result: (ASDisplayNode, CGRect)? + controller.forEachItemNode { itemNode in + if let itemNode = itemNode as? ItemListAvatarAndNameInfoItemNode { + result = itemNode.avatarTransitionNode() + } + } + if let (node, _) = result { + return GalleryTransitionArguments(transitionNode: node, addToTransitionSurface: { _ in + }) + } + } + return nil + } + updateHiddenAvatarImpl = { [weak controller] in + if let controller = controller { + controller.forEachItemNode { itemNode in + if let itemNode = itemNode as? ItemListAvatarAndNameInfoItemNode { + itemNode.updateAvatarHidden() + } + } + } + } + return controller +} + diff --git a/TelegramUI/EditableTokenListNode.swift b/TelegramUI/EditableTokenListNode.swift index 33eb3f2dca..621b4afc7e 100644 --- a/TelegramUI/EditableTokenListNode.swift +++ b/TelegramUI/EditableTokenListNode.swift @@ -155,7 +155,7 @@ final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate { } } - func updateLayout(tokens: [EditableTokenListToken], width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { + func updateLayout(tokens: [EditableTokenListToken], width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { let validTokens = Set(tokens.map { $0.id }) for i in (0 ..< self.tokenNodes.count).reversed() { @@ -177,7 +177,7 @@ final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate { self.selectedTokenId = nil } - let sideInset: CGFloat = 4.0 + let sideInset: CGFloat = 4.0 + leftInset let verticalInset: CGFloat = 6.0 let placeholderSize = self.placeholderNode.measure(CGSize(width: max(1.0, width - sideInset - sideInset), height: CGFloat.greatestFiniteMagnitude)) diff --git a/TelegramUI/EmbedGalleryVideoItem.swift b/TelegramUI/EmbedGalleryVideoItem.swift deleted file mode 100644 index f0c0735a85..0000000000 --- a/TelegramUI/EmbedGalleryVideoItem.swift +++ /dev/null @@ -1,422 +0,0 @@ -import Foundation -import Display -import AsyncDisplayKit -import SwiftSignalKit -import Postbox -import TelegramCore - -class EmbedVideoGalleryItem: GalleryItem { - let account: Account - let theme: PresentationTheme - let strings: PresentationStrings - let message: Message - let location: MessageHistoryEntryLocation? - - init(account: Account, theme: PresentationTheme, strings: PresentationStrings, message: Message, location: MessageHistoryEntryLocation?) { - self.account = account - self.theme = theme - self.strings = strings - self.message = message - self.location = location - } - - func node() -> GalleryItemNode { - let node = EmbedVideoGalleryItemNode(account: self.account, theme: self.theme, strings: self.strings) - - for media in self.message.media { - if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { - node.setWebpage(account: account, webpage: content) - break - } - } - - if let location = self.location { - node._title.set(.single("\(location.index + 1) of \(location.count)")) - } - node.setMessage(self.message) - - return node - } - - func updateNode(node: GalleryItemNode) { - if let node = node as? EmbedVideoGalleryItemNode, let location = self.location { - node._title.set(.single("\(location.index + 1) of \(location.count)")) - node.setMessage(self.message) - } - } -} - -private let pictureInPictureButtonImage = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/PictureInPictureButton"), color: .white) - -final class EmbedVideoGalleryItemNode: ZoomableContentGalleryItemNode { - fileprivate let _ready = Promise() - fileprivate let _title = Promise() - fileprivate let _titleView = Promise() - fileprivate let _rightBarButtonItem = Promise() - - private var videoNode: EmbedVideoNode? - private let scrubberView: ChatVideoGalleryItemScrubberView - - private let progressButtonNode: HighlightableButtonNode - private let progressNode: RadialProgressNode - - private var accountAndWebpage: (Account, TelegramMediaWebpageLoadedContent)? - private var message: Message? - - private var isCentral = false - - private let fetchStatusDisposable = MetaDisposable() - private let fetchDisposable = MetaDisposable() - private var resourceStatus: MediaResourceStatus? - - private let footerContentNode: ChatItemGalleryFooterContentNode - - init(account: Account, theme: PresentationTheme, strings: PresentationStrings) { - self.scrubberView = ChatVideoGalleryItemScrubberView() - self.scrubberView.hideWhenDurationIsUnknown = true - - self.progressButtonNode = HighlightableButtonNode() - self.progressNode = RadialProgressNode(theme: RadialProgressTheme(backgroundColor: UIColor(white: 0.0, alpha: 0.6), foregroundColor: UIColor.white, icon: nil)) - - self.footerContentNode = ChatItemGalleryFooterContentNode(account: account, theme: theme, strings: strings) - - super.init() - - self._titleView.set(.single(self.scrubberView)) - self.scrubberView.seek = { [weak self] timestamp in - self?.videoNode?.seek(timestamp) - } - - self.progressButtonNode.addSubnode(self.progressNode) - self.progressButtonNode.addTarget(self, action: #selector(progressButtonPressed), forControlEvents: .touchUpInside) - } - - deinit { - self.fetchStatusDisposable.dispose() - self.fetchDisposable.dispose() - } - - override func ready() -> Signal { - return self._ready.get() - } - - override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { - super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) - - let progressDiameter: CGFloat = 50.0 - let progressFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - progressDiameter) / 2.0), y: floor((layout.size.height - progressDiameter) / 2.0)), size: CGSize(width: progressDiameter, height: progressDiameter)) - transition.updateFrame(node: self.progressButtonNode, frame: progressFrame) - transition.updateFrame(node: self.progressNode, frame: CGRect(origin: CGPoint(), size: progressFrame.size)) - } - - fileprivate func setMessage(_ message: Message) { - self.footerContentNode.setMessage(message) - - self.message = message - - var rightBarButtonItem: UIBarButtonItem? - for media in message.media { - if let _ = media as? TelegramMediaWebpage { - rightBarButtonItem = UIBarButtonItem(image: pictureInPictureButtonImage, style: .plain, target: self, action: #selector(self.pictureInPictureButtonPressed)) - break - } - } - self._rightBarButtonItem.set(.single(rightBarButtonItem)) - } - - func setWebpage(account: Account, webpage: TelegramMediaWebpageLoadedContent) { - if self.accountAndWebpage == nil || self.accountAndWebpage!.1 != webpage { - if let videoNode = self.videoNode { - videoNode.pause() - videoNode.removeFromSupernode() - self.videoNode = nil - } - if let largestSize = webpage.embedSize { - let videoNode = EmbedVideoNode(manager: account.telegramApplicationContext.mediaManager, account: account, source: .webpage(webpage), priority: 0, withSound: true) - videoNode.isUserInteractionEnabled = false - scrubberView.setStatusSignal(videoNode.status) - videoNode.setShouldAcquireContext(true) - self.videoNode = videoNode - self.zoomableContent = (largestSize, videoNode) - - self._ready.set(videoNode.ready) - } else { - self._ready.set(.single(Void())) - } - - /*self.resourceStatus = nil - self.fetchStatusDisposable.set((account.postbox.mediaBox.resourceStatus(file.resource) |> deliverOnMainQueue).start(next: { [weak self] status in - if let strongSelf = self { - strongSelf.resourceStatus = status - switch status { - case let .Fetching(progress): - strongSelf.progressNode.state = .Fetching(progress: progress) - strongSelf.progressButtonNode.isHidden = false - case .Local: - strongSelf.progressNode.state = .Play - strongSelf.progressButtonNode.isHidden = strongSelf.player != nil - case .Remote: - strongSelf.progressNode.state = .Remote - strongSelf.progressButtonNode.isHidden = false - } - } - })) - if self.progressButtonNode.supernode == nil { - self.addSubnode(self.progressButtonNode) - }*/ - - self.accountAndWebpage = (account, webpage) - if true && self.isCentral { - self.progressButtonPressed() - } - } - } - - private func playVideo() { - self.videoNode?.play() - } - - private func stopVideo() { - self.videoNode?.pause() - self.progressButtonNode.isHidden = false - } - - override func centralityUpdated(isCentral: Bool) { - super.centralityUpdated(isCentral: isCentral) - - if self.isCentral != isCentral { - self.isCentral = isCentral - if isCentral { - self.playVideo() - } else { - self.stopVideo() - } - } - } - - override func animateIn(from node: ASDisplayNode, addToTransitionSurface: (UIView) -> Void) { - guard let videoNode = self.videoNode else { - return - } - - if let node = node as? EmbedVideoNode, let account = self.accountAndWebpage?.0 { - var transformedFrame = node.view.convert(node.view.bounds, to: videoNode.view) - let transformedSuperFrame = node.view.convert(node.view.bounds, to: videoNode.view.superview) - - videoNode.layer.animatePosition(from: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), to: videoNode.layer.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) - - transformedFrame.origin = CGPoint() - - let transform = CATransform3DScale(videoNode.layer.transform, transformedFrame.size.width / videoNode.layer.bounds.size.width, transformedFrame.size.height / videoNode.layer.bounds.size.height, 1.0) - videoNode.layer.animate(from: NSValue(caTransform3D: transform), to: NSValue(caTransform3D: videoNode.layer.transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25) - - account.telegramApplicationContext.mediaManager.setOverlayVideoNode(nil) - } else { - var transformedFrame = node.view.convert(node.view.bounds, to: videoNode.view) - let transformedSuperFrame = node.view.convert(node.view.bounds, to: videoNode.view.superview) - - videoNode.layer.animatePosition(from: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), to: videoNode.layer.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) - - transformedFrame.origin = CGPoint() - - let transform = CATransform3DScale(videoNode.layer.transform, transformedFrame.size.width / videoNode.layer.bounds.size.width, transformedFrame.size.height / videoNode.layer.bounds.size.height, 1.0) - videoNode.layer.animate(from: NSValue(caTransform3D: transform), to: NSValue(caTransform3D: videoNode.layer.transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25) - } - } - - override func animateOut(to node: ASDisplayNode, addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) { - guard let videoNode = self.videoNode else { - completion() - return - } - - var transformedFrame = node.view.convert(node.view.bounds, to: videoNode.view) - let transformedSuperFrame = node.view.convert(node.view.bounds, to: videoNode.view.superview) - let transformedSelfFrame = node.view.convert(node.view.bounds, to: self.view) - let transformedCopyViewInitialFrame = videoNode.view.convert(videoNode.view.bounds, to: self.view) - - var positionCompleted = false - var boundsCompleted = false - var copyCompleted = false - - let copyView = node.view.snapshotContentTree()! - - self.view.insertSubview(copyView, belowSubview: self.scrollNode.view) - copyView.frame = transformedSelfFrame - - let intermediateCompletion = { [weak copyView] in - if positionCompleted && boundsCompleted && copyCompleted { - copyView?.removeFromSuperview() - completion() - } - } - - copyView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, removeOnCompletion: false) - - copyView.layer.animatePosition(from: CGPoint(x: transformedCopyViewInitialFrame.midX, y: transformedCopyViewInitialFrame.midY), to: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) - let scale = CGSize(width: transformedCopyViewInitialFrame.size.width / transformedSelfFrame.size.width, height: transformedCopyViewInitialFrame.size.height / transformedSelfFrame.size.height) - copyView.layer.animate(from: NSValue(caTransform3D: CATransform3DMakeScale(scale.width, scale.height, 1.0)), to: NSValue(caTransform3D: CATransform3DIdentity), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in - copyCompleted = true - intermediateCompletion() - }) - - videoNode.layer.animatePosition(from: videoNode.layer.position, to: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in - positionCompleted = true - intermediateCompletion() - }) - - videoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) - - self.progressNode.layer.animatePosition(from: self.progressNode.layer.position, to: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in - //positionCompleted = true - //intermediateCompletion() - }) - self.progressNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) - self.progressNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.25, removeOnCompletion: false) - - transformedFrame.origin = CGPoint() - - let transform = CATransform3DScale(videoNode.layer.transform, transformedFrame.size.width / videoNode.layer.bounds.size.width, transformedFrame.size.height / videoNode.layer.bounds.size.height, 1.0) - videoNode.layer.animate(from: NSValue(caTransform3D: videoNode.layer.transform), to: NSValue(caTransform3D: transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in - boundsCompleted = true - intermediateCompletion() - }) - } - - func animateOut(toOverlay node: ASDisplayNode, completion: @escaping () -> Void) { - guard let videoNode = self.videoNode else { - completion() - return - } - - var transformedFrame = node.view.convert(node.view.bounds, to: videoNode.view) - let transformedSuperFrame = node.view.convert(node.view.bounds, to: videoNode.view.superview) - let transformedSelfFrame = node.view.convert(node.view.bounds, to: self.view) - let transformedCopyViewInitialFrame = videoNode.view.convert(videoNode.view.bounds, to: self.view) - let transformedSelfTargetSuperFrame = videoNode.view.convert(videoNode.view.bounds, to: node.view.superview) - - var positionCompleted = false - var boundsCompleted = false - var copyCompleted = false - var nodeCompleted = false - - let copyView = node.view.snapshotContentTree()! - - //self.view.insertSubview(copyView, belowSubview: self.scrollView) - videoNode.isHidden = true - copyView.frame = transformedSelfFrame - - let intermediateCompletion = { [weak copyView] in - if positionCompleted && boundsCompleted && copyCompleted && nodeCompleted { - copyView?.removeFromSuperview() - completion() - } - } - - copyView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, removeOnCompletion: false) - - copyView.layer.animatePosition(from: CGPoint(x: transformedCopyViewInitialFrame.midX, y: transformedCopyViewInitialFrame.midY), to: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) - let scale = CGSize(width: transformedCopyViewInitialFrame.size.width / transformedSelfFrame.size.width, height: transformedCopyViewInitialFrame.size.height / transformedSelfFrame.size.height) - copyView.layer.animate(from: NSValue(caTransform3D: CATransform3DMakeScale(scale.width, scale.height, 1.0)), to: NSValue(caTransform3D: CATransform3DIdentity), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in - copyCompleted = true - intermediateCompletion() - }) - - videoNode.layer.animatePosition(from: videoNode.layer.position, to: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in - positionCompleted = true - intermediateCompletion() - }) - - videoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) - - self.progressNode.layer.animatePosition(from: self.progressNode.layer.position, to: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in - //positionCompleted = true - //intermediateCompletion() - }) - self.progressNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) - self.progressNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.25, removeOnCompletion: false) - - transformedFrame.origin = CGPoint() - - let transform = CATransform3DScale(videoNode.layer.transform, transformedFrame.size.width / videoNode.layer.bounds.size.width, transformedFrame.size.height / videoNode.layer.bounds.size.height, 1.0) - videoNode.layer.animate(from: NSValue(caTransform3D: videoNode.layer.transform), to: NSValue(caTransform3D: transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in - boundsCompleted = true - intermediateCompletion() - }) - - //node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) - let nodeTransform = CATransform3DScale(node.layer.transform, videoNode.layer.bounds.size.width / transformedFrame.size.width, videoNode.layer.bounds.size.height / transformedFrame.size.height, 1.0) - node.layer.animatePosition(from: CGPoint(x: transformedSelfTargetSuperFrame.midX, y: transformedSelfTargetSuperFrame.midY), to: node.layer.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) - node.layer.animate(from: NSValue(caTransform3D: nodeTransform), to: NSValue(caTransform3D: node.layer.transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in - nodeCompleted = true - intermediateCompletion() - }) - } - - override func title() -> Signal { - return .single("") - } - - override func titleView() -> Signal { - return self._titleView.get() - } - - override func rightBarButtonItem() -> Signal { - return self._rightBarButtonItem.get() - } - - private func activateVideo() { - if let _ = self.accountAndWebpage { - self.playVideo() - } - } - - @objc func progressButtonPressed() { - if let _ = self.accountAndWebpage { - self.playVideo() - } - } - - @objc func pictureInPictureButtonPressed() { - if let account = self.accountAndWebpage?.0, let message = self.message, let webpage = self.accountAndWebpage?.1 { - let overlayNode = EmbedVideoNode(manager: account.telegramApplicationContext.mediaManager, account: account, source: .webpage(webpage), priority: 1, withSound: true, withOverlayControls: true) - overlayNode.dismissed = { [weak account, weak overlayNode] in - if let account = account, let overlayNode = overlayNode { - if overlayNode.supernode != nil { - account.telegramApplicationContext.mediaManager.setOverlayVideoNode(nil) - } - } - } - let baseNavigationController = self.baseNavigationController() - overlayNode.unembed = { [weak account, weak overlayNode, weak baseNavigationController] in - if let account = account { - let gallery = GalleryController(account: account, messageId: message.id, replaceRootController: { controller, ready in - if let baseNavigationController = baseNavigationController { - baseNavigationController.replaceTopController(controller, animated: false, ready: ready) - } - }, baseNavigationController: baseNavigationController) - - (baseNavigationController?.topViewController as? ViewController)?.present(gallery, in: .window(.root), with: GalleryControllerPresentationArguments(transitionArguments: { _, _ in - if let overlayNode = overlayNode, let overlaySupernode = overlayNode.supernode { - return GalleryTransitionArguments(transitionNode: overlayNode, addToTransitionSurface: { _ in - }) - } - return nil - })) - } - } - overlayNode.setShouldAcquireContext(true) - account.telegramApplicationContext.mediaManager.setOverlayVideoNode(overlayNode) - if overlayNode.supernode != nil { - self.beginCustomDismiss() - self.animateOut(toOverlay: overlayNode, completion: { [weak self] in - self?.completeCustomDismiss() - }) - } - } - } - - override func footerContent() -> Signal { - return .single(self.footerContentNode) - } -} diff --git a/TelegramUI/EmbedVideoNode.swift b/TelegramUI/EmbedVideoNode.swift index 3b6c86ef33..2a2877dde8 100644 --- a/TelegramUI/EmbedVideoNode.swift +++ b/TelegramUI/EmbedVideoNode.swift @@ -103,7 +103,7 @@ private final class SharedEmbedVideoContext: SharedVideoContext { }) if let image = webpage.image { - self.thumbnailDisposable = (rawMessagePhoto(account: account, photo: image) |> deliverOnMainQueue).start(next: { [weak self] image in + self.thumbnailDisposable = (rawMessagePhoto(postbox: account.postbox, photo: image) |> deliverOnMainQueue).start(next: { [weak self] image in if let strongSelf = self { strongSelf.thumbnail.set(.single(image)) strongSelf._ready.set(.single(Void())) @@ -113,38 +113,6 @@ private final class SharedEmbedVideoContext: SharedVideoContext { self._ready.set(.single(Void())) } - /*self.playerView.requestAudioSession = { [weak self] in - assert(Queue.mainQueue().isCurrent()) - if let strongSelf = self, !strongSelf.hasAudioSession { - strongSelf.audioSessionDisposable.set(audioSessionManager.push(audioSessionType: .play, overrideSpeaker: false, once: false, activate: { - if let strongSelf = self { - strongSelf.hasAudioSession = true - } - }, deactivate: { - return Signal { subscriber in - Queue.mainQueue().async { - if let strongSelf = self, strongSelf.hasAudioSession { - strongSelf.hasAudioSession = false - strongSelf.audioSessionDisposable.set(nil) - strongSelf.playerView.pauseVideo() - } - subscriber.putCompletion() - } - - return EmptyDisposable - } - })) - } - } - - self.playerView.disposeAudioSession = { [weak self] in - assert(Queue.mainQueue().isCurrent()) - if let strongSelf = self { - strongSelf.hasAudioSession = false - strongSelf.audioSessionDisposable.set(nil) - } - }*/ - self.playerView.stateSignal() } @@ -332,7 +300,7 @@ final class EmbedVideoNode: OverlayMediaItemNode { } if let image = source.image { - self.imageNode.setSignal(account: account, signal: chatMessagePhoto(account: account, photo: image)) + self.imageNode.setSignal(chatMessagePhoto(postbox: account.postbox, photo: image)) } } @@ -521,11 +489,11 @@ final class EmbedVideoNode: OverlayMediaItemNode { if next.playing { status = .playing } else if next.downloadProgress.isEqual(to: 1.0) { - status = .buffering(whilePlaying: next.playing) + status = .buffering(initial: false, whilePlaying: next.playing) } else { status = .paused } - subscriber.putNext(MediaPlayerStatus(generationTimestamp: 0.0, duration: next.duration, timestamp: next.position, status: status)) + subscriber.putNext(MediaPlayerStatus(generationTimestamp: 0.0, duration: next.duration, timestamp: next.position, seekId: 0, status: status)) } }) return ActionDisposable { diff --git a/TelegramUI/EmojiUtils.swift b/TelegramUI/EmojiUtils.swift index 34fda25aa2..109119ac2a 100644 --- a/TelegramUI/EmojiUtils.swift +++ b/TelegramUI/EmojiUtils.swift @@ -45,6 +45,14 @@ extension String { return emojiScalars.map { String($0) }.reduce("", +) } + var firstEmoji: String { + if let first = emojiScalars.first { + return String(first) + } else { + return "" + } + } + var emojis: [String] { var scalars: [[UnicodeScalar]] = [] var currentScalarSet: [UnicodeScalar] = [] diff --git a/TelegramUI/ExperimentalSettings.swift b/TelegramUI/ExperimentalSettings.swift new file mode 100644 index 0000000000..f18d1a13dd --- /dev/null +++ b/TelegramUI/ExperimentalSettings.swift @@ -0,0 +1,49 @@ +import Foundation +import Postbox +import SwiftSignalKit + +public struct ExperimentalSettings: PreferencesEntry, Equatable { + public var enableFeed: Bool + + public static var defaultSettings: ExperimentalSettings { + return ExperimentalSettings(decoder: PostboxDecoder(buffer: MemoryBuffer())) + } + + public init(enableFeed: Bool) { + self.enableFeed = enableFeed + } + + public init(decoder: PostboxDecoder) { + self.enableFeed = decoder.decodeInt32ForKey("enableFeed", orElse: 0) != 0 + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeInt32(self.enableFeed ? 1 : 0, forKey: "enableFeed") + } + + public func isEqual(to: PreferencesEntry) -> Bool { + if let to = to as? ExperimentalSettings { + return self == to + } else { + return false + } + } + + public static func ==(lhs: ExperimentalSettings, rhs: ExperimentalSettings) -> Bool { + return lhs.enableFeed == rhs.enableFeed + } +} + +func updateExperimentalSettingsInteractively(postbox: Postbox, _ f: @escaping (ExperimentalSettings) -> ExperimentalSettings) -> Signal { + return postbox.modify { modifier -> Void in + modifier.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.callListSettings, { entry in + let currentSettings: ExperimentalSettings + if let entry = entry as? ExperimentalSettings { + currentSettings = entry + } else { + currentSettings = ExperimentalSettings.defaultSettings + } + return f(currentSettings) + }) + } +} diff --git a/TelegramUI/ExternalMusicAlbumArtResources.swift b/TelegramUI/ExternalMusicAlbumArtResources.swift new file mode 100644 index 0000000000..fff1d8d0cc --- /dev/null +++ b/TelegramUI/ExternalMusicAlbumArtResources.swift @@ -0,0 +1,100 @@ +import Foundation +import TelegramCore +import SwiftSignalKit +import Postbox + +private func urlEncodedStringFromString(_ string: String) -> String { + var nsString: NSString = string as NSString + nsString = nsString.replacingPercentEscapes(using: String.Encoding.utf8.rawValue)! as NSString + + let result = CFURLCreateStringByAddingPercentEscapes(nil, nsString as CFString, nil, "?!@#$^&%*+=,:;'\"`<>()[]{}/\\|~ " as CFString, CFStringConvertNSStringEncodingToEncoding(String.Encoding.utf8.rawValue))! + return result as String +} + +func fetchExternalMusicAlbumArtResource(account: Account, resource: ExternalMusicAlbumArtResource) -> Signal { + return Signal { subscriber in + subscriber.putNext(.reset) + + if resource.performer.isEmpty || resource.performer.lowercased().trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) == "unknown artist" || resource.title.isEmpty { + subscriber.putNext(.dataPart(data: Data(), range: 0 ..< 0, complete: true)) + subscriber.putCompletion() + return EmptyDisposable + } else { + let excludeWords: [String] = [ + " vs. ", + " vs ", + " versus ", + " ft. ", + " ft ", + " featuring ", + " feat. ", + " feat ", + " presents ", + " pres. ", + " pres ", + " and ", + " & ", + " . " + ] + + var performer = resource.performer + + for word in excludeWords { + performer = performer.replacingOccurrences(of: word, with: " ") + } + + let metaUrl = "https://itunes.apple.com/search?term=\(urlEncodedStringFromString("\(performer) \(resource.title)"))&entity=song&limit=4" + + let fetchDisposable = MetaDisposable() + + let disposable = fetchHttpResource(url: metaUrl).start(next: { result in + if case let .dataPart(data, _, complete) = result, complete { + guard let dict = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: Any] else { + subscriber.putNext(.dataPart(data: Data(), range: 0 ..< 0, complete: true)) + subscriber.putCompletion() + return + } + + guard let results = dict["results"] as? [Any] else { + subscriber.putNext(.dataPart(data: Data(), range: 0 ..< 0, complete: true)) + subscriber.putCompletion() + return + } + + guard let result = results.first as? [String: Any] else { + subscriber.putNext(.dataPart(data: Data(), range: 0 ..< 0, complete: true)) + subscriber.putCompletion() + return + } + + guard var artworkUrl = result["artworkUrl100"] as? String else { + subscriber.putNext(.dataPart(data: Data(), range: 0 ..< 0, complete: true)) + subscriber.putCompletion() + return + } + + if !resource.isThumbnail { + artworkUrl = artworkUrl.replacingOccurrences(of: "100x100", with: "600x600") + } + + if artworkUrl.isEmpty { + subscriber.putNext(.dataPart(data: Data(), range: 0 ..< 0, complete: true)) + subscriber.putCompletion() + return + } else { + fetchDisposable.set(fetchHttpResource(url: artworkUrl).start(next: { next in + subscriber.putNext(next) + }, completed: { + subscriber.putCompletion() + })) + } + } + }) + + return ActionDisposable { + disposable.dispose() + fetchDisposable.dispose() + } + } + } +} diff --git a/TelegramUI/FFMpegMediaFrameSource.swift b/TelegramUI/FFMpegMediaFrameSource.swift index 505c75d4ad..f5cce54a31 100644 --- a/TelegramUI/FFMpegMediaFrameSource.swift +++ b/TelegramUI/FFMpegMediaFrameSource.swift @@ -183,7 +183,7 @@ final class FFMpegMediaFrameSource: NSObject, MediaFrameSource { } } - func seek(timestamp: Double) -> Signal { + func seek(timestamp: Double) -> Signal, MediaFrameSourceSeekError> { assert(self.queue.isCurrent()) return Signal { subscriber in @@ -202,19 +202,25 @@ final class FFMpegMediaFrameSource: NSObject, MediaFrameSource { context.seek(timestamp: timestamp, completed: { streamDescriptions, timestamp in queue.async { if let strongSelf = self { - var audioBuffer: MediaTrackFrameBuffer? - var videoBuffer: MediaTrackFrameBuffer? - - if let audio = streamDescriptions.audio { - audioBuffer = MediaTrackFrameBuffer(frameSource: strongSelf, decoder: audio.decoder, type: .audio, duration: audio.duration, rotationAngle: 0.0) - } - - if let video = streamDescriptions.video { - videoBuffer = MediaTrackFrameBuffer(frameSource: strongSelf, decoder: video.decoder, type: .video, duration: video.duration, rotationAngle: video.rotationAngle) - } - strongSelf.requestedFrameGenerationTimestamp = nil - subscriber.putNext(MediaFrameSourceSeekResult(buffers: MediaPlaybackBuffers(audioBuffer: audioBuffer, videoBuffer: videoBuffer), timestamp: timestamp)) + subscriber.putNext(QueueLocalObject(queue: queue, generate: { + if let strongSelf = self { + var audioBuffer: MediaTrackFrameBuffer? + var videoBuffer: MediaTrackFrameBuffer? + + if let audio = streamDescriptions.audio { + audioBuffer = MediaTrackFrameBuffer(frameSource: strongSelf, decoder: audio.decoder, type: .audio, duration: audio.duration, rotationAngle: 0.0, aspect: 1.0) + } + + if let video = streamDescriptions.video { + videoBuffer = MediaTrackFrameBuffer(frameSource: strongSelf, decoder: video.decoder, type: .video, duration: video.duration, rotationAngle: video.rotationAngle, aspect: video.aspect) + } + + return MediaFrameSourceSeekResult(buffers: MediaPlaybackBuffers(audioBuffer: audioBuffer, videoBuffer: videoBuffer), timestamp: timestamp) + } else { + return MediaFrameSourceSeekResult(buffers: MediaPlaybackBuffers(audioBuffer: nil, videoBuffer: nil), timestamp: timestamp) + } + })) subscriber.putCompletion() } } diff --git a/TelegramUI/FFMpegMediaFrameSourceContext.swift b/TelegramUI/FFMpegMediaFrameSourceContext.swift index 2dc6303c37..4507ceaa16 100644 --- a/TelegramUI/FFMpegMediaFrameSourceContext.swift +++ b/TelegramUI/FFMpegMediaFrameSourceContext.swift @@ -13,12 +13,14 @@ private struct StreamContext { let duration: CMTime let decoder: MediaTrackFrameDecoder let rotationAngle: Double + let aspect: Double } struct FFMpegMediaFrameSourceDescription { let duration: CMTime let decoder: MediaTrackFrameDecoder let rotationAngle: Double + let aspect: Double } struct FFMpegMediaFrameSourceDescriptionSet { @@ -266,7 +268,9 @@ final class FFMpegMediaFrameSourceContext: NSObject { } } - videoStream = StreamContext(index: streamIndex, codecContext: codecContext, fps: fps, timebase: timebase, duration: duration, decoder: FFMpegMediaVideoFrameDecoder(codecContext: codecContext), rotationAngle: rotationAngle) + let aspect = Double(codecPar.pointee.width) / Double(codecPar.pointee.height) + + videoStream = StreamContext(index: streamIndex, codecContext: codecContext, fps: fps, timebase: timebase, duration: duration, decoder: FFMpegMediaVideoFrameDecoder(codecContext: codecContext), rotationAngle: rotationAngle, aspect: aspect) break } else { var codecContextRef: UnsafeMutablePointer? = codecContext @@ -293,7 +297,9 @@ final class FFMpegMediaFrameSourceContext: NSObject { } } - videoStream = StreamContext(index: streamIndex, codecContext: nil, fps: fps, timebase: timebase, duration: duration, decoder: FFMpegMediaPassthroughVideoFrameDecoder(videoFormat: videoFormat, rotationAngle: rotationAngle), rotationAngle: rotationAngle) + let aspect = Double(codecPar.pointee.width) / Double(codecPar.pointee.height) + + videoStream = StreamContext(index: streamIndex, codecContext: nil, fps: fps, timebase: timebase, duration: duration, decoder: FFMpegMediaPassthroughVideoFrameDecoder(videoFormat: videoFormat, rotationAngle: rotationAngle), rotationAngle: rotationAngle, aspect: aspect) break } } @@ -309,7 +315,7 @@ final class FFMpegMediaFrameSourceContext: NSObject { let duration = CMTimeMake(avFormatContext.pointee.streams.advanced(by: streamIndex).pointee!.pointee.duration, timebase.timescale) - audioStream = StreamContext(index: streamIndex, codecContext: codecContext, fps: fps, timebase: timebase, duration: duration, decoder: FFMpegAudioFrameDecoder(codecContext: codecContext), rotationAngle: 0.0) + audioStream = StreamContext(index: streamIndex, codecContext: codecContext, fps: fps, timebase: timebase, duration: duration, decoder: FFMpegAudioFrameDecoder(codecContext: codecContext), rotationAngle: 0.0, aspect: 1.0) } else { var codecContextRef: UnsafeMutablePointer? = codecContext avcodec_free_context(&codecContextRef) @@ -461,11 +467,11 @@ final class FFMpegMediaFrameSourceContext: NSObject { var videoDescription: FFMpegMediaFrameSourceDescription? if let audioStream = initializedState.audioStream { - audioDescription = FFMpegMediaFrameSourceDescription(duration: audioStream.duration, decoder: audioStream.decoder, rotationAngle: 0.0) + audioDescription = FFMpegMediaFrameSourceDescription(duration: audioStream.duration, decoder: audioStream.decoder, rotationAngle: 0.0, aspect: 1.0) } if let videoStream = initializedState.videoStream { - videoDescription = FFMpegMediaFrameSourceDescription(duration: videoStream.duration, decoder: videoStream.decoder, rotationAngle: videoStream.rotationAngle) + videoDescription = FFMpegMediaFrameSourceDescription(duration: videoStream.duration, decoder: videoStream.decoder, rotationAngle: videoStream.rotationAngle, aspect: videoStream.aspect) } var actualPts: CMTime = CMTimeMake(0, 1) diff --git a/TelegramUI/FeaturedStickerPacksController.swift b/TelegramUI/FeaturedStickerPacksController.swift index 38c191c51a..46f91aa2b3 100644 --- a/TelegramUI/FeaturedStickerPacksController.swift +++ b/TelegramUI/FeaturedStickerPacksController.swift @@ -44,7 +44,7 @@ private enum FeaturedStickerPacksEntryId: Hashable { } private enum FeaturedStickerPacksEntry: ItemListNodeEntry { - case pack(Int32, PresentationTheme, StickerPackCollectionInfo, Bool, StickerPackItem?, String, Bool) + case pack(Int32, PresentationTheme, PresentationStrings, StickerPackCollectionInfo, Bool, StickerPackItem?, String, Bool) var section: ItemListSectionId { switch self { @@ -55,21 +55,24 @@ private enum FeaturedStickerPacksEntry: ItemListNodeEntry { var stableId: FeaturedStickerPacksEntryId { switch self { - case let .pack(_, _, info, _, _, _, _): + case let .pack(_, _, _, info, _, _, _, _): return .pack(info.id) } } static func ==(lhs: FeaturedStickerPacksEntry, rhs: FeaturedStickerPacksEntry) -> Bool { switch lhs { - case let .pack(lhsIndex, lhsTheme, lhsInfo, lhsUnread, lhsTopItem, lhsCount, lhsInstalled): - if case let .pack(rhsIndex, rhsTheme, rhsInfo, rhsUnread, rhsTopItem, rhsCount, rhsInstalled) = rhs { + case let .pack(lhsIndex, lhsTheme, lhsStrings, lhsInfo, lhsUnread, lhsTopItem, lhsCount, lhsInstalled): + if case let .pack(rhsIndex, rhsTheme, rhsStrings, rhsInfo, rhsUnread, rhsTopItem, rhsCount, rhsInstalled) = rhs { if lhsIndex != rhsIndex { return false } if lhsTheme !== rhsTheme { return false } + if lhsStrings !== rhsStrings { + return false + } if lhsInfo != rhsInfo { return false } @@ -94,9 +97,9 @@ private enum FeaturedStickerPacksEntry: ItemListNodeEntry { static func <(lhs: FeaturedStickerPacksEntry, rhs: FeaturedStickerPacksEntry) -> Bool { switch lhs { - case let .pack(lhsIndex, _, _, _, _, _, _): + case let .pack(lhsIndex, _, _, _, _, _, _, _): switch rhs { - case let .pack(rhsIndex, _, _, _, _, _, _): + case let .pack(rhsIndex, _, _, _, _, _, _, _): return lhsIndex < rhsIndex } } @@ -104,8 +107,8 @@ private enum FeaturedStickerPacksEntry: ItemListNodeEntry { func item(_ arguments: FeaturedStickerPacksControllerArguments) -> ListViewItem { switch self { - case let .pack(_, theme, info, unread, topItem, count, installed): - return ItemListStickerPackItem(theme: theme, account: arguments.account, packInfo: info, itemCount: count, topItem: topItem, unread: unread, control: .installation(installed: installed), editing: ItemListStickerPackItemEditing(editable: false, editing: false, revealed: false), enabled: true, sectionId: self.section, action: { + case let .pack(_, theme, strings, info, unread, topItem, count, installed): + return ItemListStickerPackItem(theme: theme, strings: strings, account: arguments.account, packInfo: info, itemCount: count, topItem: topItem, unread: unread, control: .installation(installed: installed), editing: ItemListStickerPackItemEditing(editable: false, editing: false, revealed: false), enabled: true, sectionId: self.section, action: { arguments.openStickerPack(info) }, setPackIdWithRevealedOptions: { _, _ in }, addPack: { @@ -148,7 +151,7 @@ private func featuredStickerPacksControllerEntries(presentationData: Presentatio if let value = unreadPacks[item.info.id] { unread = value } - entries.append(.pack(index, presentationData.theme, item.info, unread, item.topItems.first, stringForStickerCount(item.info.count), installedPacks.contains(item.info.id))) + entries.append(.pack(index, presentationData.theme, presentationData.strings, item.info, unread, item.topItems.first, stringForStickerCount(item.info.count), installedPacks.contains(item.info.id))) index += 1 } } @@ -217,7 +220,7 @@ public func featuredStickerPacksController(account: Account) -> ViewController { var unreadIds: [ItemCollectionId] = [] for entry in entries { switch entry { - case let .pack(_, _, info, unread, _, _, _): + case let .pack(_, _, _, info, unread, _, _, _): if unread && !alreadyReadIds.contains(info.id) { unreadIds.append(info.id) } diff --git a/TelegramUI/FetchCachedRepresentations.swift b/TelegramUI/FetchCachedRepresentations.swift index 5e85b5525c..0db7c82f96 100644 --- a/TelegramUI/FetchCachedRepresentations.swift +++ b/TelegramUI/FetchCachedRepresentations.swift @@ -104,7 +104,13 @@ private func fetchCachedScaledImageRepresentation(account: Account, resource: Me let path = NSTemporaryDirectory() + "\(randomId)" let url = URL(fileURLWithPath: path) - let size = representation.size + let size: CGSize + switch representation.mode { + case .fill: + size = representation.size + case .aspectFit: + size = image.size.fitted(representation.size) + } let colorImage = generateImage(size, contextGenerator: { size, context in context.setBlendMode(.copy) diff --git a/TelegramUI/FetchVideoMediaResource.swift b/TelegramUI/FetchVideoMediaResource.swift index a85c4df470..2413b19b06 100644 --- a/TelegramUI/FetchVideoMediaResource.swift +++ b/TelegramUI/FetchVideoMediaResource.swift @@ -30,7 +30,7 @@ private final class VideoConversionWatcher: TGMediaVideoFileWatcher { } } -func fetchVideoLibraryMediaResource(resource: VideoLibraryMediaResource) -> Signal { +public func fetchVideoLibraryMediaResource(resource: VideoLibraryMediaResource) -> Signal { return Signal { subscriber in subscriber.putNext(.reset) @@ -160,3 +160,50 @@ func fetchLocalFileVideoMediaResource(resource: LocalFileVideoMediaResource) -> } } } + +public func fetchVideoLibraryMediaResourceHash(resource: VideoLibraryMediaResource) -> Signal { + return Signal { subscriber in + let fetchResult = PHAsset.fetchAssets(withLocalIdentifiers: [resource.localIdentifier], options: nil) + var requestId: PHImageRequestID? + let disposable = MetaDisposable() + if fetchResult.count != 0 { + let asset = fetchResult.object(at: 0) + let option = PHVideoRequestOptions() + option.deliveryMode = .highQualityFormat + + let alreadyReceivedAsset = Atomic(value: false) + requestId = PHImageManager.default().requestAVAsset(forVideo: asset, options: option, resultHandler: { avAsset, _, _ in + if alreadyReceivedAsset.swap(true) { + return + } + + var adjustments: TGVideoEditAdjustments? + if let videoAdjustments = resource.adjustments { + if let dict = NSKeyedUnarchiver.unarchiveObject(with: videoAdjustments.data.makeData()) as? [AnyHashable : Any] { + adjustments = TGVideoEditAdjustments(dictionary: dict) + } + } + let signal = TGMediaVideoConverter.hash(for: avAsset, adjustments: adjustments)! + let signalDisposable = signal.start(next: { next in + if let next = next as? String, let data = next.data(using: .utf8) { + subscriber.putNext(data) + } else { + subscriber.putNext(nil) + } + subscriber.putCompletion() + }, error: { _ in + }, completed: nil) + disposable.set(ActionDisposable { + signalDisposable?.dispose() + }) + }) + } + + return ActionDisposable { + if let requestId = requestId { + PHImageManager.default().cancelImageRequest(requestId) + } + disposable.dispose() + } + } +} diff --git a/TelegramUI/FileMediaResourceStatus.swift b/TelegramUI/FileMediaResourceStatus.swift index 1a5eecf6dd..14d2b50a74 100644 --- a/TelegramUI/FileMediaResourceStatus.swift +++ b/TelegramUI/FileMediaResourceStatus.swift @@ -13,25 +13,31 @@ enum FileMediaResourceStatus { case playbackStatus(FileMediaResourcePlaybackStatus) } -func messageFileMediaResourceStatus(account: Account, file: TelegramMediaFile, message: Message) -> Signal { - let playbackStatus: Signal - if let applicationContext = account.applicationContext as? TelegramApplicationContext, let (playlistId, itemId) = peerMessageAudioPlaylistAndItemIds(message) { - playbackStatus = applicationContext.mediaManager.filteredPlaylistPlayerStateAndStatus(playlistId: playlistId, itemId: itemId) - |> mapToSignal { status -> Signal in - if let status = status, let playbackStatus = status.status { - return playbackStatus - |> map { playbackStatus -> MediaPlayerPlaybackStatus? in - return playbackStatus.status - } - |> distinctUntilChanged(isEqual: { lhs, rhs in - return lhs == rhs - }) - } else { - return .single(nil) - } - } +private func internalMessageFileMediaPlaybackStatus(account: Account, file: TelegramMediaFile, message: Message) -> Signal { + if let playerType = peerMessageMediaPlayerType(message), let (playlistId, itemId) = peerMessagesMediaPlaylistAndItemId(message) { + return account.telegramApplicationContext.mediaManager.filteredPlaylistState(playlistId: playlistId, itemId: itemId, type: playerType) + |> mapToSignal { state -> Signal in + return .single(state?.status) + } } else { - playbackStatus = .single(nil) + return .single(nil) + } +} + +func messageFileMediaPlaybackStatus(account: Account, file: TelegramMediaFile, message: Message) -> Signal { + var duration = 0.0 + if let value = file.duration { + duration = Double(value) + } + let defaultStatus = MediaPlayerStatus(generationTimestamp: 0.0, duration: duration, timestamp: 0.0, seekId: 0, status: .paused) + return internalMessageFileMediaPlaybackStatus(account: account, file: file, message: message) |> map { status in + return status ?? defaultStatus + } +} + +func messageFileMediaResourceStatus(account: Account, file: TelegramMediaFile, message: Message) -> Signal { + let playbackStatus = internalMessageFileMediaPlaybackStatus(account: account, file: file, message: message) |> map { status -> MediaPlayerPlaybackStatus? in + return status?.status } if message.flags.isSending { @@ -39,16 +45,16 @@ func messageFileMediaResourceStatus(account: Account, file: TelegramMediaFile, m |> map { resourceStatus, pendingStatus, playbackStatus -> FileMediaResourceStatus in if let playbackStatus = playbackStatus { switch playbackStatus { - case .playing: - return .playbackStatus(.playing) - case .paused: - return .playbackStatus(.paused) - case let .buffering(whilePlaying): - if whilePlaying { + case .playing: return .playbackStatus(.playing) - } else { + case .paused: return .playbackStatus(.paused) - } + case let .buffering(_, whilePlaying): + if whilePlaying { + return .playbackStatus(.playing) + } else { + return .playbackStatus(.paused) + } } } else if let pendingStatus = pendingStatus { return .fetchStatus(.Fetching(isActive: pendingStatus.isRunning, progress: pendingStatus.progress)) @@ -61,10 +67,16 @@ func messageFileMediaResourceStatus(account: Account, file: TelegramMediaFile, m |> map { resourceStatus, playbackStatus -> FileMediaResourceStatus in if let playbackStatus = playbackStatus { switch playbackStatus { - case .playing: - return .playbackStatus(.playing) - case .paused, .buffering: - return .playbackStatus(.paused) + case .playing: + return .playbackStatus(.playing) + case .paused: + return .playbackStatus(.paused) + case let .buffering(_, whilePlaying): + if whilePlaying { + return .playbackStatus(.playing) + } else { + return .playbackStatus(.paused) + } } } else { return .fetchStatus(resourceStatus) diff --git a/TelegramUI/ForwardAccessoryPanelNode.swift b/TelegramUI/ForwardAccessoryPanelNode.swift index d62b80d690..0c845fa08e 100644 --- a/TelegramUI/ForwardAccessoryPanelNode.swift +++ b/TelegramUI/ForwardAccessoryPanelNode.swift @@ -5,25 +5,22 @@ import Postbox import SwiftSignalKit import Display -func textStringForForwardedMessage(_ message: Message) -> (String, Bool) { - if !message.text.isEmpty { - return (message.text, false) - } else { - for media in message.media { - switch media { +func textStringForForwardedMessage(_ message: Message, strings: PresentationStrings) -> (String, Bool) { + for media in message.media { + switch media { case _ as TelegramMediaImage: - return ("Forwarded photo", true) + return (strings.ForwardedPhotos(1), true) case let file as TelegramMediaFile: - var fileName: String = "Forwarded file" + var fileName: String = strings.ForwardedFiles(1) for attribute in file.attributes { switch attribute { case .Sticker: - return ("Forwarded sticker", true) + return (strings.ForwardedStickers(1), true) case let .FileName(name): fileName = name case let .Audio(isVoice, _, title, performer, _): if isVoice { - return ("Forwarded voice Message", true) + return (strings.ForwardedAudios(1), true) } else { if let title = title, let performer = performer, !title.isEmpty, !performer.isEmpty { return (title + " — " + performer, true) @@ -32,14 +29,14 @@ func textStringForForwardedMessage(_ message: Message) -> (String, Bool) { } else if let performer = performer, !performer.isEmpty { return (performer, true) } else { - return ("Forwarded audio", true) + return (strings.ForwardedAudios(1), true) } } case .Video: if file.isAnimated { - return ("Forwarded gIF", true) + return (strings.ForwardedGifs(1), true) } else { - return ("Forwarded video", true) + return (strings.ForwardedVideos(1), true) } default: break @@ -47,19 +44,18 @@ func textStringForForwardedMessage(_ message: Message) -> (String, Bool) { } return (fileName, true) case _ as TelegramMediaContact: - return ("Forwarded contact", true) + return (strings.ForwardedContacts(1), true) case let game as TelegramMediaGame: return (game.title, true) case _ as TelegramMediaMap: - return ("Forwarded map", true) - case let action as TelegramMediaAction: + return (strings.ForwardedLocations(1), true) + case _ as TelegramMediaAction: return ("", true) default: break - } } - return ("", false) } + return (message.text, false) } final class ForwardAccessoryPanelNode: AccessoryPanelNode { @@ -122,14 +118,14 @@ final class ForwardAccessoryPanelNode: AccessoryPanelNode { } } if messages.count == 1 { - let (string, _) = textStringForForwardedMessage(messages[0]) + let (string, _) = textStringForForwardedMessage(messages[0], strings: strings) text = string } else { - text = "\(messages.count) messages" + text = strings.ForwardedMessages(Int32(messages.count)) } strongSelf.titleNode.attributedText = NSAttributedString(string: authors, font: Font.medium(15.0), textColor: strongSelf.theme.chat.inputPanel.panelControlAccentColor) - strongSelf.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(15.0), textColor: strongSelf.theme.chat.inputPanel.primaryTextColor) + strongSelf.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(15.0), textColor: strongSelf.theme.chat.inputPanel.secondaryTextColor) strongSelf.setNeedsLayout() } diff --git a/TelegramUI/GalleryController.swift b/TelegramUI/GalleryController.swift index 8627774553..99c20da50b 100644 --- a/TelegramUI/GalleryController.swift +++ b/TelegramUI/GalleryController.swift @@ -71,7 +71,7 @@ private func mediaForMessage(message: Message) -> Media? { return nil } -func galleryItemForEntry(account: Account, theme: PresentationTheme, strings: PresentationStrings, entry: MessageHistoryEntry) -> GalleryItem { +func galleryItemForEntry(account: Account, theme: PresentationTheme, strings: PresentationStrings, entry: MessageHistoryEntry, streamVideos: Bool) -> GalleryItem? { switch entry { case let .MessageEntry(message, _, location, _): if let media = mediaForMessage(message: message) { @@ -79,24 +79,32 @@ func galleryItemForEntry(account: Account, theme: PresentationTheme, strings: Pr return ChatImageGalleryItem(account: account, theme: theme, strings: strings, message: message, location: location) } else if let file = media as? TelegramMediaFile { if file.isVideo || file.mimeType.hasPrefix("video/") { - return UniversalVideoGalleryItem(account: account, theme: theme, strings: strings, content: NativeVideoContent(file: file), originData: GalleryItemOriginData(title: message.author?.displayTitle, timestamp: message.timestamp), indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) }, caption: message.text) + return UniversalVideoGalleryItem(account: account, theme: theme, strings: strings, content: NativeVideoContent(id: .message(message.id, file.fileId), file: file, streamVideo: streamVideos), originData: GalleryItemOriginData(title: message.author?.displayTitle, timestamp: message.timestamp), indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) }, contentInfo: .message(message), caption: message.text) } else { - if file.mimeType.hasPrefix("image/") { + if file.mimeType.hasPrefix("image/") && file.mimeType != "image/gif" && (file.size == nil || file.size! < 5 * 1024 * 1024) { return ChatImageGalleryItem(account: account, theme: theme, strings: strings, message: message, location: location) } else { return ChatDocumentGalleryItem(account: account, theme: theme, strings: strings, message: message, location: location) } } } else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(webpageContent) = webpage.content { - if let content = WebEmbedVideoContent(webpageContent: webpageContent) { - return UniversalVideoGalleryItem(account: account, theme: theme, strings: strings, content: content, originData: GalleryItemOriginData(title: message.author?.displayTitle, timestamp: message.timestamp), indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) }, caption: message.text) + switch websiteType(of: webpageContent) { + case .instagram where webpageContent.file != nil && webpageContent.image != nil && webpageContent.file!.isVideo: + return UniversalVideoGalleryItem(account: account, theme: theme, strings: strings, content: NativeVideoContent(id: NativeVideoContentId.message(message.id, webpage.webpageId), file: webpageContent.file!, streamVideo: true, enableSound: true), originData: GalleryItemOriginData(title: message.author?.displayTitle, timestamp: message.timestamp), indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) }, contentInfo: .message(message), caption: "") + //return UniversalVideoGalleryItem(account: account, theme: theme, strings: strings, content: SystemVideoContent(url: webpageContent.embedUrl!, image: webpageContent.image!, dimensions: webpageContent.embedSize ?? CGSize(width: 640.0, height: 640.0), duration: Int32(webpageContent.duration ?? 0)), originData: GalleryItemOriginData(title: message.author?.displayTitle, timestamp: message.timestamp), indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) }, contentInfo: .message(message), caption: "") + /*case .twitter where webpageContent.embedUrl != nil && webpageContent.image != nil: + return UniversalVideoGalleryItem(account: account, theme: theme, strings: strings, content: SystemVideoContent(url: webpageContent.embedUrl!, image: webpageContent.image!, dimensions: webpageContent.embedSize ?? CGSize(width: 640.0, height: 640.0), duration: Int32(webpageContent.duration ?? 0)), originData: GalleryItemOriginData(title: message.author?.displayTitle, timestamp: message.timestamp), indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) }, contentInfo: .message(message), caption: "")*/ + default: + if let content = WebEmbedVideoContent(webpageContent: webpageContent) { + return UniversalVideoGalleryItem(account: account, theme: theme, strings: strings, content: content, originData: GalleryItemOriginData(title: message.author?.displayTitle, timestamp: message.timestamp), indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) }, contentInfo: .message(message), caption: "") + } } } } default: break } - return ChatHoleGalleryItem() + return nil } final class GalleryTransitionArguments { @@ -132,8 +140,8 @@ private enum GalleryMessageHistoryView { } class GalleryController: ViewController { - static let darkNavigationTheme = NavigationBarTheme(buttonColor: .white, primaryTextColor: .white, backgroundColor: UIColor(white: 0.0, alpha: 0.6), separatorColor: UIColor(white: 0.0, alpha: 0.8)) - static let lightNavigationTheme = NavigationBarTheme(buttonColor: UIColor(rgb: 0x007ee5), primaryTextColor: .black, backgroundColor: UIColor(red: 0.968626451, green: 0.968626451, blue: 0.968626451, alpha: 1.0), separatorColor: UIColor(red: 0.6953125, green: 0.6953125, blue: 0.6953125, alpha: 1.0)) + static let darkNavigationTheme = NavigationBarTheme(buttonColor: .white, primaryTextColor: .white, backgroundColor: UIColor(white: 0.0, alpha: 0.6), separatorColor: UIColor(white: 0.0, alpha: 0.8), badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear) + static let lightNavigationTheme = NavigationBarTheme(buttonColor: UIColor(rgb: 0x007ee5), primaryTextColor: .black, backgroundColor: UIColor(red: 0.968626451, green: 0.968626451, blue: 0.968626451, alpha: 1.0), separatorColor: UIColor(red: 0.6953125, green: 0.6953125, blue: 0.6953125, alpha: 1.0), badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear) private var galleryNode: GalleryControllerNode { return self.displayNode as! GalleryControllerNode @@ -142,16 +150,20 @@ class GalleryController: ViewController { private let account: Account private var presentationData: PresentationData + private let streamVideos: Bool + private let _ready = Promise() override var ready: Promise { return self._ready } private var didSetReady = false + var temporaryDoNotWaitForReady = false + private let disposable = MetaDisposable() private var entries: [MessageHistoryEntry] = [] - private var centralEntryIndex: Int? + private var centralEntryStableId: UInt32? private let centralItemTitle = Promise() private let centralItemTitleView = Promise() @@ -161,23 +173,24 @@ class GalleryController: ViewController { private let centralItemAttributesDisposable = DisposableSet(); private let _hiddenMedia = Promise<(MessageId, Media)?>(nil) - var hiddenMedia: Signal<(MessageId, Media)?, NoError> { - return self._hiddenMedia.get() - } private let replaceRootController: (ViewController, ValuePromise?) -> Void private let baseNavigationController: NavigationController? - init(account: Account, messageId: MessageId, invertItemOrder: Bool = false, replaceRootController: @escaping (ViewController, ValuePromise?) -> Void, baseNavigationController: NavigationController?) { + private var hiddenMediaManagerIndex: Int? + + init(account: Account, messageId: MessageId, invertItemOrder: Bool = false, streamSingleVideo: Bool = false, replaceRootController: @escaping (ViewController, ValuePromise?) -> Void, baseNavigationController: NavigationController?) { self.account = account self.replaceRootController = replaceRootController self.baseNavigationController = baseNavigationController + self.streamVideos = streamSingleVideo self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } super.init(navigationBarTheme: GalleryController.darkNavigationTheme) - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)) + let backItem = UIBarButtonItem(backButtonAppearanceWithTitle: presentationData.strings.Common_Back, target: self, action: #selector(self.donePressed)) + self.navigationItem.leftBarButtonItem = backItem self.statusBar.statusBarStyle = .White @@ -186,8 +199,8 @@ class GalleryController: ViewController { let messageView = message |> filter({ $0 != nil }) |> mapToSignal { message -> Signal in - if let tags = tagsForMessage(message!) { - let view = account.postbox.aroundMessageHistoryViewForPeerId(messageId.peerId, index: MessageIndex(message!), count: 50, clipHoles: false, anchorIndex: MessageIndex(message!), fixedCombinedReadState: nil, topTaggedMessageIdNamespaces: [], tagMask: tags, orderStatistics: [.combinedLocation]) + if !streamSingleVideo, let tags = tagsForMessage(message!) { + let view = account.postbox.aroundMessageHistoryViewForLocation(.peer(messageId.peerId), index: .message(MessageIndex(message!)), anchorIndex: .message(MessageIndex(message!)), count: 50, clipHoles: false, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tagMask: tags, orderStatistics: [.combinedLocation]) return view |> mapToSignal { (view, _, _) -> Signal in @@ -204,23 +217,54 @@ class GalleryController: ViewController { self.disposable.set(messageView.start(next: { [weak self] view in if let strongSelf = self { if let view = view { - strongSelf.entries = view.entries - loop: for i in 0 ..< strongSelf.entries.count { - switch strongSelf.entries[i] { + let entries = view.entries.filter { entry in + if case .MessageEntry = entry { + return true + } else { + return false + } + } + var centralEntryStableId: UInt32? + loop: for i in 0 ..< entries.count { + switch entries[i] { case let .MessageEntry(message, _, _, _) where message.id == messageId: - strongSelf.centralEntryIndex = i + centralEntryStableId = message.stableId break loop default: break } } - if strongSelf.isViewLoaded { - strongSelf.galleryNode.pager.replaceItems(strongSelf.entries.map({ galleryItemForEntry(account: account, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, entry: $0) }), centralItemIndex: strongSelf.centralEntryIndex) - - let ready = strongSelf.galleryNode.pager.ready() |> timeout(2.0, queue: Queue.mainQueue(), alternate: .single(Void())) |> afterNext { [weak strongSelf] _ in - strongSelf?.didSetReady = true + if invertItemOrder { + strongSelf.entries = entries.reversed() + if let centralEntryStableId = centralEntryStableId { + strongSelf.centralEntryStableId = centralEntryStableId + } + } else { + strongSelf.entries = entries + strongSelf.centralEntryStableId = centralEntryStableId + } + if strongSelf.isViewLoaded { + var items: [GalleryItem] = [] + var centralItemIndex: Int? + for entry in strongSelf.entries { + if let item = galleryItemForEntry(account: account, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, entry: entry, streamVideos: streamSingleVideo) { + if case let .MessageEntry(message, _, _, _) = entry, message.stableId == strongSelf.centralEntryStableId { + centralItemIndex = items.count + } + items.append(item) + } + } + + strongSelf.galleryNode.pager.replaceItems(items, centralItemIndex: centralItemIndex) + + if strongSelf.temporaryDoNotWaitForReady { + strongSelf._ready.set(.single(true)) + } else { + let ready = strongSelf.galleryNode.pager.ready() |> timeout(2.0, queue: Queue.mainQueue(), alternate: .single(Void())) |> afterNext { [weak strongSelf] _ in + strongSelf?.didSetReady = true + } + strongSelf._ready.set(ready |> map { true }) } - strongSelf._ready.set(ready |> map { true }) } } } @@ -260,6 +304,14 @@ class GalleryController: ViewController { } } })) + + self.hiddenMediaManagerIndex = account.telegramApplicationContext.mediaManager.galleryHiddenMediaManager.addSource(self._hiddenMedia.get() |> map { messageIdAndMedia in + if let (messageId, media) = messageIdAndMedia { + return .chat(messageId, media) + } else { + return nil + } + }) } required init(coder aDecoder: NSCoder) { @@ -269,6 +321,9 @@ class GalleryController: ViewController { deinit { self.disposable.dispose() self.centralItemAttributesDisposable.dispose() + if let hiddenMediaManagerIndex = self.hiddenMediaManagerIndex { + self.account.telegramApplicationContext.mediaManager.galleryHiddenMediaManager.removeSource(hiddenMediaManagerIndex) + } } @objc func donePressed() { @@ -352,18 +407,6 @@ class GalleryController: ViewController { } } - /*if let centralItemNode = self.galleryNode.pager.centralItemNode(), let presentationArguments = self.presentationArguments as? GalleryControllerPresentationArguments { - if case let .MessageEntry(message, _, _, _) = self.entries[centralItemNode.index] { - if let media = mediaForMessage(message: message), let transitionArguments = presentationArguments.transitionArguments(message.id, media), !forceAway { - animatedOutNode = false - centralItemNode.animateOut(to: transitionArguments.transitionNode, completion: { - animatedOutNode = true - completion() - }) - } - } - }*/ - strongSelf.galleryNode.animateOut(animateContent: animatedOutNode, completion: { animatedOutInterface = true //completion() @@ -381,7 +424,18 @@ class GalleryController: ViewController { return baseNavigationController } - self.galleryNode.pager.replaceItems(self.entries.map({ galleryItemForEntry(account: self.account, theme: self.presentationData.theme, strings: self.presentationData.strings, entry: $0) }), centralItemIndex: self.centralEntryIndex) + var items: [GalleryItem] = [] + var centralItemIndex: Int? + for entry in self.entries { + if let item = galleryItemForEntry(account: account, theme: self.presentationData.theme, strings: self.presentationData.strings, entry: entry, streamVideos: self.streamVideos) { + if case let .MessageEntry(message, _, _, _) = entry, message.stableId == self.centralEntryStableId { + centralItemIndex = items.count + } + items.append(item) + } + } + + self.galleryNode.pager.replaceItems(items, centralItemIndex: centralItemIndex) self.galleryNode.pager.centralItemIndexUpdated = { [weak self] index in if let strongSelf = self { diff --git a/TelegramUI/GalleryControllerNode.swift b/TelegramUI/GalleryControllerNode.swift index d0096fb1b8..5068477775 100644 --- a/TelegramUI/GalleryControllerNode.swift +++ b/TelegramUI/GalleryControllerNode.swift @@ -3,7 +3,7 @@ import AsyncDisplayKit import Display import Postbox -class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate { +class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDelegate { var statusBar: StatusBar? var navigationBar: NavigationBar? let footerNode: GalleryFooterNode @@ -93,6 +93,14 @@ class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate { self.addSubnode(self.footerNode) } + override func didLoad() { + super.didLoad() + + /*let recognizer = SwipeToDismissGestureRecognizer(target: self, action: #selector(self.panGesture(_:))) + recognizer.delegate = self + self.view.addGestureRecognizer(recognizer)*/ + } + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { self.containerLayout = (navigationBarHeight, layout) @@ -148,8 +156,13 @@ class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate { } } + if let backgroundColor = self.backgroundNode.backgroundColor { + let updatedColor = backgroundColor.withAlphaComponent(0.0) + self.backgroundNode.backgroundColor = updatedColor + self.backgroundNode.layer.animate(from: backgroundColor.cgColor, to: updatedColor.cgColor, keyPath: "backgroundColor", timingFunction: kCAMediaTimingFunctionLinear, duration: 0.15) + } UIView.animate(withDuration: 0.25, animations: { - self.backgroundNode.backgroundColor = self.backgroundNode.backgroundColor?.withAlphaComponent(0.0) + self.statusBar?.alpha = 0.0 self.navigationBar?.alpha = 0.0 self.footerNode.alpha = 0.0 @@ -239,4 +252,19 @@ class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate { self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) } } + + @objc func panGesture(_ recognizer: SwipeToDismissGestureRecognizer) { + switch recognizer.state { + case .began: + break + case .changed: + print("changed") + case .ended: + break + case .cancelled: + break + default: + break + } + } } diff --git a/TelegramUI/GalleryFooterContentNode.swift b/TelegramUI/GalleryFooterContentNode.swift index 29261b00f2..ce81aa01de 100644 --- a/TelegramUI/GalleryFooterContentNode.swift +++ b/TelegramUI/GalleryFooterContentNode.swift @@ -19,7 +19,7 @@ open class GalleryFooterContentNode: ASDisplayNode { var requestLayout: ((ContainedViewLayoutTransition) -> Void)? var controllerInteraction: GalleryControllerInteraction? - func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { + func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { return 0.0 } } diff --git a/TelegramUI/GalleryFooterNode.swift b/TelegramUI/GalleryFooterNode.swift index 1abb0d2683..ad384cb4ec 100644 --- a/TelegramUI/GalleryFooterNode.swift +++ b/TelegramUI/GalleryFooterNode.swift @@ -23,6 +23,7 @@ final class GalleryFooterNode: ASDisplayNode { func updateLayout(_ layout: ContainerViewLayout, footerContentNode: GalleryFooterContentNode?, transition: ContainedViewLayoutTransition) { self.currentLayout = layout + let cleanInsets = layout.insets(options: []) var removeCurrentFooterContentNode: GalleryFooterContentNode? if self.currentFooterContentNode !== footerContentNode { @@ -48,7 +49,7 @@ final class GalleryFooterNode: ASDisplayNode { var backgroundHeight: CGFloat = 0.0 if let footerContentNode = self.currentFooterContentNode { - backgroundHeight = footerContentNode.updateLayout(width: layout.size.width, transition: transition) + backgroundHeight = footerContentNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: cleanInsets.bottom, transition: transition) transition.updateFrame(node: footerContentNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - backgroundHeight), size: CGSize(width: layout.size.width, height: backgroundHeight))) } diff --git a/TelegramUI/GalleryHiddenMediaManager.swift b/TelegramUI/GalleryHiddenMediaManager.swift new file mode 100644 index 0000000000..d2e3a8c646 --- /dev/null +++ b/TelegramUI/GalleryHiddenMediaManager.swift @@ -0,0 +1,136 @@ +import Foundation +import Postbox +import SwiftSignalKit + +enum GalleryHiddenMediaId: Hashable { + case chat(MessageId, Media) + + static func ==(lhs: GalleryHiddenMediaId, rhs: GalleryHiddenMediaId) -> Bool { + switch lhs { + case let .chat(lhsMessageId, lhsMedia): + if case let .chat(rhsMessageId, rhsMedia) = rhs, lhsMessageId == rhsMessageId, lhsMedia.isEqual(rhsMedia) { + return true + } else { + return false + } + } + } + + var hashValue: Int { + switch self { + case let .chat(messageId, _): + return messageId.hashValue + } + } +} + +private final class GalleryHiddenMediaContext { + private var ids = Set() + + func add(id: Int32) { + self.ids.insert(id) + } + + func remove(id: Int32) { + self.ids.remove(id) + } + + var isEmpty: Bool { + return self.ids.isEmpty + } +} + +final class GalleryHiddenMediaManager { + private var nextId: Int32 = 0 + private var contexts: [GalleryHiddenMediaId: GalleryHiddenMediaContext] = [:] + + private var sourcesDisposables = Bag() + private var subscribers = Bag<(Set) -> Void>() + + func hiddenIds() -> Signal, NoError> { + return Signal { [weak self] subscriber in + let disposable = MetaDisposable() + Queue.mainQueue().async { + if let strongSelf = self { + subscriber.putNext(Set(strongSelf.contexts.keys)) + let index = strongSelf.subscribers.add({ next in + subscriber.putNext(next) + }) + disposable.set(ActionDisposable { + Queue.mainQueue().async { + if let strongSelf = self { + strongSelf.subscribers.remove(index) + } + } + }) + } + } + return disposable + } + } + + private func withContext(id: GalleryHiddenMediaId, _ f: (GalleryHiddenMediaContext) -> Void) { + let context: GalleryHiddenMediaContext + if let current = self.contexts[id] { + context = current + } else { + context = GalleryHiddenMediaContext() + self.contexts[id] = context + } + + let wasEmpty = context.isEmpty + + f(context) + + if context.isEmpty { + self.contexts.removeValue(forKey: id) + } + + if context.isEmpty != wasEmpty { + let allIds = Set(self.contexts.keys) + for subscriber in self.subscribers.copyItems() { + subscriber(allIds) + } + } + } + + func addSource(_ signal: Signal) -> Int { + var state: (GalleryHiddenMediaId, Int32)? + let index = self.sourcesDisposables.add((signal |> deliverOnMainQueue).start(next: { [weak self] id in + if let strongSelf = self { + if id != state?.0 { + if let (previousId, previousIndex) = state { + strongSelf.removeHiddenMedia(id: previousId, index: previousIndex) + state = nil + } + if let id = id { + state = (id, strongSelf.addHiddenMedia(id: id)) + } + } + } + })) + return index + } + + func removeSource(_ index: Int) { + if let disposable = self.sourcesDisposables.get(index) { + self.sourcesDisposables.remove(index) + disposable.dispose() + } + } + + private func addHiddenMedia(id: GalleryHiddenMediaId) -> Int32 { + let itemId = self.nextId + self.nextId += 1 + self.withContext(id: id, { context in + context.add(id: itemId) + }) + return itemId + } + + private func removeHiddenMedia(id: GalleryHiddenMediaId, index: Int32) { + self.withContext(id: id, { context in + context.remove(id: index) + }) + } +} diff --git a/TelegramUI/GalleryPagerNode.swift b/TelegramUI/GalleryPagerNode.swift index d93016fc8c..50c925de49 100644 --- a/TelegramUI/GalleryPagerNode.swift +++ b/TelegramUI/GalleryPagerNode.swift @@ -42,6 +42,7 @@ final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate { private var items: [GalleryItem] = [] private var itemNodes: [GalleryItemNode] = [] + private var ignoreDidScroll = false private var ignoreCentralItemIndexUpdate = false private var centralItemIndex: Int? { didSet { @@ -83,15 +84,22 @@ final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate { func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { self.containerLayout = (layout, navigationBarHeight) + var centralPoint: CGPoint? + if transition.isAnimated, let centralItemIndex = self.centralItemIndex, let centralItemNode = self.visibleItemNode(at: centralItemIndex) { + centralPoint = self.view.convert(CGPoint(x: centralItemNode.frame.size.width / 2.0, y: centralItemNode.frame.size.height / 2.0), from: centralItemNode.view) + } + var previousCentralNodeHorizontalOffset: CGFloat? if let centralItemIndex = self.centralItemIndex, let centralNode = self.visibleItemNode(at: centralItemIndex) { previousCentralNodeHorizontalOffset = self.scrollView.contentOffset.x - centralNode.frame.minX } + self.ignoreDidScroll = true self.scrollView.frame = CGRect(origin: CGPoint(x: -self.pageGap, y: 0.0), size: CGSize(width: layout.size.width + self.pageGap * 2.0, height: layout.size.height)) + self.ignoreDidScroll = false for i in 0 ..< self.itemNodes.count { - self.itemNodes[i].frame = CGRect(origin: CGPoint(x: CGFloat(i) * self.scrollView.bounds.size.width + self.pageGap, y: 0.0), size: CGSize(width: self.scrollView.bounds.size.width - self.pageGap * 2.0, height: self.scrollView.bounds.size.height)) + transition.updateFrame(node: self.itemNodes[i], frame: CGRect(origin: CGPoint(x: CGFloat(i) * self.scrollView.bounds.size.width + self.pageGap, y: 0.0), size: CGSize(width: self.scrollView.bounds.size.width - self.pageGap * 2.0, height: self.scrollView.bounds.size.height))) self.itemNodes[i].containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) } @@ -99,7 +107,13 @@ final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate { self.scrollView.contentOffset = CGPoint(x: centralNode.frame.minX + previousCentralNodeHorizontalOffset, y: 0.0) } - self.updateItemNodes() + self.updateItemNodes(transition: transition) + + if let centralPoint = centralPoint, let centralItemIndex = self.centralItemIndex, let centralItemNode = self.visibleItemNode(at: centralItemIndex) { + let updatedCentralPoint = self.view.convert(CGPoint(x: centralItemNode.frame.size.width / 2.0, y: centralItemNode.frame.size.height / 2.0), from: centralItemNode.view) + + transition.animatePosition(node: centralItemNode, from: centralItemNode.position.offsetBy(dx: -updatedCentralPoint.x + centralPoint.x, dy: -updatedCentralPoint.y + centralPoint.y)) + } } func ready() -> Signal { @@ -195,7 +209,7 @@ final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate { self.centralItemIndex = focusOnItem } - self.updateItemNodes() + self.updateItemNodes(transition: .immediate) } } @@ -238,7 +252,7 @@ final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate { self.itemNodes.remove(at: internalIndex) } - private func updateItemNodes() { + private func updateItemNodes(transition: ContainedViewLayoutTransition) { if self.items.isEmpty || self.containerLayout == nil { return } @@ -281,7 +295,7 @@ final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate { } for i in 0 ..< self.itemNodes.count { - self.itemNodes[i].frame = CGRect(origin: CGPoint(x: CGFloat(i) * self.scrollView.bounds.size.width + self.pageGap, y: 0.0), size: CGSize(width: self.scrollView.bounds.size.width - self.pageGap * 2.0, height: self.scrollView.bounds.size.height)) + transition.updateFrame(node: self.itemNodes[i], frame: CGRect(origin: CGPoint(x: CGFloat(i) * self.scrollView.bounds.size.width + self.pageGap, y: 0.0), size: CGSize(width: self.scrollView.bounds.size.width - self.pageGap * 2.0, height: self.scrollView.bounds.size.height))) } if resetOffsetToCentralItem { @@ -326,7 +340,7 @@ final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate { let previousCentralCandidateHorizontalOffset = self.scrollView.contentOffset.x - centralItemCandidateNode.frame.minX for i in 0 ..< self.itemNodes.count { - self.itemNodes[i].frame = CGRect(origin: CGPoint(x: CGFloat(i) * self.scrollView.bounds.size.width + self.pageGap, y: 0.0), size: CGSize(width: self.scrollView.bounds.size.width - self.pageGap * 2.0, height: self.scrollView.bounds.size.height)) + transition.updateFrame(node: self.itemNodes[i], frame: CGRect(origin: CGPoint(x: CGFloat(i) * self.scrollView.bounds.size.width + self.pageGap, y: 0.0), size: CGSize(width: self.scrollView.bounds.size.width - self.pageGap * 2.0, height: self.scrollView.bounds.size.height))) } self.scrollView.contentOffset = CGPoint(x: centralItemCandidateNode.frame.minX + previousCentralCandidateHorizontalOffset, y: 0.0) @@ -338,8 +352,10 @@ final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate { } for itemNode in self.itemNodes { + let isVisible = self.scrollView.bounds.intersects(itemNode.frame) itemNode.centralityUpdated(isCentral: itemNode.index == self.centralItemIndex) - itemNode.visibilityUpdated(isVisible: self.scrollView.bounds.intersects(itemNode.frame)) + itemNode.visibilityUpdated(isVisible: isVisible) + itemNode.isHidden = !isVisible } if notifyCentralItemUpdated { @@ -348,7 +364,9 @@ final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate { } func scrollViewDidScroll(_ scrollView: UIScrollView) { - self.updateItemNodes() + if !self.ignoreDidScroll { + self.updateItemNodes(transition: .immediate) + } } private func centralItemCandidate() -> GalleryItemNode? { diff --git a/TelegramUI/GalleryVideoDecoration.swift b/TelegramUI/GalleryVideoDecoration.swift index ecddc1086f..4e9488932f 100644 --- a/TelegramUI/GalleryVideoDecoration.swift +++ b/TelegramUI/GalleryVideoDecoration.swift @@ -39,6 +39,9 @@ final class GalleryVideoDecoration: UniversalVideoDecoration { } } + func updateContentNodeSnapshot(_ snapshot: UIView?) { + } + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { self.validLayoutSize = size diff --git a/TelegramUI/GameControllerNode.swift b/TelegramUI/GameControllerNode.swift index c02d9bd03f..e1b384c3a4 100644 --- a/TelegramUI/GameControllerNode.swift +++ b/TelegramUI/GameControllerNode.swift @@ -27,6 +27,8 @@ final class GameControllerNode: ViewControllerTracingNode { super.init() + self.backgroundColor = .white + let js = "var TelegramWebviewProxyProto = function() {}; " + "TelegramWebviewProxyProto.prototype.postEvent = function(eventName, eventData) { " + "window.webkit.messageHandlers.performAction.postMessage({'eventName': eventName, 'eventData': eventData}); " + diff --git a/TelegramUI/GenerateTextEntities.swift b/TelegramUI/GenerateTextEntities.swift index 46ea4278db..b2e5f583fd 100644 --- a/TelegramUI/GenerateTextEntities.swift +++ b/TelegramUI/GenerateTextEntities.swift @@ -2,6 +2,8 @@ import Foundation import TelegramCore private let dataDetector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType([.link]).rawValue) +private let dataAndPhoneNumberDetector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType([.link, .phoneNumber]).rawValue) +private let phoneNumberDetector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType([.phoneNumber]).rawValue) private let alphanumericSet = CharacterSet.alphanumerics private let validIdentifierSet: CharacterSet = { var set = CharacterSet(charactersIn: "a".unicodeScalars.first! ... "z".unicodeScalars.first!) @@ -20,111 +22,214 @@ private enum CurrentEntityType { case command case mention case hashtag + + var type: EnabledEntityTypes { + switch self { + case .command: + return .command + case .mention: + return .mention + case .hashtag: + return .hashtag + } + } } -func generateTextEntities(_ text: String) -> [MessageTextEntity] { +struct EnabledEntityTypes: OptionSet { + var rawValue: Int32 + + init(rawValue: Int32) { + self.rawValue = rawValue + } + + static let command = EnabledEntityTypes(rawValue: 1 << 0) + static let mention = EnabledEntityTypes(rawValue: 1 << 1) + static let hashtag = EnabledEntityTypes(rawValue: 1 << 2) + static let url = EnabledEntityTypes(rawValue: 1 << 3) + static let phoneNumber = EnabledEntityTypes(rawValue: 1 << 4) + + static let all: EnabledEntityTypes = [.command, .mention, .hashtag, .url, .phoneNumber] +} + +private func commitEntity(_ utf16: String.UTF16View, _ type: CurrentEntityType, _ range: Range, _ enabledTypes: EnabledEntityTypes, _ entities: inout [MessageTextEntity]) { + if !enabledTypes.contains(type.type) { + return + } + let indexRange: Range = utf16.distance(from: utf16.startIndex, to: range.lowerBound) ..< utf16.distance(from: utf16.startIndex, to: range.upperBound) + var overlaps = false + for entity in entities { + if entity.range.overlaps(indexRange) { + overlaps = true + break + } + } + if !overlaps { + let entityType: MessageTextEntityType + switch type { + case .command: + entityType = .BotCommand + case .mention: + entityType = .Mention + case .hashtag: + entityType = .Hashtag + } + entities.append(MessageTextEntity(range: indexRange, type: entityType)) + } +} + +func generateTextEntities(_ text: String, enabledTypes: EnabledEntityTypes) -> [MessageTextEntity] { var entities: [MessageTextEntity] = [] let utf16 = text.utf16 - if let dataDetector = dataDetector { - dataDetector.enumerateMatches(in: text, options: [], range: NSMakeRange(0, utf16.count), using: { result, _, _ in + var detector: NSDataDetector? + if enabledTypes.contains(.phoneNumber) && enabledTypes.contains(.url) { + detector = dataAndPhoneNumberDetector + } else if enabledTypes.contains(.phoneNumber) { + detector = phoneNumberDetector + } else if enabledTypes.contains(.url) { + detector = dataDetector + } + + if let detector = detector { + detector.enumerateMatches(in: text, options: [], range: NSMakeRange(0, utf16.count), using: { result, _, _ in if let result = result { - if result.resultType == NSTextCheckingResult.CheckingType.link { + if result.resultType == NSTextCheckingResult.CheckingType.link || result.resultType == NSTextCheckingResult.CheckingType.phoneNumber { let lowerBound = utf16.index(utf16.startIndex, offsetBy: result.range.location).samePosition(in: text) let upperBound = utf16.index(utf16.startIndex, offsetBy: result.range.location + result.range.length).samePosition(in: text) if let lowerBound = lowerBound, let upperBound = upperBound { - entities.append(MessageTextEntity(range: text.distance(from: text.startIndex, to: lowerBound) ..< text.distance(from: text.startIndex, to: upperBound), type: .Url)) + let type: MessageTextEntityType + if result.resultType == NSTextCheckingResult.CheckingType.link { + type = .Url + } else { + type = .PhoneNumber + } + entities.append(MessageTextEntity(range: utf16.distance(from: text.startIndex, to: lowerBound) ..< utf16.distance(from: text.startIndex, to: upperBound), type: type)) } } } }) } - let unicodeScalars = text.unicodeScalars - var index = unicodeScalars.startIndex - var currentEntity: (CurrentEntityType, Range)? + var index = utf16.startIndex + var currentEntity: (CurrentEntityType, Range)? - func commitEntity(_ unicodeScalars: String.UnicodeScalarView, _ type: CurrentEntityType, _ range: Range, _ entities: inout [MessageTextEntity]) { - let indexRange: Range = unicodeScalars.distance(from: unicodeScalars.startIndex, to: range.lowerBound) ..< unicodeScalars.distance(from: unicodeScalars.startIndex, to: range.upperBound) - var overlaps = false - for entity in entities { - if entity.range.overlaps(indexRange) { - overlaps = true - break - } - } - if !overlaps { - let entityType: MessageTextEntityType - switch type { - case .command: - entityType = .BotCommand - case .mention: - entityType = .Mention - case .hashtag: - entityType = .Hashtag - } - entities.append(MessageTextEntity(range: indexRange, type: entityType)) - } - } var previousScalar: UnicodeScalar? - while index != unicodeScalars.endIndex { - let c = unicodeScalars[index] - if c == "/" { - if previousScalar != nil && !identifierDelimiterSet.contains(previousScalar!) { - currentEntity = nil - } else { - if let (type, range) = currentEntity { - commitEntity(unicodeScalars, type, range, &entities) - } - currentEntity = (.command, index ..< index) - } - } else if c == "@" { - if let (type, range) = currentEntity { - if case .command = type { - currentEntity = (type, range.lowerBound ..< unicodeScalars.index(after: index)) + while index != utf16.endIndex { + let c = utf16[index] + let scalar = UnicodeScalar(c) + var notFound = true + if let scalar = scalar { + if scalar == "/" { + notFound = false + if previousScalar != nil && !identifierDelimiterSet.contains(previousScalar!) { + currentEntity = nil + } else { + if let (type, range) = currentEntity { + commitEntity(utf16, type, range, enabledTypes, &entities) + } + currentEntity = (.command, index ..< index) + } + } else if scalar == "@" { + notFound = false + if let (type, range) = currentEntity { + if case .command = type { + currentEntity = (type, range.lowerBound ..< utf16.index(after: index)) + } else { + commitEntity(utf16, type, range, enabledTypes, &entities) + currentEntity = (.mention, index ..< index) + } } else { - commitEntity(unicodeScalars, type, range, &entities) currentEntity = (.mention, index ..< index) } - } else { - currentEntity = (.mention, index ..< index) + } else if scalar == "#" { + notFound = false + if let (type, range) = currentEntity { + commitEntity(utf16, type, range, enabledTypes, &entities) + } + currentEntity = (.hashtag, index ..< index) } - } else if c == "#" { - if let (type, range) = currentEntity { - commitEntity(unicodeScalars, type, range, &entities) - } - currentEntity = (.hashtag, index ..< index) - } else { - if let (type, range) = currentEntity { - switch type { - case .command, .mention: - if validIdentifierSet.contains(c) { - currentEntity = (type, range.lowerBound ..< unicodeScalars.index(after: index)) - } else if identifierDelimiterSet.contains(c) { - if let (type, range) = currentEntity { - commitEntity(unicodeScalars, type, range, &entities) + + if notFound { + if let (type, range) = currentEntity { + switch type { + case .command, .mention: + if validIdentifierSet.contains(scalar) { + currentEntity = (type, range.lowerBound ..< utf16.index(after: index)) + } else if identifierDelimiterSet.contains(scalar) { + if let (type, range) = currentEntity { + commitEntity(utf16, type, range, enabledTypes, &entities) + } + currentEntity = nil } - currentEntity = nil - } - case .hashtag: - if alphanumericSet.contains(c) { - currentEntity = (type, range.lowerBound ..< unicodeScalars.index(after: index)) - } else if identifierDelimiterSet.contains(c) { - if let (type, range) = currentEntity { - commitEntity(unicodeScalars, type, range, &entities) + case .hashtag: + if alphanumericSet.contains(scalar) { + currentEntity = (type, range.lowerBound ..< utf16.index(after: index)) + } else if identifierDelimiterSet.contains(scalar) { + if let (type, range) = currentEntity { + commitEntity(utf16, type, range, enabledTypes, &entities) + } + currentEntity = nil } - currentEntity = nil - } + } } } } - index = unicodeScalars.index(after: index) - previousScalar = c + index = utf16.index(after: index) + previousScalar = scalar } if let (type, range) = currentEntity { - commitEntity(unicodeScalars, type, range, &entities) + commitEntity(utf16, type, range, enabledTypes, &entities) } return entities } + +func addLocallyGeneratedEntities(_ text: String, enabledTypes: EnabledEntityTypes, entities: [MessageTextEntity]) -> [MessageTextEntity]? { + var resultEntities = entities + + var hasDigits = false + if enabledTypes.contains(.phoneNumber) { + loop: for c in text.utf16 { + if let scalar = UnicodeScalar(c) { + if scalar >= "0" && scalar <= "9" { + hasDigits = true + break loop + } + } + } + } + + if hasDigits { + if let phoneNumberDetector = phoneNumberDetector, enabledTypes.contains(.phoneNumber) { + let utf16 = text.utf16 + phoneNumberDetector.enumerateMatches(in: text, options: [], range: NSMakeRange(0, utf16.count), using: { result, _, _ in + if let result = result { + if result.resultType == NSTextCheckingResult.CheckingType.phoneNumber { + let lowerBound = utf16.index(utf16.startIndex, offsetBy: result.range.location).samePosition(in: text) + let upperBound = utf16.index(utf16.startIndex, offsetBy: result.range.location + result.range.length).samePosition(in: text) + if let lowerBound = lowerBound, let upperBound = upperBound { + let indexRange: Range = utf16.distance(from: text.startIndex, to: lowerBound) ..< utf16.distance(from: text.startIndex, to: upperBound) + var overlaps = false + for entity in resultEntities { + if entity.range.overlaps(indexRange) { + overlaps = true + break + } + } + if !overlaps { + resultEntities.append(MessageTextEntity(range: indexRange, type: .PhoneNumber)) + } + } + } + } + }) + } + } + + if resultEntities.count != entities.count { + return resultEntities + } else { + return nil + } +} diff --git a/TelegramUI/GlobalExperimentalSettings.swift b/TelegramUI/GlobalExperimentalSettings.swift new file mode 100644 index 0000000000..143bfa6a53 --- /dev/null +++ b/TelegramUI/GlobalExperimentalSettings.swift @@ -0,0 +1,6 @@ +import Foundation + +public struct GlobalExperimentalSettings { + public static var isAppStoreBuild: Bool = false + public static var enableFeed: Bool = false +} diff --git a/TelegramUI/GridMessageItem.swift b/TelegramUI/GridMessageItem.swift index 270f2caa8d..835f3951c6 100644 --- a/TelegramUI/GridMessageItem.swift +++ b/TelegramUI/GridMessageItem.swift @@ -31,9 +31,9 @@ private let timezoneOffset: Int32 = { }() final class GridMessageItemSection: GridSection { - let height: CGFloat = 44.0 + let height: CGFloat = 36.0 - private let theme: PresentationTheme + fileprivate let theme: PresentationTheme private let strings: PresentationStrings private let roundedTimestamp: Int32 @@ -70,7 +70,7 @@ final class GridMessageItemSection: GridSection { } } -private let sectionTitleFont = Font.regular(17.0) +private let sectionTitleFont = Font.regular(14.0) final class GridMessageItemSectionNode: ASDisplayNode { var theme: PresentationTheme @@ -101,12 +101,12 @@ final class GridMessageItemSectionNode: ASDisplayNode { let bounds = self.bounds let titleSize = self.titleNode.measure(CGSize(width: bounds.size.width - 24.0, height: CGFloat.greatestFiniteMagnitude)) - self.titleNode.frame = CGRect(origin: CGPoint(x: 12.0, y: 18.0), size: titleSize) + self.titleNode.frame = CGRect(origin: CGPoint(x: 12.0, y: 8.0), size: titleSize) } } final class GridMessageItem: GridItem { - private let theme: PresentationTheme + fileprivate let theme: PresentationTheme private let strings: PresentationStrings private let account: Account private let message: Message @@ -126,7 +126,7 @@ final class GridMessageItem: GridItem { func node(layout: GridNodeLayout) -> GridItemNode { let node = GridMessageItemNode() if let media = mediaForMessage(self.message) { - node.setup(account: self.account, media: media, messageId: self.message.id, controllerInteraction: self.controllerInteraction) + node.setup(account: self.account, item: self, media: media, messageId: self.message.id, controllerInteraction: self.controllerInteraction) } return node } @@ -137,7 +137,7 @@ final class GridMessageItem: GridItem { return } if let media = mediaForMessage(self.message) { - node.setup(account: self.account, media: media, messageId: self.message.id, controllerInteraction: self.controllerInteraction) + node.setup(account: self.account, item: self, media: media, messageId: self.message.id, controllerInteraction: self.controllerInteraction) } } } @@ -146,8 +146,9 @@ final class GridMessageItemNode: GridItemNode { private var currentState: (Account, Media, CGSize)? private let imageNode: TransformImageNode private var messageId: MessageId? + private var item: GridMessageItem? private var controllerInteraction: ChatControllerInteraction? - private var progressNode: RadialProgressNode + private var statusNode: RadialStatusNode private var selectionNode: GridMessageSelectionNode? @@ -157,8 +158,8 @@ final class GridMessageItemNode: GridItemNode { override init() { self.imageNode = TransformImageNode() - self.progressNode = RadialProgressNode(theme: RadialProgressTheme(backgroundColor: UIColor(white: 0.0, alpha: 0.6), foregroundColor: UIColor.white, icon: nil)) - self.progressNode.isUserInteractionEnabled = false + self.statusNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.6)) + self.statusNode.isUserInteractionEnabled = false super.init() @@ -173,47 +174,63 @@ final class GridMessageItemNode: GridItemNode { override func didLoad() { super.didLoad() - self.imageNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.imageNodeTap(_:)))) + let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:))) + recognizer.tapActionAtPoint = { _ in + return .waitForSingleTap + } + self.imageNode.view.addGestureRecognizer(recognizer) } - func setup(account: Account, media: Media, messageId: MessageId, controllerInteraction: ChatControllerInteraction) { + func setup(account: Account, item: GridMessageItem, media: Media, messageId: MessageId, controllerInteraction: ChatControllerInteraction) { if self.currentState == nil || self.currentState!.0 !== account || !self.currentState!.1.isEqual(media) { var mediaDimensions: CGSize? if let image = media as? TelegramMediaImage, let largestSize = largestImageRepresentation(image.representations)?.dimensions { mediaDimensions = largestSize - self.imageNode.setSignal(account: account, signal: mediaGridMessagePhoto(account: account, photo: image), dispatchOnDisplayLink: true) + self.imageNode.setSignal(mediaGridMessagePhoto(account: account, photo: image), dispatchOnDisplayLink: true) self.fetchStatusDisposable.set(nil) - self.progressNode.removeFromSupernode() - self.progressNode.isHidden = true + self.statusNode.transitionToState(.none, completion: { [weak self] in + self?.statusNode.isHidden = true + }) self.resourceStatus = nil } else if let file = media as? TelegramMediaFile, file.isVideo { mediaDimensions = file.dimensions - self.imageNode.setSignal(account: account, signal: mediaGridMessageVideo(account: account, video: file)) + self.imageNode.setSignal(mediaGridMessageVideo(postbox: account.postbox, video: file)) self.resourceStatus = nil - self.fetchStatusDisposable.set((account.postbox.mediaBox.resourceStatus(file.resource) |> deliverOnMainQueue).start(next: { [weak self] status in + self.fetchStatusDisposable.set((messageMediaFileStatus(account: account, messageId: messageId, file: file) |> deliverOnMainQueue).start(next: { [weak self] status in if let strongSelf = self { strongSelf.resourceStatus = status + let statusState: RadialStatusNodeState switch status { case let .Fetching(isActive, progress): var adjustedProgress = progress if isActive { adjustedProgress = max(adjustedProgress, 0.027) } - strongSelf.progressNode.state = .Fetching(progress: adjustedProgress) - strongSelf.progressNode.isHidden = false + statusState = .progress(color: .white, value: CGFloat(adjustedProgress), cancelEnabled: true) case .Local: - strongSelf.progressNode.state = .None - strongSelf.progressNode.isHidden = true + statusState = .play(.white) case .Remote: - strongSelf.progressNode.state = .Remote - strongSelf.progressNode.isHidden = false + statusState = .download(.white) } + switch statusState { + case .none: + break + default: + strongSelf.statusNode.isHidden = false + } + strongSelf.statusNode.transitionToState(statusState, animated: true, completion: { + if let strongSelf = self { + if case .none = statusState { + strongSelf.statusNode.isHidden = true + } + } + }) } })) - if self.progressNode.supernode == nil { - self.addSubnode(self.progressNode) + if self.statusNode.supernode == nil { + self.imageNode.addSubnode(self.statusNode) } } @@ -224,6 +241,7 @@ final class GridMessageItemNode: GridItemNode { } self.messageId = messageId + self.item = item self.controllerInteraction = controllerInteraction self.updateSelectionState(animated: false) @@ -243,21 +261,25 @@ final class GridMessageItemNode: GridItemNode { self.selectionNode?.frame = CGRect(origin: CGPoint(), size: self.bounds.size) let progressDiameter: CGFloat = 40.0 - self.progressNode.frame = CGRect(origin: CGPoint(x: imageFrame.minX + floor((imageFrame.size.width - progressDiameter) / 2.0), y: imageFrame.minY + floor((imageFrame.size.height - progressDiameter) / 2.0)), size: CGSize(width: progressDiameter, height: progressDiameter)) + self.statusNode.frame = CGRect(origin: CGPoint(x: floor((imageFrame.size.width - progressDiameter) / 2.0), y: floor((imageFrame.size.height - progressDiameter) / 2.0)), size: CGSize(width: progressDiameter, height: progressDiameter)) } func updateSelectionState(animated: Bool) { if let messageId = self.messageId, let controllerInteraction = self.controllerInteraction { if let selectionState = controllerInteraction.selectionState { + guard let item = self.item else { + return + } + let selected = selectionState.selectedIds.contains(messageId) if let selectionNode = self.selectionNode { selectionNode.updateSelected(selected, animated: animated) selectionNode.frame = CGRect(origin: CGPoint(), size: self.bounds.size) } else { - let selectionNode = GridMessageSelectionNode(toggle: { [weak self] in + let selectionNode = GridMessageSelectionNode(theme: item.theme, toggle: { [weak self] value in if let strongSelf = self, let messageId = strongSelf.messageId { - strongSelf.controllerInteraction?.toggleMessageSelection(messageId) + strongSelf.controllerInteraction?.toggleMessagesSelection([messageId], value) } }) @@ -300,24 +322,40 @@ final class GridMessageItemNode: GridItemNode { } } - @objc func imageNodeTap(_ recognizer: UITapGestureRecognizer) { - if let controllerInteraction = self.controllerInteraction, let messageId = self.messageId, case .ended = recognizer.state { - if let (account, media, _) = self.currentState { - if let file = media as? TelegramMediaFile { - if let resourceStatus = self.resourceStatus { - switch resourceStatus { - case .Fetching: - messageMediaFileCancelInteractiveFetch(account: account, messageId: messageId, file: file) - case .Local: - controllerInteraction.openMessage(messageId) - case .Remote: - self.fetchDisposable.set(messageMediaFileInteractiveFetched(account: account, messageId: messageId, file: file).start()) - } + @objc func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { + guard let controllerInteraction = self.controllerInteraction, let messageId = self.messageId else { + return + } + + switch recognizer.state { + case .ended: + if let (gesture, _) = recognizer.lastRecognizedGestureAndLocation { + switch gesture { + case .tap: + if let (account, media, _) = self.currentState { + if let file = media as? TelegramMediaFile { + if let resourceStatus = self.resourceStatus { + switch resourceStatus { + case .Fetching: + messageMediaFileCancelInteractiveFetch(account: account, messageId: messageId, file: file) + case .Local: + controllerInteraction.openMessage(messageId) + case .Remote: + self.fetchDisposable.set(messageMediaFileInteractiveFetched(account: account, messageId: messageId, file: file).start()) + } + } + } else { + controllerInteraction.openMessage(messageId) + } + } + case .longTap: + controllerInteraction.openMessageContextMenu(messageId, self, self.bounds) + default: + break } - } else { - controllerInteraction.openMessage(messageId) } - } + default: + break } } } diff --git a/TelegramUI/GridMessageSelectionNode.swift b/TelegramUI/GridMessageSelectionNode.swift index f86e2d549a..84c8385843 100644 --- a/TelegramUI/GridMessageSelectionNode.swift +++ b/TelegramUI/GridMessageSelectionNode.swift @@ -2,25 +2,18 @@ import Foundation import AsyncDisplayKit import Display -private let checkedImage = UIImage(bundleImageName: "Chat/Message/SelectionChecked")?.precomposed() -private let uncheckedImage = UIImage(bundleImageName: "Chat/Message/SelectionUnchecked")?.precomposed() - final class GridMessageSelectionNode: ASDisplayNode { - private let toggle: () -> Void + private let toggle: (Bool) -> Void private var selected = false - private let checkNode: ASImageNode + private let checkNode: CheckNode - init(toggle: @escaping () -> Void) { + init(theme: PresentationTheme, toggle: @escaping (Bool) -> Void) { self.toggle = toggle - self.checkNode = ASImageNode() - self.checkNode.displaysAsynchronously = false - self.checkNode.displayWithoutProcessing = true - self.checkNode.isLayerBacked = true + self.checkNode = CheckNode(strokeColor: theme.list.itemCheckColors.strokeColor, fillColor: theme.list.itemCheckColors.fillColor, foregroundColor: theme.list.itemCheckColors.foregroundColor, style: .overlay) super.init() - self.checkNode.image = uncheckedImage self.addSubnode(self.checkNode) } @@ -45,20 +38,20 @@ final class GridMessageSelectionNode: ASDisplayNode { func updateSelected(_ selected: Bool, animated: Bool) { if self.selected != selected { self.selected = selected - self.checkNode.image = selected ? checkedImage : uncheckedImage + self.checkNode.setIsChecked(selected, animated: animated) } } @objc func tapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { - self.toggle() + self.toggle(!self.selected) } } override func layout() { super.layout() - let checkSize = self.checkNode.measure(CGSize(width: 200.0, height: 200.0)) - self.checkNode.frame = CGRect(origin: CGPoint(x: self.bounds.size.width - checkSize.width - 2.0, y: 2.0), size: checkSize) + let checkSize = CGSize(width: 32.0, height: 32.0) + self.checkNode.frame = CGRect(origin: CGPoint(x: self.bounds.size.width - checkSize.width - 4.0, y: 4.0), size: checkSize) } } diff --git a/TelegramUI/GroupAdminsController.swift b/TelegramUI/GroupAdminsController.swift index bc7b817f15..371a547e88 100644 --- a/TelegramUI/GroupAdminsController.swift +++ b/TelegramUI/GroupAdminsController.swift @@ -54,9 +54,9 @@ private enum GroupAdminsEntryStableId: Hashable { } private enum GroupAdminsEntry: ItemListNodeEntry { - case allAdmins(Bool) - case allAdminsInfo(String) - case peerItem(Int32, Peer, PeerPresence?, Bool, Bool) + case allAdmins(PresentationTheme, String, Bool) + case allAdminsInfo(PresentationTheme, String) + case peerItem(Int32, PresentationTheme, PresentationStrings, Peer, PeerPresence?, Bool, Bool) var section: ItemListSectionId { switch self { @@ -73,30 +73,36 @@ private enum GroupAdminsEntry: ItemListNodeEntry { return .index(0) case .allAdminsInfo: return .index(1) - case let .peerItem(_, peer, _, _, _): + case let .peerItem(_, _, _, peer, _, _, _): return .peer(peer.id) } } static func ==(lhs: GroupAdminsEntry, rhs: GroupAdminsEntry) -> Bool { switch lhs { - case let .allAdmins(value): - if case .allAdmins(value) = rhs { + case let .allAdmins(lhsTheme, lhsText, lhsValue): + if case let .allAdmins(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } - case let .allAdminsInfo(text): - if case .allAdminsInfo(text) = rhs { + case let .allAdminsInfo(lhsTheme, lhsText): + if case let .allAdminsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .peerItem(lhsIndex, lhsPeer, lhsPresence, lhsToggled, lhsEnabled): - if case let .peerItem(rhsIndex, rhsPeer, rhsPresence, rhsToggled, rhsEnabled) = rhs { + case let .peerItem(lhsIndex, lhsTheme, lhsStrings, lhsPeer, lhsPresence, lhsToggled, lhsEnabled): + if case let .peerItem(rhsIndex, rhsTheme, rhsStrings, rhsPeer, rhsPresence, rhsToggled, rhsEnabled) = rhs { if lhsIndex != rhsIndex { return false } + if lhsTheme !== rhsTheme { + return false + } + if lhsStrings !== rhsStrings { + return false + } if !lhsPeer.isEqual(rhsPeer) { return false } @@ -131,9 +137,9 @@ private enum GroupAdminsEntry: ItemListNodeEntry { default: return true } - case let .peerItem(index, _, _, _, _): + case let .peerItem(index, _, _, _, _, _, _): switch rhs { - case let .peerItem(rhsIndex, _, _, _, _): + case let .peerItem(rhsIndex, _, _, _, _, _, _): return index < rhsIndex case .allAdmins, .allAdminsInfo: return false @@ -143,16 +149,16 @@ private enum GroupAdminsEntry: ItemListNodeEntry { func item(_ arguments: GroupAdminsControllerArguments) -> ListViewItem { switch self { - case let .allAdmins(value): - return ItemListSwitchItem(title: "All Members Are Admins", value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in - arguments.updateAllAreAdmins(updatedValue) - }) - case let .allAdminsInfo(text): - return ItemListTextItem(text: .plain(text), sectionId: self.section) - case let .peerItem(_, peer, presence, toggled, enabled): - return ItemListPeerItem(account: arguments.account, peer: peer, presence: presence, text: .presence, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: toggled, enabled: enabled, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, toggleUpdated: { value in - arguments.updatePeerIsAdmin(peer.id, value) - }) + case let .allAdmins(theme, text, value): + return ItemListSwitchItem(theme: theme, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in + arguments.updateAllAreAdmins(updatedValue) + }) + case let .allAdminsInfo(theme, text): + return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + case let .peerItem(_, theme, strings, peer, presence, toggled, enabled): + return ItemListPeerItem(theme: theme, strings: strings, account: arguments.account, peer: peer, presence: presence, text: .presence, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: toggled, enabled: enabled, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, toggleUpdated: { value in + arguments.updatePeerIsAdmin(peer.id, value) + }) } } } @@ -202,7 +208,7 @@ private struct GroupAdminsControllerState: Equatable { } } -private func groupAdminsControllerEntries(account: Account, view: PeerView, state: GroupAdminsControllerState) -> [GroupAdminsEntry] { +private func groupAdminsControllerEntries(account: Account, presentationData: PresentationData, view: PeerView, state: GroupAdminsControllerState) -> [GroupAdminsEntry] { var entries: [GroupAdminsEntry] = [] if let peer = view.peers[view.peerId] as? TelegramGroup, let cachedData = view.cachedData as? CachedGroupData, let participants = cachedData.participants { @@ -213,11 +219,11 @@ private func groupAdminsControllerEntries(account: Account, view: PeerView, stat effectiveAdminsEnabled = peer.flags.contains(.adminsEnabled) } - entries.append(.allAdmins(!effectiveAdminsEnabled)) + entries.append(.allAdmins(presentationData.theme, presentationData.strings.ChatAdmins_AllMembersAreAdmins, !effectiveAdminsEnabled)) if effectiveAdminsEnabled { - entries.append(.allAdminsInfo("Only admins can add and remove members, edit name and photo of this group.")) + entries.append(.allAdminsInfo(presentationData.theme, presentationData.strings.ChatAdmins_AllMembersAreAdminsOnHelp)) } else { - entries.append(.allAdminsInfo("Group members can add new members, edit name and photo of this group.")) + entries.append(.allAdminsInfo(presentationData.theme, presentationData.strings.ChatAdmins_AllMembersAreAdminsOffHelp)) } let sortedParticipants = participants.participants.sorted(by: { lhs, rhs in @@ -270,7 +276,7 @@ private func groupAdminsControllerEntries(account: Account, view: PeerView, stat } } } - entries.append(.peerItem(index, peer, view.peerPresences[participant.peerId], isAdmin, isEnabled)) + entries.append(.peerItem(index, presentationData.theme, presentationData.strings, peer, view.peerPresences[participant.peerId], isAdmin, isEnabled)) index += 1 } } @@ -353,7 +359,7 @@ public func groupAdminsController(account: Account, peerId: PeerId) -> ViewContr var emptyStateItem: ItemListControllerEmptyStateItem? if view.cachedData == nil { - emptyStateItem = ItemListLoadingIndicatorEmptyStateItem() + emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme) } var rightNavigationButton: ItemListNavigationButton? @@ -361,8 +367,8 @@ public func groupAdminsController(account: Account, peerId: PeerId) -> ViewContr rightNavigationButton = ItemListNavigationButton(title: "", style: .activity, enabled: true, action: {}) } - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text("Admins"), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: "Back"), animateChanges: true) - let listState = ItemListNodeState(entries: groupAdminsControllerEntries(account: account, view: view, state: state), style: .blocks, emptyStateItem: emptyStateItem, animateChanges: true) + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.ChatAdmins_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) + let listState = ItemListNodeState(entries: groupAdminsControllerEntries(account: account, presentationData: presentationData, view: view, state: state), style: .blocks, emptyStateItem: emptyStateItem, animateChanges: true) return (controllerState, (listState, arguments)) } |> afterDisposed { diff --git a/TelegramUI/GroupInfoController.swift b/TelegramUI/GroupInfoController.swift index 36136b69d6..ff5953383e 100644 --- a/TelegramUI/GroupInfoController.swift +++ b/TelegramUI/GroupInfoController.swift @@ -5,6 +5,8 @@ import Postbox import TelegramCore import LegacyComponents +import SafariServices + private final class GroupInfoArguments { let account: Account let peerId: PeerId @@ -16,6 +18,7 @@ private final class GroupInfoArguments { let presentController: (ViewController, ViewControllerPresentationArguments) -> Void let changeNotificationMuteSettings: () -> Void let changeNotificationSoundSettings: () -> Void + let togglePreHistory: (Bool) -> Void let openSharedMedia: () -> Void let openAdminManagement: () -> Void let updateEditingName: (ItemListAvatarAndNameInfoItemName) -> Void @@ -25,8 +28,11 @@ private final class GroupInfoArguments { let removePeer: (PeerId) -> Void let convertToSupergroup: () -> Void let leave: () -> Void + let displayUsernameContextMenu: (String) -> Void + let displayAboutContextMenu: (String) -> Void + let aboutLinkAction: (TextLinkItemActionType, TextLinkItem) -> Void - init(account: Account, peerId: PeerId, avatarAndNameInfoContext: ItemListAvatarAndNameInfoItemContext, tapAvatarAction: @escaping () -> Void, changeProfilePhoto: @escaping () -> Void, pushController: @escaping (ViewController) -> Void, presentController: @escaping (ViewController, ViewControllerPresentationArguments) -> Void, changeNotificationMuteSettings: @escaping () -> Void, changeNotificationSoundSettings: @escaping () -> Void, openSharedMedia: @escaping () -> Void, openAdminManagement: @escaping () -> Void, updateEditingName: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, updateEditingDescriptionText: @escaping (String) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, addMember: @escaping () -> Void, removePeer: @escaping (PeerId) -> Void, convertToSupergroup: @escaping () -> Void, leave: @escaping () -> Void) { + init(account: Account, peerId: PeerId, avatarAndNameInfoContext: ItemListAvatarAndNameInfoItemContext, tapAvatarAction: @escaping () -> Void, changeProfilePhoto: @escaping () -> Void, pushController: @escaping (ViewController) -> Void, presentController: @escaping (ViewController, ViewControllerPresentationArguments) -> Void, changeNotificationMuteSettings: @escaping () -> Void, changeNotificationSoundSettings: @escaping () -> Void, togglePreHistory: @escaping (Bool) -> Void, openSharedMedia: @escaping () -> Void, openAdminManagement: @escaping () -> Void, updateEditingName: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, updateEditingDescriptionText: @escaping (String) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, addMember: @escaping () -> Void, removePeer: @escaping (PeerId) -> Void, convertToSupergroup: @escaping () -> Void, leave: @escaping () -> Void, displayUsernameContextMenu: @escaping (String) -> Void, displayAboutContextMenu: @escaping (String) -> Void, aboutLinkAction: @escaping (TextLinkItemActionType, TextLinkItem) -> Void) { self.account = account self.peerId = peerId self.avatarAndNameInfoContext = avatarAndNameInfoContext @@ -36,6 +42,7 @@ private final class GroupInfoArguments { self.presentController = presentController self.changeNotificationMuteSettings = changeNotificationMuteSettings self.changeNotificationSoundSettings = changeNotificationSoundSettings + self.togglePreHistory = togglePreHistory self.openSharedMedia = openSharedMedia self.openAdminManagement = openAdminManagement self.updateEditingName = updateEditingName @@ -45,6 +52,9 @@ private final class GroupInfoArguments { self.removePeer = removePeer self.convertToSupergroup = convertToSupergroup self.leave = leave + self.displayUsernameContextMenu = displayUsernameContextMenu + self.displayAboutContextMenu = displayAboutContextMenu + self.aboutLinkAction = aboutLinkAction } } @@ -58,6 +68,10 @@ private enum GroupInfoSection: ItemListSectionId { case leave } +private enum GroupInfoEntryTag { + case about +} + private enum GroupInfoMemberStatus { case member case admin @@ -95,23 +109,24 @@ private enum GroupEntryStableId: Hashable, Equatable { } private enum GroupInfoEntry: ItemListNodeEntry { - case info(PresentationTheme, PresentationStrings, peer: Peer?, cachedData: CachedPeerData?, state: ItemListAvatarAndNameInfoItemState, updatingAvatar: TelegramMediaImageRepresentation?) - case setGroupPhoto(PresentationTheme) + case info(PresentationTheme, PresentationStrings, peer: Peer?, cachedData: CachedPeerData?, state: ItemListAvatarAndNameInfoItemState, updatingAvatar: ItemListAvatarAndNameInfoItemUpdatingAvatar?) + case setGroupPhoto(PresentationTheme, String) case about(PresentationTheme, String) case link(PresentationTheme, String) - case sharedMedia(PresentationTheme) - case notifications(PresentationTheme, settings: PeerNotificationSettings?) + case sharedMedia(PresentationTheme, String) + case notifications(PresentationTheme, String, String) case notificationSound(PresentationTheme, String, String) - case adminManagement(PresentationTheme) - case groupTypeSetup(PresentationTheme, isPublic: Bool) - case groupDescriptionSetup(PresentationTheme, text: String) - case groupManagementInfoLabel(PresentationTheme, text: String) - case membersAdmins(PresentationTheme, count: Int) - case membersBlacklist(PresentationTheme, count: Int) - case addMember(PresentationTheme, editing: Bool) - case member(PresentationTheme, index: Int, peerId: PeerId, peer: Peer, presence: PeerPresence?, memberStatus: GroupInfoMemberStatus, editing: ItemListPeerItemEditing, enabled: Bool) - case convertToSupergroup(PresentationTheme) - case leave(PresentationTheme) + case adminManagement(PresentationTheme, String) + case groupTypeSetup(PresentationTheme, String, String) + case preHistory(PresentationTheme, String, Bool) + case groupDescriptionSetup(PresentationTheme, String, String) + case groupManagementInfoLabel(PresentationTheme, String, String) + case membersAdmins(PresentationTheme, String, String) + case membersBlacklist(PresentationTheme, String, String) + case addMember(PresentationTheme, String, editing: Bool) + case member(PresentationTheme, PresentationStrings, index: Int, peerId: PeerId, peer: Peer, presence: PeerPresence?, memberStatus: GroupInfoMemberStatus, editing: ItemListPeerItemEditing, enabled: Bool) + case convertToSupergroup(PresentationTheme, String) + case leave(PresentationTheme, String) var section: ItemListSectionId { switch self { @@ -119,7 +134,7 @@ private enum GroupInfoEntry: ItemListNodeEntry { return GroupInfoSection.info.rawValue case .about, .link: return GroupInfoSection.about.rawValue - case .groupTypeSetup, .groupDescriptionSetup, .groupManagementInfoLabel: + case .groupTypeSetup, .preHistory, .groupDescriptionSetup, .groupManagementInfoLabel: return GroupInfoSection.infoManagement.rawValue case .sharedMedia, .notifications, .notificationSound, .adminManagement: return GroupInfoSection.sharedMediaAndNotifications.rawValue @@ -166,32 +181,32 @@ private enum GroupInfoEntry: ItemListNodeEntry { } else { return false } - case let .setGroupPhoto(lhsTheme): - if case let .setGroupPhoto(rhsTheme) = rhs, lhsTheme === rhsTheme { + case let .setGroupPhoto(lhsTheme, lhsText): + if case let .setGroupPhoto(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .sharedMedia(lhsTheme): - if case let .sharedMedia(rhsTheme) = rhs, lhsTheme === rhsTheme { + case let .sharedMedia(lhsTheme, lhsText): + if case let .sharedMedia(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .leave(lhsTheme): - if case let .leave(rhsTheme) = rhs, lhsTheme === rhsTheme { + case let .leave(lhsTheme, lhsText): + if case let .leave(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .convertToSupergroup(lhsTheme): - if case let .convertToSupergroup(rhsTheme) = rhs, lhsTheme === rhsTheme { + case let .convertToSupergroup(lhsTheme, lhsText): + if case let .convertToSupergroup(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .adminManagement(lhsTheme): - if case let .adminManagement(rhsTheme) = rhs, lhsTheme === rhsTheme { + case let .adminManagement(lhsTheme, lhsText): + if case let .adminManagement(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false @@ -208,14 +223,15 @@ private enum GroupInfoEntry: ItemListNodeEntry { } else { return false } - case let .notifications(lhsTheme, lhsSettings): - if case let .notifications(rhsTheme, rhsSettings) = rhs { + case let .notifications(lhsTheme, lhsTitle, lhsText): + if case let .notifications(rhsTheme, rhsTitle, rhsText) = rhs { if lhsTheme !== rhsTheme { return false } - if let lhsSettings = lhsSettings, let rhsSettings = rhsSettings { - return lhsSettings.isEqual(to: rhsSettings) - } else if (lhsSettings != nil) != (rhsSettings != nil) { + if lhsTitle != rhsTitle { + return false + } + if lhsText != rhsText { return false } return true @@ -228,47 +244,56 @@ private enum GroupInfoEntry: ItemListNodeEntry { } else { return false } - case let .groupTypeSetup(lhsTheme, lhsIsPublic): - if case let .groupTypeSetup(rhsTheme, rhsIsPublic) = rhs, lhsTheme == rhsTheme, lhsIsPublic == rhsIsPublic { + case let .preHistory(lhsTheme, lhsTitle, lhsValue): + if case let .preHistory(rhsTheme, rhsTitle, rhsValue) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsValue == rhsValue { return true } else { return false } - case let .groupDescriptionSetup(lhsTheme, lhsText): - if case let .groupDescriptionSetup(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + case let .groupTypeSetup(lhsTheme, lhsTitle, lhsText): + if case let .groupTypeSetup(rhsTheme, rhsTitle, rhsText) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsText == rhsText { return true } else { return false } - case let .groupManagementInfoLabel(lhsTheme, lhsText): - if case let .groupManagementInfoLabel(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + case let .groupDescriptionSetup(lhsTheme, lhsPlaceholder, lhsText): + if case let .groupDescriptionSetup(rhsTheme, rhsPlaceholder, rhsText) = rhs, lhsTheme === rhsTheme, lhsPlaceholder == rhsPlaceholder, lhsText == rhsText { return true } else { return false } - case let .membersAdmins(lhsTheme, lhsCount): - if case let .membersAdmins(rhsTheme, rhsCount) = rhs, lhsTheme === rhsTheme, lhsCount == rhsCount { + case let .groupManagementInfoLabel(lhsTheme, lhsTitle, lhsText): + if case let .groupManagementInfoLabel(rhsTheme, rhsTitle, rhsText) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsText == rhsText { return true } else { return false } - case let .membersBlacklist(lhsTheme, lhsCount): - if case let .membersBlacklist(rhsTheme, rhsCount) = rhs, lhsTheme === rhsTheme, lhsCount == rhsCount { + case let .membersAdmins(lhsTheme, lhsTitle, lhsText): + if case let .membersAdmins(rhsTheme, rhsTitle, rhsText) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsText == rhsText { return true } else { return false } - case let .addMember(lhsTheme, lhsEditing): - if case let .addMember(rhsTheme, rhsEditing) = rhs, lhsTheme === rhsTheme, lhsEditing == rhsEditing { + case let .membersBlacklist(lhsTheme, lhsTitle, lhsText): + if case let .membersBlacklist(rhsTheme, rhsTitle, rhsText) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsText == rhsText { return true } else { return false } - case let .member(lhsTheme, lhsIndex, lhsPeerId, lhsPeer, lhsPresence, lhsMemberStatus, lhsEditing, lhsEnabled): - if case let .member(rhsTheme, rhsIndex, rhsPeerId, rhsPeer, rhsPresence, rhsMemberStatus, rhsEditing, rhsEnabled) = rhs { + case let .addMember(lhsTheme, lhsTitle, lhsEditing): + if case let .addMember(rhsTheme, rhsTitle, rhsEditing) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsEditing == rhsEditing { + return true + } else { + return false + } + case let .member(lhsTheme, lhsStrings, lhsIndex, lhsPeerId, lhsPeer, lhsPresence, lhsMemberStatus, lhsEditing, lhsEnabled): + if case let .member(rhsTheme, rhsStrings, rhsIndex, rhsPeerId, rhsPeer, rhsPresence, rhsMemberStatus, rhsEditing, rhsEnabled) = rhs { if lhsTheme !== rhsTheme { return false } + if lhsStrings !== rhsStrings { + return false + } if lhsIndex != rhsIndex { return false } @@ -303,7 +328,7 @@ private enum GroupInfoEntry: ItemListNodeEntry { var stableId: GroupEntryStableId { switch self { - case let .member(_, _, peerId, _, _, _, _, _): + case let .member(_, _, _, peerId, _, _, _, _, _): return .peer(peerId) default: return .index(self.sortIndex) @@ -324,23 +349,25 @@ private enum GroupInfoEntry: ItemListNodeEntry { return 4 case .groupTypeSetup: return 5 - case .groupDescriptionSetup: + case .preHistory: return 6 - case .notifications: + case .groupDescriptionSetup: return 7 - case .notificationSound: + case .notifications: return 8 - case .sharedMedia: + case .notificationSound: return 9 - case .groupManagementInfoLabel: + case .sharedMedia: return 10 - case .membersAdmins: + case .groupManagementInfoLabel: return 11 - case .membersBlacklist: + case .membersAdmins: return 12 - case .addMember: + case .membersBlacklist: return 13 - case let .member(_, index, _, _, _, _, _, _): + case .addMember: + return 14 + case let .member(_, _, index, _, _, _, _, _, _): return 20 + index case .convertToSupergroup: return 100000 @@ -356,73 +383,76 @@ private enum GroupInfoEntry: ItemListNodeEntry { func item(_ arguments: GroupInfoArguments) -> ListViewItem { switch self { case let .info(theme, strings, peer, cachedData, state, updatingAvatar): - return ItemListAvatarAndNameInfoItem(account: arguments.account, theme: theme, strings: strings, peer: peer, presence: nil, cachedData: cachedData, state: state, sectionId: self.section, style: .blocks(withTopInset: false), editingNameUpdated: { editingName in + return ItemListAvatarAndNameInfoItem(account: arguments.account, theme: theme, strings: strings, mode: .generic, peer: peer, presence: nil, cachedData: cachedData, state: state, sectionId: self.section, style: .blocks(withTopInset: false), editingNameUpdated: { editingName in arguments.updateEditingName(editingName) }, avatarTapped: { arguments.tapAvatarAction() }, context: arguments.avatarAndNameInfoContext, updatingImage: updatingAvatar) - case let .setGroupPhoto(theme): - return ItemListActionItem(theme: theme, title: "Set Group Photo", kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { + case let .setGroupPhoto(theme, text): + return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.changeProfilePhoto() }) case let .about(theme, text): - return ItemListMultilineTextItem(theme: theme, text: text, sectionId: self.section, style: .blocks) + return ItemListMultilineTextItem(theme: theme, text: text, enabledEntitiyTypes: [.url, .mention, .hashtag], sectionId: self.section, style: .blocks, longTapAction: { + arguments.displayAboutContextMenu(text) + }, linkItemAction: { action, itemLink in + arguments.aboutLinkAction(action, itemLink) + }, tag: GroupInfoEntryTag.about) case let .link(theme, url): return ItemListActionItem(theme: theme, title: url, kind: .neutral, alignment: .natural, sectionId: self.section, style: .blocks, action: { + arguments.displayUsernameContextMenu(url) }) - case let .notifications(theme, settings): - let label: String - if let settings = settings as? TelegramPeerNotificationSettings, case .muted = settings.muteState { - label = "Disabled" - } else { - label = "Enabled" - } - return ItemListDisclosureItem(theme: theme, title: "Notifications", label: label, sectionId: self.section, style: .blocks, action: { + case let .notifications(theme, title, text): + return ItemListDisclosureItem(theme: theme, title: title, label: text, sectionId: self.section, style: .blocks, action: { arguments.changeNotificationMuteSettings() }) case let .notificationSound(theme, title, value): return ItemListDisclosureItem(theme: theme, title: title, label: value, sectionId: self.section, style: .blocks, action: { arguments.changeNotificationSoundSettings() }) - case let .sharedMedia(theme): - return ItemListDisclosureItem(theme: theme, title: "Shared Media", label: "", sectionId: self.section, style: .blocks, action: { + case let .preHistory(theme, title, value): + return ItemListSwitchItem(theme: theme, title: title, value: value, enabled: true, sectionId: self.section, style: .blocks, updated: { value in + arguments.togglePreHistory(value) + }) + case let .sharedMedia(theme, title): + return ItemListDisclosureItem(theme: theme, title: title, label: "", sectionId: self.section, style: .blocks, action: { arguments.openSharedMedia() }) - case let .adminManagement(theme): - return ItemListDisclosureItem(theme: theme, title: "Add Admins", label: "", sectionId: self.section, style: .blocks, action: { + case let .adminManagement(theme, title): + return ItemListDisclosureItem(theme: theme, title: title, label: "", sectionId: self.section, style: .blocks, action: { arguments.openAdminManagement() }) - case let .addMember(theme, editing): - return ItemListPeerActionItem(theme: theme, icon: PresentationResourcesItemList.plusIconImage(theme), title: "Add Member", sectionId: self.section, editing: editing, action: { + case let .addMember(theme, title, editing): + return ItemListPeerActionItem(theme: theme, icon: PresentationResourcesItemList.addPersonIcon(theme), title: title, sectionId: self.section, editing: editing, action: { arguments.addMember() }) - case let .groupTypeSetup(theme, isPublic): - return ItemListDisclosureItem(theme: theme, title: "Group Type", label: isPublic ? "Public" : "Private", sectionId: self.section, style: .blocks, action: { + case let .groupTypeSetup(theme, title, text): + return ItemListDisclosureItem(theme: theme, title: title, label: text, sectionId: self.section, style: .blocks, action: { arguments.presentController(channelVisibilityController(account: arguments.account, peerId: arguments.peerId, mode: .generic), ViewControllerPresentationArguments(presentationAnimation: ViewControllerPresentationAnimation.modalSheet)) }) - case let .groupDescriptionSetup(theme, text): - return ItemListMultilineInputItem(theme: theme, text: text, placeholder: "Group Description", sectionId: self.section, style: .blocks, textUpdated: { updatedText in + case let .groupDescriptionSetup(theme, placeholder, text): + return ItemListMultilineInputItem(theme: theme, text: text, placeholder: placeholder, maxLength: 1000, sectionId: self.section, style: .blocks, textUpdated: { updatedText in arguments.updateEditingDescriptionText(updatedText) }, action: { }) - case let .membersAdmins(theme, count): - return ItemListDisclosureItem(theme: theme, title: "Admins", label: "\(count)", sectionId: self.section, style: .blocks, action: { + case let .membersAdmins(theme, title, text): + return ItemListDisclosureItem(theme: theme, title: title, label: text, sectionId: self.section, style: .blocks, action: { arguments.pushController(channelAdminsController(account: arguments.account, peerId: arguments.peerId)) }) - case let .membersBlacklist(theme, count): - return ItemListDisclosureItem(theme: theme, title: "Blacklist", label: "\(count)", sectionId: self.section, style: .blocks, action: { + case let .membersBlacklist(theme, title, text): + return ItemListDisclosureItem(theme: theme, title: title, label: text, sectionId: self.section, style: .blocks, action: { arguments.pushController(channelBlacklistController(account: arguments.account, peerId: arguments.peerId)) }) - case let .member(theme, _, _, peer, presence, memberStatus, editing, enabled): + case let .member(theme, strings, _, _, peer, presence, memberStatus, editing, enabled): let label: String? switch memberStatus { case .admin: - label = "admin" + label = strings.ChatAdmins_AdminLabel case .member: label = nil } - return ItemListPeerItem(theme: theme, account: arguments.account, peer: peer, presence: presence, text: .presence, label: label == nil ? .none : .text(label!), editing: editing, switchValue: nil, enabled: enabled, sectionId: self.section, action: { + return ItemListPeerItem(theme: theme, strings: strings, account: arguments.account, peer: peer, presence: presence, text: .presence, label: label == nil ? .none : .text(label!), editing: editing, switchValue: nil, enabled: enabled, sectionId: self.section, action: { if let infoController = peerInfoController(account: arguments.account, peer: peer) { arguments.pushController(infoController) } @@ -431,12 +461,12 @@ private enum GroupInfoEntry: ItemListNodeEntry { }, removePeer: { peerId in arguments.removePeer(peerId) }) - case let .convertToSupergroup(theme): - return ItemListActionItem(theme: theme, title: "Convert to Supergroup", kind: .generic, alignment: .center, sectionId: self.section, style: .blocks, action: { + case let .convertToSupergroup(theme, title): + return ItemListActionItem(theme: theme, title: title, kind: .generic, alignment: .center, sectionId: self.section, style: .blocks, action: { arguments.convertToSupergroup() }) - case let .leave(theme): - return ItemListActionItem(theme: theme, title: "Delete and Exit", kind: .destructive, alignment: .center, sectionId: self.section, style: .blocks, action: { + case let .leave(theme, title): + return ItemListActionItem(theme: theme, title: title, kind: .destructive, alignment: .center, sectionId: self.section, style: .blocks, action: { arguments.leave() }) default: @@ -466,7 +496,7 @@ private struct TemporaryParticipant: Equatable { } private struct GroupInfoState: Equatable { - let updatingAvatar: TelegramMediaImageRepresentation? + let updatingAvatar: ItemListAvatarAndNameInfoItemUpdatingAvatar? let editingState: GroupInfoEditingState? let updatingName: ItemListAvatarAndNameInfoItemName? let peerIdWithRevealedOptions: PeerId? @@ -505,7 +535,7 @@ private struct GroupInfoState: Equatable { return true } - func withUpdatedUpdatingAvatar(_ updatingAvatar: TelegramMediaImageRepresentation?) -> GroupInfoState { + func withUpdatedUpdatingAvatar(_ updatingAvatar: ItemListAvatarAndNameInfoItemUpdatingAvatar?) -> GroupInfoState { return GroupInfoState(updatingAvatar: updatingAvatar, editingState: self.editingState, updatingName: self.updatingName, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, temporaryParticipants: self.temporaryParticipants, successfullyAddedParticipantIds: self.successfullyAddedParticipantIds, removingParticipantIds: self.removingParticipantIds, savingData: self.savingData) } @@ -618,35 +648,45 @@ private func groupInfoEntries(account: Account, presentationData: PresentationDa } if let peer = peerViewMainPeer(view) { - let infoState = ItemListAvatarAndNameInfoItemState(editingName: canEditGroupInfo ? nil : state.editingState?.editingName, updatingName: state.updatingName) + let infoState = ItemListAvatarAndNameInfoItemState(editingName: canEditGroupInfo ? state.editingState?.editingName : nil, updatingName: state.updatingName) entries.append(.info(presentationData.theme, presentationData.strings, peer: peer, cachedData: view.cachedData, state: infoState, updatingAvatar: state.updatingAvatar)) } if canEditGroupInfo { - entries.append(GroupInfoEntry.setGroupPhoto(presentationData.theme)) + entries.append(GroupInfoEntry.setGroupPhoto(presentationData.theme, presentationData.strings.GroupInfo_SetGroupPhoto)) } let peerNotificationSettings: TelegramPeerNotificationSettings = (view.notificationSettings as? TelegramPeerNotificationSettings) ?? TelegramPeerNotificationSettings.defaultSettings + let notificationsText: String + switch peerNotificationSettings.muteState { + case .muted: + notificationsText = presentationData.strings.UserInfo_NotificationsDisabled + case .unmuted: + notificationsText = presentationData.strings.UserInfo_NotificationsEnabled + } if let editingState = state.editingState { if let group = view.peers[view.peerId] as? TelegramGroup, case .creator = group.role { - entries.append(.adminManagement(presentationData.theme)) + entries.append(.adminManagement(presentationData.theme, presentationData.strings.GroupInfo_ChatAdmins)) } else if let cachedChannelData = view.cachedData as? CachedChannelData { if isCreator { - entries.append(GroupInfoEntry.groupTypeSetup(presentationData.theme, isPublic: isPublic)) + entries.append(GroupInfoEntry.groupTypeSetup(presentationData.theme, presentationData.strings.GroupInfo_GroupType, isPublic ? presentationData.strings.Channel_Setup_TypePublic : presentationData.strings.Channel_Setup_TypePrivate)) + if !isPublic, let cachedData = view.cachedData as? CachedChannelData { + entries.append(GroupInfoEntry.preHistory(presentationData.theme, "Group History For New Members", cachedData.flags.contains(.preHistoryEnabled))) + } } if canEditGroupInfo { - entries.append(GroupInfoEntry.groupDescriptionSetup(presentationData.theme, text: editingState.editingDescriptionText)) + entries.append(GroupInfoEntry.groupDescriptionSetup(presentationData.theme, presentationData.strings.Channel_Edit_AboutItem, editingState.editingDescriptionText)) } - entries.append(GroupInfoEntry.notifications(presentationData.theme, settings: view.notificationSettings)) + entries.append(GroupInfoEntry.notifications(presentationData.theme, presentationData.strings.GroupInfo_Notifications, notificationsText)) entries.append(GroupInfoEntry.notificationSound(presentationData.theme, presentationData.strings.GroupInfo_Sound, localizedPeerNotificationSoundString(strings: presentationData.strings, sound: peerNotificationSettings.messageSound, default: globalNotificationSettings.effective.groupChats.sound))) if let adminCount = cachedChannelData.participantsSummary.adminCount { - entries.append(GroupInfoEntry.membersAdmins(presentationData.theme, count: Int(adminCount))) + entries.append(GroupInfoEntry.membersAdmins(presentationData.theme, presentationData.strings.Channel_Info_Management, "\(adminCount)")) } if let bannedCount = cachedChannelData.participantsSummary.bannedCount { - entries.append(GroupInfoEntry.membersBlacklist(presentationData.theme, count: Int(bannedCount))) + entries.append(GroupInfoEntry.membersBlacklist(presentationData.theme, presentationData.strings.Channel_Info_Banned, "\(bannedCount)")) } } } else { @@ -659,8 +699,8 @@ private func groupInfoEntries(account: Account, presentationData: PresentationDa } } - entries.append(GroupInfoEntry.notifications(presentationData.theme, settings: view.notificationSettings)) - entries.append(GroupInfoEntry.sharedMedia(presentationData.theme)) + entries.append(GroupInfoEntry.notifications(presentationData.theme, presentationData.strings.GroupInfo_Notifications, notificationsText)) + entries.append(GroupInfoEntry.sharedMedia(presentationData.theme, presentationData.strings.GroupInfo_SharedMedia)) } var canRemoveAnyMember = false @@ -681,7 +721,7 @@ private func groupInfoEntries(account: Account, presentationData: PresentationDa } if canAddMembers { - entries.append(GroupInfoEntry.addMember(presentationData.theme, editing: state.editingState != nil && canRemoveAnyMember)) + entries.append(GroupInfoEntry.addMember(presentationData.theme, presentationData.strings.GroupInfo_AddParticipant, editing: state.editingState != nil && canRemoveAnyMember)) } if let cachedGroupData = view.cachedData as? CachedGroupData, let participants = cachedGroupData.participants { @@ -771,7 +811,7 @@ private func groupInfoEntries(account: Account, presentationData: PresentationDa } else { memberStatus = .member } - entries.append(GroupInfoEntry.member(presentationData.theme, index: i, peerId: peer.id, peer: peer, presence: peerPresences[peer.id], memberStatus: memberStatus, editing: ItemListPeerItemEditing(editable: canRemoveParticipant(account: account, isAdmin: canEditMembers, participantId: peer.id, invitedBy: sortedParticipants[i].invitedBy), editing: state.editingState != nil && canRemoveAnyMember, revealed: state.peerIdWithRevealedOptions == peer.id), enabled: !disabledPeerIds.contains(peer.id))) + entries.append(GroupInfoEntry.member(presentationData.theme, presentationData.strings, index: i, peerId: peer.id, peer: peer, presence: peerPresences[peer.id], memberStatus: memberStatus, editing: ItemListPeerItemEditing(editable: canRemoveParticipant(account: account, isAdmin: canEditMembers, participantId: peer.id, invitedBy: sortedParticipants[i].invitedBy), editing: state.editingState != nil && canRemoveAnyMember, revealed: state.peerIdWithRevealedOptions == peer.id), enabled: !disabledPeerIds.contains(peer.id))) } } } else if let cachedChannelData = view.cachedData as? CachedChannelData, let participants = cachedChannelData.topParticipants { @@ -844,7 +884,7 @@ private func groupInfoEntries(account: Account, presentationData: PresentationDa } else { memberStatus = .member } - entries.append(GroupInfoEntry.member(presentationData.theme, index: i, peerId: peer.id, peer: peer, presence: peerPresences[peer.id], memberStatus: memberStatus, editing: ItemListPeerItemEditing(editable: canRemoveParticipant(account: account, isAdmin: canEditMembers, participantId: peer.id, invitedBy: nil), editing: state.editingState != nil && canRemoveAnyMember, revealed: state.peerIdWithRevealedOptions == peer.id), enabled: !disabledPeerIds.contains(peer.id))) + entries.append(GroupInfoEntry.member(presentationData.theme, presentationData.strings, index: i, peerId: peer.id, peer: peer, presence: peerPresences[peer.id], memberStatus: memberStatus, editing: ItemListPeerItemEditing(editable: canRemoveParticipant(account: account, isAdmin: canEditMembers, participantId: peer.id, invitedBy: nil), editing: state.editingState != nil && canRemoveAnyMember, revealed: state.peerIdWithRevealedOptions == peer.id), enabled: !disabledPeerIds.contains(peer.id))) } } } @@ -852,13 +892,13 @@ private func groupInfoEntries(account: Account, presentationData: PresentationDa if let group = view.peers[view.peerId] as? TelegramGroup { if case .Member = group.membership { if case .creator = group.role, state.editingState != nil { - entries.append(.convertToSupergroup(presentationData.theme)) + entries.append(.convertToSupergroup(presentationData.theme, presentationData.strings.GroupInfo_ConvertToSupergroup)) } - entries.append(.leave(presentationData.theme)) + entries.append(.leave(presentationData.theme, presentationData.strings.GroupInfo_DeleteAndExit)) } } else if let channel = view.peers[view.peerId] as? TelegramChannel { - if case .member = channel.participationStatus { - entries.append(.leave(presentationData.theme)) + if case .member = channel.participationStatus, let cachedChannelData = view.cachedData as? CachedChannelData, let memberCount = cachedChannelData.participantsSummary.memberCount, memberCount <= 200 { + entries.append(.leave(presentationData.theme, presentationData.strings.GroupInfo_DeleteAndExit)) } } @@ -936,10 +976,19 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl actionsDisposable.add(updateAvatarDisposable) let currentAvatarMixin = Atomic(value: nil) + let updatePreHistoryDisposable = MetaDisposable() + actionsDisposable.add(updatePreHistoryDisposable) + + let navigateDisposable = MetaDisposable() + actionsDisposable.add(navigateDisposable) + var avatarGalleryTransitionArguments: ((AvatarGalleryEntry) -> GalleryTransitionArguments?)? let avatarAndNameInfoContext = ItemListAvatarAndNameInfoItemContext() var updateHiddenAvatarImpl: (() -> Void)? + var displayAboutContextMenuImpl: ((String) -> Void)? + var aboutLinkActionImpl: ((TextLinkItemActionType, TextLinkItem) -> Void)? + let arguments = GroupInfoArguments(account: account, peerId: peerId, avatarAndNameInfoContext: avatarAndNameInfoContext, tapAvatarAction: { let _ = (account.postbox.loadedPeerWithId(peerId) |> take(1) |> deliverOnMainQueue).start(next: { peer in if peer.profileImageRepresentations.isEmpty { @@ -958,29 +1007,62 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl })) }) }, changeProfilePhoto: { - /*let emptyController = LegacyEmptyController() - let navigationController = makeLegacyNavigationController(rootController: emptyController) - navigationController.setNavigationBarHidden(true, animated: false) - navigationController.navigationBar.transform = CGAffineTransform(translationX: -1000.0, y: 0.0) - - let legacyController = LegacyController(presentation: .custom, legacyController: navigationController) - - presentControllerImpl?(legacyController, nil) - let mixin = TGMediaAvatarMenuMixin(context: LegacyControllerContext(controller: nil), parentController: emptyController, hasDeleteButton: false, personalPhoto: true, saveEditedPhotos: false, saveCapturedMedia: false)! - let _ = currentAvatarMixin.swap(mixin) - mixin.didDismiss = { [weak legacyController] in - legacyController?.dismiss() - } - mixin.didFinishWithImage = { image in - if let image = image { - if let data = UIImageJPEGRepresentation(image, 0.6) { - let resource = LocalFileMediaResource(fileId: arc4random64()) - account.postbox.mediaBox.storeResourceData(resource.id, data: data) - let representation = TelegramMediaImageRepresentation(dimensions: CGSize(width: 640.0, height: 640.0), resource: resource) - updateState { - $0.withUpdatedUpdatingAvatar(representation) + let _ = (account.postbox.modify { modifier -> Peer? in + return modifier.getPeer(peerId) + } |> deliverOnMainQueue).start(next: { peer in + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + + let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme) + legacyController.statusBar.statusBarStyle = .Ignore + + let emptyController = LegacyEmptyController(context: legacyController.context)! + let navigationController = makeLegacyNavigationController(rootController: emptyController) + navigationController.setNavigationBarHidden(true, animated: false) + navigationController.navigationBar.transform = CGAffineTransform(translationX: -1000.0, y: 0.0) + + legacyController.bind(controller: navigationController) + + presentControllerImpl?(legacyController, nil) + + var hasPhotos = false + if let peer = peer, !peer.profileImageRepresentations.isEmpty { + hasPhotos = true + } + + let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasDeleteButton: hasPhotos, personalPhoto: true, saveEditedPhotos: false, saveCapturedMedia: false)! + let _ = currentAvatarMixin.swap(mixin) + mixin.didFinishWithImage = { image in + if let image = image { + if let data = UIImageJPEGRepresentation(image, 0.6) { + let resource = LocalFileMediaResource(fileId: arc4random64()) + account.postbox.mediaBox.storeResourceData(resource.id, data: data) + let representation = TelegramMediaImageRepresentation(dimensions: CGSize(width: 640.0, height: 640.0), resource: resource) + updateState { + $0.withUpdatedUpdatingAvatar(.image(representation)) + } + updateAvatarDisposable.set((updatePeerPhoto(account: account, peerId: peerId, resource: resource) |> deliverOnMainQueue).start(next: { result in + switch result { + case .complete: + updateState { + $0.withUpdatedUpdatingAvatar(nil) + } + case .progress: + break + } + })) + } } - updateAvatarDisposable.set((updatePeerPhoto(account: account, peerId: peerId, resource: resource) |> deliverOnMainQueue).start(next: { result in + } + mixin.didFinishWithDelete = { + let _ = currentAvatarMixin.swap(nil) + updateState { + if let profileImage = peer?.smallProfileImage { + return $0.withUpdatedUpdatingAvatar(.image(profileImage)) + } else { + return $0.withUpdatedUpdatingAvatar(.none) + } + } + updateAvatarDisposable.set((updatePeerPhoto(account: account, peerId: peerId, resource: nil) |> deliverOnMainQueue).start(next: { result in switch result { case .complete: updateState { @@ -991,13 +1073,17 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl } })) } - } - } - mixin.didDismiss = { [weak legacyController] in - let _ = currentAvatarMixin.swap(nil) - legacyController?.dismiss() - } - mixin.present()*/ + mixin.didDismiss = { [weak legacyController] in + let _ = currentAvatarMixin.swap(nil) + legacyController?.dismiss() + } + let menuController = mixin.present() + if let menuController = menuController { + menuController.customRemoveFromParentViewController = { [weak legacyController] in + legacyController?.dismiss() + } + } + }) }, pushController: { controller in pushControllerImpl?(controller) }, presentController: { controller, presentationArguments in @@ -1056,6 +1142,8 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl }) presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) }) + }, togglePreHistory: { value in + updatePreHistoryDisposable.set(updateChannelHistoryAvailabilitySettingsInteractively(postbox: account.postbox, network: account.network, peerId: peerId, historyAvailableForNewMembers: value).start()) }, openSharedMedia: { if let controller = peerSharedMediaController(account: account, peerId: peerId) { pushControllerImpl?(controller) @@ -1086,117 +1174,117 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl } } }, addMember: { - var confirmationImpl: ((PeerId) -> Signal)? - let contactsController = ContactSelectionController(account: account, title: { $0.GroupInfo_AddParticipantTitle }, confirmation: { peerId in - if let confirmationImpl = confirmationImpl { - return confirmationImpl(peerId) - } else { - return .single(false) - } - }) - confirmationImpl = { [weak contactsController] peerId in - return account.postbox.loadedPeerWithId(peerId) - |> deliverOnMainQueue - |> mapToSignal { peer in - let result = ValuePromise() - if let contactsController = contactsController { - let alertController = standardTextAlertController(title: nil, text: "Add \(peer.displayTitle)?", actions: [ - TextAlertAction(type: .genericAction, title: "Cancel", action: { - result.set(false) - }), - TextAlertAction(type: .defaultAction, title: "OK", action: { - result.set(true) - }) - ]) - contactsController.present(alertController, in: .window(.root)) - } + let _ = (account.postbox.loadedPeerWithId(peerId) + |> deliverOnMainQueue).start(next: { groupPeer in + var confirmationImpl: ((PeerId) -> Signal)? + var options: [ContactListAdditionalOption] = [] + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + var inviteByLinkImpl: (() -> Void)? + options.append(ContactListAdditionalOption(title: presentationData.strings.GroupInfo_InviteByLink, icon: generateTintedImage(image: UIImage(bundleImageName: "Contact List/LinkActionIcon"), color: presentationData.theme.list.itemAccentColor), action: { + inviteByLinkImpl?() + })) - return result.get() - } - } - let addMember = contactsController.result - |> deliverOnMainQueue - |> mapToSignal { memberId -> Signal in - if let memberId = memberId { - return account.postbox.peerView(id: memberId) - |> take(1) - |> deliverOnMainQueue - |> mapToSignal { view -> Signal in - if let peer = view.peers[memberId] { - updateState { state in - var found = false - for participant in state.temporaryParticipants { - if participant.peer.id == memberId { - found = true - break - } - } - if !found { - let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) - var temporaryParticipants = state.temporaryParticipants - temporaryParticipants.append(TemporaryParticipant(peer: peer, presence: view.peerPresences[memberId], timestamp: timestamp)) - return state.withUpdatedTemporaryParticipants(temporaryParticipants) - } else { - return state - } - } - } - - return addPeerMember(account: account, peerId: peerId, memberId: memberId) + let contactsController = ContactSelectionController(account: account, title: { $0.GroupInfo_AddParticipantTitle }, options: options, confirmation: { peerId in + if let confirmationImpl = confirmationImpl { + return confirmationImpl(peerId) + } else { + return .single(false) + } + }) + confirmationImpl = { [weak contactsController] peerId in + return account.postbox.loadedPeerWithId(peerId) + |> deliverOnMainQueue + |> mapToSignal { peer in + let result = ValuePromise() + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + if let contactsController = contactsController { + let alertController = standardTextAlertController(title: nil, text: presentationData.strings.GroupInfo_AddParticipantConfirmation(peer.displayTitle).0, actions: [ + TextAlertAction(type: .genericAction, title: presentationData.strings.Common_No, action: { + result.set(false) + }), + TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Yes, action: { + result.set(true) + }) + ]) + contactsController.present(alertController, in: .window(.root)) + } + + return result.get() + } + } + let addMember = contactsController.result + |> deliverOnMainQueue + |> mapToSignal { memberId -> Signal in + if let memberId = memberId { + return account.postbox.peerView(id: memberId) + |> take(1) |> deliverOnMainQueue - |> afterCompleted { - updateState { state in - var successfullyAddedParticipantIds = state.successfullyAddedParticipantIds - successfullyAddedParticipantIds.insert(memberId) - - return state.withUpdatedSuccessfullyAddedParticipantIds(successfullyAddedParticipantIds) - } - } |> `catch` { _ -> Signal in - updateState { state in - var temporaryParticipants = state.temporaryParticipants - for i in 0 ..< temporaryParticipants.count { - if temporaryParticipants[i].peer.id == memberId { - temporaryParticipants.remove(at: i) - break + |> mapToSignal { view -> Signal in + if let peer = view.peers[memberId] { + updateState { state in + var found = false + for participant in state.temporaryParticipants { + if participant.peer.id == memberId { + found = true + break + } + } + if !found { + let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) + var temporaryParticipants = state.temporaryParticipants + temporaryParticipants.append(TemporaryParticipant(peer: peer, presence: view.peerPresences[memberId], timestamp: timestamp)) + return state.withUpdatedTemporaryParticipants(temporaryParticipants) + } else { + return state } } - var successfullyAddedParticipantIds = state.successfullyAddedParticipantIds - successfullyAddedParticipantIds.remove(memberId) - - return state.withUpdatedTemporaryParticipants(temporaryParticipants).withUpdatedSuccessfullyAddedParticipantIds(successfullyAddedParticipantIds) } - return .complete() + return addPeerMember(account: account, peerId: peerId, memberId: memberId) + |> deliverOnMainQueue + |> afterCompleted { + updateState { state in + var successfullyAddedParticipantIds = state.successfullyAddedParticipantIds + successfullyAddedParticipantIds.insert(memberId) + + return state.withUpdatedSuccessfullyAddedParticipantIds(successfullyAddedParticipantIds) + } + } |> `catch` { _ -> Signal in + updateState { state in + var temporaryParticipants = state.temporaryParticipants + for i in 0 ..< temporaryParticipants.count { + if temporaryParticipants[i].peer.id == memberId { + temporaryParticipants.remove(at: i) + break + } + } + var successfullyAddedParticipantIds = state.successfullyAddedParticipantIds + successfullyAddedParticipantIds.remove(memberId) + + return state.withUpdatedTemporaryParticipants(temporaryParticipants).withUpdatedSuccessfullyAddedParticipantIds(successfullyAddedParticipantIds) + } + + return .complete() + } } + } else { + return .complete() } - } else { - return .complete() + } + inviteByLinkImpl = { [weak contactsController] in + contactsController?.dismiss() + + presentControllerImpl?(channelVisibilityController(account: account, peerId: peerId, mode: .privateLink), ViewControllerPresentationArguments(presentationAnimation: ViewControllerPresentationAnimation.modalSheet)) } - } - presentControllerImpl?(contactsController, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) - addMemberDisposable.set(addMember.start()) + presentControllerImpl?(contactsController, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + addMemberDisposable.set(addMember.start()) + }) }, removePeer: { memberId in let signal = account.postbox.loadedPeerWithId(memberId) |> deliverOnMainQueue |> mapToSignal { peer -> Signal in let result = ValuePromise() - - let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } - - let actionSheet = ActionSheetController(presentationTheme: presentationData.theme) - actionSheet.setItemGroups([ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: "Remove \(peer.displayTitle)?", color: .destructive, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - - result.set(true) - }) - ]), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ])]) - presentControllerImpl?(actionSheet, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) - + result.set(true) return result.get() } |> mapToSignal { value -> Signal in @@ -1241,17 +1329,6 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } - let notificationAction: (Int32) -> Void = { muteUntil in - let muteState: PeerMuteState - if muteUntil <= 0 { - muteState = .unmuted - } else if muteUntil == Int32.max { - muteState = .muted(until: Int32.max) - } else { - muteState = .muted(until: Int32(Date().timeIntervalSince1970) + muteUntil) - } - changeMuteSettingsDisposable.set(changePeerNotificationSettings(account: account, peerId: peerId, settings: TelegramPeerNotificationSettings(muteState: muteState, messageSound: PeerMessageSound.bundledModern(id: 0))).start()) - } controller.setItemGroups([ ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: presentationData.strings.DialogList_DeleteConversationConfirmation, color: .destructive, action: { @@ -1265,6 +1342,13 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) ]) presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + }, displayUsernameContextMenu: { text in + let shareController = ShareController(account: account, subject: .url(text)) + presentControllerImpl?(shareController, nil) + }, displayAboutContextMenu: { text in + displayAboutContextMenuImpl?(text) + }, aboutLinkAction: { action, itemLink in + aboutLinkActionImpl?(action, itemLink) }) let globalNotificationsKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.globalNotifications])) @@ -1338,7 +1422,7 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Edit, style: .regular, enabled: true, action: { if let peer = peer as? TelegramGroup { updateState { state in - return state.withUpdatedEditingState(GroupInfoEditingState(editingName: ItemListAvatarAndNameInfoItemName(peer.indexName), editingDescriptionText: "")) + return state.withUpdatedEditingState(GroupInfoEditingState(editingName: ItemListAvatarAndNameInfoItemName(peer), editingDescriptionText: "")) } } else if let channel = peer as? TelegramChannel, case .group = channel.info { var text = "" @@ -1346,7 +1430,7 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl text = about } updateState { state in - return state.withUpdatedEditingState(GroupInfoEditingState(editingName: ItemListAvatarAndNameInfoItemName(channel.indexName), editingDescriptionText: text)) + return state.withUpdatedEditingState(GroupInfoEditingState(editingName: ItemListAvatarAndNameInfoItemName(channel), editingDescriptionText: text)) } } }) @@ -1371,6 +1455,43 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl popToRootImpl = { [weak controller] in (controller?.navigationController as? NavigationController)?.popToRoot(animated: true) } + displayAboutContextMenuImpl = { [weak controller] text in + if let strongController = controller { + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + var resultItemNode: ListViewItemNode? + let _ = strongController.frameForItemNode({ itemNode in + if let itemNode = itemNode as? ItemListMultilineTextItemNode { + if let tag = itemNode.tag as? GroupInfoEntryTag { + if tag == .about { + resultItemNode = itemNode + return true + } + } + } + return false + }) + if let resultItemNode = resultItemNode { + let contextMenuController = ContextMenuController(actions: [ContextMenuAction(content: .text(presentationData.strings.Conversation_ContextMenuCopy), action: { + UIPasteboard.general.string = text + })]) + strongController.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak resultItemNode] in + if let resultItemNode = resultItemNode { + return (resultItemNode, resultItemNode.contentBounds.insetBy(dx: 0.0, dy: -2.0)) + } else { + return nil + } + })) + + } + } + } + + aboutLinkActionImpl = { [weak controller] action, itemLink in + if let controller = controller { + handlePeerInfoAboutTextAction(account: account, navigateDisposable: navigateDisposable, controller: controller, action: action, itemLink: itemLink) + } + } + avatarGalleryTransitionArguments = { [weak controller] entry in if let controller = controller { var result: (ASDisplayNode, CGRect)? @@ -1397,3 +1518,130 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl } return controller } + +func handlePeerInfoAboutTextAction(account: Account, navigateDisposable: MetaDisposable, controller: ViewController, action: TextLinkItemActionType, itemLink: TextLinkItem) { + let openPeerImpl: (PeerId) -> Void = { [weak controller] peerId in + let peerSignal: Signal + peerSignal = account.postbox.loadedPeerWithId(peerId) |> map { Optional($0) } + navigateDisposable.set((peerSignal |> take(1) |> deliverOnMainQueue).start(next: { peer in + if let controller = controller, let peer = peer { + if let infoController = peerInfoController(account: account, peer: peer) { + (controller.navigationController as? NavigationController)?.pushViewController(infoController) + } + } + })) + } + + let openLinkImpl: (String) -> Void = { [weak controller] url in + navigateDisposable.set((resolveUrl(account: account, url: url) |> deliverOnMainQueue).start(next: { result in + if let controller = controller { + switch result { + case let .externalUrl(url): + account.telegramApplicationContext.applicationBindings.openUrl(url) + case let .peer(peerId): + openPeerImpl(peerId) + case let .channelMessage(peerId, messageId): + if let navigationController = controller.navigationController as? NavigationController { + navigateToChatController(navigationController: navigationController, account: account, chatLocation: .peer(peerId), messageId: messageId) + } + case let .stickerPack(name): + controller.present(StickerPackPreviewController(account: account, stickerPack: .name(name)), in: .window(.root)) + case let .instantView(webpage, anchor): + (controller.navigationController as? NavigationController)?.pushViewController(InstantPageController(account: account, webPage: webpage, anchor: anchor)) + case let .join(link): + controller.present(JoinLinkPreviewController(account: account, link: link, navigateToPeer: { peerId in + openPeerImpl(peerId) + }), in: .window(.root)) + default: + break + } + } + })) + } + + let openPeerMentionImpl: (String) -> Void = { [weak controller] mention in + navigateDisposable.set((resolvePeerByName(account: account, name: mention, ageLimit: 10) |> take(1) |> deliverOnMainQueue).start(next: { peerId in + if let controller = controller, let peerId = peerId { + (controller.navigationController as? NavigationController)?.pushViewController(ChatController(account: account, chatLocation: .peer(peerId), messageId: nil)) + } + })) + } + + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + switch action { + case .tap: + switch itemLink { + case let .url(url): + openLinkImpl(url) + case let .mention(mention): + openPeerMentionImpl(mention) + case let .hashtag(peerName, hashtag): + let searchController = HashtagSearchController(account: account, peerName: peerName, query: hashtag) + (controller.navigationController as? NavigationController)?.pushViewController(searchController) + } + case .longTap: + switch itemLink { + case let .url(url): + let actionSheet = ActionSheetController(presentationTheme: presentationData.theme) + actionSheet.setItemGroups([ActionSheetItemGroup(items: [ + ActionSheetTextItem(title: url), + ActionSheetButtonItem(title: presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + openLinkImpl(url) + }), + ActionSheetButtonItem(title: presentationData.strings.ShareMenu_CopyShareLink, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + UIPasteboard.general.string = url + }), + ActionSheetButtonItem(title: presentationData.strings.Conversation_AddToReadingList, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + if let link = URL(string: url) { + let _ = try? SSReadingList.default()?.addItem(with: link, title: nil, previewText: nil) + } + }) + ]), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + controller.present(actionSheet, in: .window(.root)) + case let .mention(mention): + let actionSheet = ActionSheetController(presentationTheme: presentationData.theme) + actionSheet.setItemGroups([ActionSheetItemGroup(items: [ + ActionSheetTextItem(title: mention), + ActionSheetButtonItem(title: presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + openPeerMentionImpl(mention) + }), + ActionSheetButtonItem(title: presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + UIPasteboard.general.string = mention + }) + ]), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + controller.present(actionSheet, in: .window(.root)) + case let .hashtag(peerName, hashtag): + let actionSheet = ActionSheetController(presentationTheme: presentationData.theme) + actionSheet.setItemGroups([ActionSheetItemGroup(items: [ + ActionSheetTextItem(title: hashtag), + ActionSheetButtonItem(title: presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + let searchController = HashtagSearchController(account: account, peerName: peerName, query: hashtag) + (controller.navigationController as? NavigationController)?.pushViewController(searchController) + }), + ActionSheetButtonItem(title: presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + UIPasteboard.general.string = hashtag + }) + ]), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + controller.present(actionSheet, in: .window(.root)) + } + } +} diff --git a/TelegramUI/GroupsInCommonController.swift b/TelegramUI/GroupsInCommonController.swift index 2fecbb449f..1b476f56bd 100644 --- a/TelegramUI/GroupsInCommonController.swift +++ b/TelegramUI/GroupsInCommonController.swift @@ -137,7 +137,7 @@ public func groupsInCommonController(account: Account, peerId: PeerId) -> ViewCo var pushControllerImpl: ((ViewController) -> Void)? let arguments = GroupsInCommonControllerArguments(account: account, openPeer: { memberId in - pushControllerImpl?(ChatController(account: account, peerId: memberId)) + pushControllerImpl?(ChatController(account: account, chatLocation: .peer(memberId))) }) let peersSignal: Signal<[Peer]?, NoError> = .single(nil) |> then(groupsInCommon(account: account, peerId: peerId) |> mapToSignal { peerIds -> Signal<[Peer], NoError> in @@ -162,7 +162,7 @@ public func groupsInCommonController(account: Account, peerId: PeerId) -> ViewCo |> map { presentationData, state, peers -> (ItemListControllerState, (ItemListNodeState, GroupsInCommonEntry.ItemGenerationArguments)) in var emptyStateItem: ItemListControllerEmptyStateItem? if peers == nil { - emptyStateItem = ItemListLoadingIndicatorEmptyStateItem() + emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme) } let previous = previousPeers diff --git a/TelegramUI/HapticFeedback.swift b/TelegramUI/HapticFeedback.swift index d7e3786586..5c54daa9fb 100644 --- a/TelegramUI/HapticFeedback.swift +++ b/TelegramUI/HapticFeedback.swift @@ -3,7 +3,7 @@ import UIKit @available(iOSApplicationExtension 10.0, *) private final class HapticFeedbackImpl { - private lazy var impactGenerator = { UIImpactFeedbackGenerator(style: .light) }() + private lazy var impactGenerator = { UIImpactFeedbackGenerator(style: .medium) }() private lazy var selectionGenerator = { UISelectionFeedbackGenerator() }() private lazy var notificationGenerator = { UINotificationFeedbackGenerator() }() @@ -15,6 +15,14 @@ private final class HapticFeedbackImpl { self.selectionGenerator.selectionChanged() } + func prepareImpact() { + self.impactGenerator.prepare() + } + + func impact() { + self.impactGenerator.impactOccurred() + } + func success() { self.notificationGenerator.notificationOccurred(.success) } @@ -65,6 +73,22 @@ final class HapticFeedback { } } + func prepareImpact() { + if #available(iOSApplicationExtension 10.0, *) { + self.withImpl { impl in + impl.prepareImpact() + } + } + } + + func impact() { + if #available(iOSApplicationExtension 10.0, *) { + self.withImpl { impl in + impl.impact() + } + } + } + func success() { if #available(iOSApplicationExtension 10.0, *) { self.withImpl { impl in diff --git a/TelegramUI/HashtagChatInputContextPanelNode.swift b/TelegramUI/HashtagChatInputContextPanelNode.swift index 9f2841f7af..4e0238bdfe 100644 --- a/TelegramUI/HashtagChatInputContextPanelNode.swift +++ b/TelegramUI/HashtagChatInputContextPanelNode.swift @@ -18,6 +18,7 @@ private struct HashtagChatInputContextPanelEntryStableId: Hashable { private struct HashtagChatInputContextPanelEntry: Comparable, Identifiable { let index: Int + let theme: PresentationTheme let text: String var stableId: HashtagChatInputContextPanelEntryStableId { @@ -25,7 +26,7 @@ private struct HashtagChatInputContextPanelEntry: Comparable, Identifiable { } static func ==(lhs: HashtagChatInputContextPanelEntry, rhs: HashtagChatInputContextPanelEntry) -> Bool { - return lhs.index == rhs.index && lhs.text == rhs.text + return lhs.index == rhs.index && lhs.text == rhs.text && lhs.theme === rhs.theme } static func <(lhs: HashtagChatInputContextPanelEntry, rhs: HashtagChatInputContextPanelEntry) -> Bool { @@ -33,7 +34,7 @@ private struct HashtagChatInputContextPanelEntry: Comparable, Identifiable { } func item(account: Account, hashtagSelected: @escaping (String) -> Void) -> ListViewItem { - return HashtagChatInputPanelItem(text: self.text, hashtagSelected: hashtagSelected) + return HashtagChatInputPanelItem(theme: self.theme, text: self.text, hashtagSelected: hashtagSelected) } } @@ -54,20 +55,24 @@ private func preparedTransition(from fromEntries: [HashtagChatInputContextPanelE } final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode { + private var theme: PresentationTheme + private let listView: ListView private var currentEntries: [HashtagChatInputContextPanelEntry]? private var enqueuedTransitions: [(HashtagChatInputContextPanelTransition, Bool)] = [] - private var hasValidLayout = false + private var validLayout: (CGSize, CGFloat, CGFloat)? - override init(account: Account) { + override init(account: Account, theme: PresentationTheme, strings: PresentationStrings) { + self.theme = theme + self.listView = ListView() self.listView.isOpaque = false self.listView.stackFromBottom = true - self.listView.keepBottomItemOverscrollBackground = .white + self.listView.keepBottomItemOverscrollBackground = theme.list.plainBackgroundColor self.listView.limitHitTestToNodes = true - super.init(account: account) + super.init(account: account, theme: theme, strings: strings) self.isOpaque = false self.clipsToBounds = true @@ -80,7 +85,7 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode { var index = 0 var stableIds = Set() for text in results { - let entry = HashtagChatInputContextPanelEntry(index: index, text: text) + let entry = HashtagChatInputContextPanelEntry(index: index, theme: self.theme, text: text) if stableIds.contains(entry.stableId) { continue } @@ -93,7 +98,15 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode { let transition = preparedTransition(from: self.currentEntries ?? [], to: entries, account: self.account, hashtagSelected: { [weak self] text in if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction { interfaceInteraction.updateTextInputState { textInputState in - if let (range, type, _) = textInputStateContextQueryRangeAndType(textInputState) { + var hashtagQueryRange: Range? + inner: for (range, type, _) in textInputStateContextQueryRangeAndType(textInputState) { + if type == [.hashtag] { + hashtagQueryRange = range + break inner + } + } + + if let range = hashtagQueryRange { var inputText = textInputState.inputText let replacementText = text + " " @@ -121,7 +134,7 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode { private func enqueueTransition(_ transition: HashtagChatInputContextPanelTransition, firstTime: Bool) { enqueuedTransitions.append((transition, firstTime)) - if self.hasValidLayout { + if self.validLayout != nil { while !self.enqueuedTransitions.isEmpty { self.dequeueTransition() } @@ -129,7 +142,7 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode { } private func dequeueTransition() { - if let (transition, firstTime) = self.enqueuedTransitions.first { + if let validLayout = self.validLayout, let (transition, firstTime) = self.enqueuedTransitions.first { self.enqueuedTransitions.remove(at: 0) var options = ListViewDeleteAndInsertOptions() @@ -142,9 +155,11 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode { } var insets = UIEdgeInsets() - insets.top = topInsetForLayout(size: self.listView.bounds.size) + insets.top = topInsetForLayout(size: validLayout.0) + insets.left = validLayout.1 + insets.right = validLayout.2 - let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: self.listView.bounds.size, insets: insets, duration: 0.0, curve: .Default) + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: validLayout.0, insets: insets, duration: 0.0, curve: .Default) self.listView.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: updateSizeAndInsets, updateOpaqueState: nil, completion: { [weak self] _ in if let strongSelf = self, firstTime { @@ -170,9 +185,14 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode { return max(size.height - minimumItemHeights, 0.0) } - override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) { + override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) { + let hadValidLayout = self.validLayout != nil + self.validLayout = (size, leftInset, rightInset) + var insets = UIEdgeInsets() insets.top = self.topInsetForLayout(size: size) + insets.left = leftInset + insets.right = rightInset transition.updateFrame(node: self.listView, frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)) @@ -184,10 +204,10 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode { case let .animated(animationDuration, animationCurve): duration = animationDuration switch animationCurve { - case .easeInOut: - break - case .spring: - curve = 7 + case .easeInOut: + break + case .spring: + curve = 7 } } @@ -202,8 +222,7 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode { self.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) - if !hasValidLayout { - hasValidLayout = true + if !hadValidLayout { while !self.enqueuedTransitions.isEmpty { self.dequeueTransition() } diff --git a/TelegramUI/HashtagChatInputPanelItem.swift b/TelegramUI/HashtagChatInputPanelItem.swift index d5de1de1fd..9ddfea3cd0 100644 --- a/TelegramUI/HashtagChatInputPanelItem.swift +++ b/TelegramUI/HashtagChatInputPanelItem.swift @@ -6,23 +6,25 @@ import SwiftSignalKit import Postbox final class HashtagChatInputPanelItem: ListViewItem { + fileprivate let theme: PresentationTheme fileprivate let text: String private let hashtagSelected: (String) -> Void let selectable: Bool = true - public init(text: String, hashtagSelected: @escaping (String) -> Void) { + public init(theme: PresentationTheme, text: String, hashtagSelected: @escaping (String) -> Void) { + self.theme = theme self.text = text self.hashtagSelected = hashtagSelected } - public func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { let configure = { () -> Void in let node = HashtagChatInputPanelItemNode() let nodeLayout = node.asyncLayout() let (top, bottom) = (previousItem != nil, nextItem != nil) - let (layout, apply) = nodeLayout(self, width, top, bottom) + let (layout, apply) = nodeLayout(self, params, top, bottom) node.contentSize = layout.contentSize node.insets = layout.insets @@ -40,7 +42,7 @@ final class HashtagChatInputPanelItem: ListViewItem { } } - public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { if let node = node as? HashtagChatInputPanelItemNode { Queue.mainQueue().async { let nodeLayout = node.asyncLayout() @@ -48,7 +50,7 @@ final class HashtagChatInputPanelItem: ListViewItem { async { let (top, bottom) = (previousItem != nil, nextItem != nil) - let (layout, apply) = nodeLayout(self, width, top, bottom) + let (layout, apply) = nodeLayout(self, params, top, bottom) Queue.mainQueue().async { completion(layout, { apply(animation) @@ -79,65 +81,68 @@ final class HashtagChatInputPanelItemNode: ListViewItemNode { self.textNode = TextNode() self.topSeparatorNode = ASDisplayNode() - self.topSeparatorNode.backgroundColor = UIColor(rgb: 0xC9CDD1) self.topSeparatorNode.isLayerBacked = true self.separatorNode = ASDisplayNode() - self.separatorNode.backgroundColor = UIColor(rgb: 0xD6D6DA) self.separatorNode.isLayerBacked = true self.highlightedBackgroundNode = ASDisplayNode() - self.highlightedBackgroundNode.backgroundColor = UIColor(rgb: 0xd9d9d9) self.highlightedBackgroundNode.isLayerBacked = true super.init(layerBacked: false, dynamicBounce: false) - self.backgroundColor = .white self.addSubnode(self.topSeparatorNode) self.addSubnode(self.separatorNode) self.addSubnode(self.textNode) } - override public func layoutForWidth(_ width: CGFloat, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { + override public func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { if let item = item as? HashtagChatInputPanelItem { let doLayout = self.asyncLayout() let merged = (top: previousItem != nil, bottom: nextItem != nil) - let (layout, apply) = doLayout(item, width, merged.top, merged.bottom) + let (layout, apply) = doLayout(item, params, merged.top, merged.bottom) self.contentSize = layout.contentSize self.insets = layout.insets apply(.None) } } - func asyncLayout() -> (_ item: HashtagChatInputPanelItem, _ width: CGFloat, _ mergedTop: Bool, _ mergedBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { + func asyncLayout() -> (_ item: HashtagChatInputPanelItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { let makeTextLayout = TextNode.asyncLayout(self.textNode) - return { [weak self] item, width, mergedTop, mergedBottom in - let leftInset: CGFloat = 15.0 - let rightInset: CGFloat = 10.0 + return { [weak self] item, params, mergedTop, mergedBottom in + let baseWidth = params.width - params.leftInset - params.rightInset - let (textLayout, textApply) = makeTextLayout(NSAttributedString(string: "#\(item.text)", font: textFont, textColor: .black), nil, 1, .end, CGSize(width: width - leftInset - rightInset, height: 100.0), .natural, nil, UIEdgeInsets()) + let leftInset: CGFloat = 15.0 + params.leftInset + let rightInset: CGFloat = 10.0 + params.rightInset - let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: width, height: HashtagChatInputPanelItemNode.itemHeight), insets: UIEdgeInsets()) + let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "#\(item.text)", font: textFont, textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: HashtagChatInputPanelItemNode.itemHeight), insets: UIEdgeInsets()) return (nodeLayout, { _ in if let strongSelf = self { + strongSelf.separatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor + strongSelf.topSeparatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor + strongSelf.backgroundColor = item.theme.list.plainBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor + let _ = textApply() strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floor((nodeLayout.contentSize.height - textLayout.size.height) / 2.0)), size: textLayout.size) strongSelf.topSeparatorNode.isHidden = mergedTop strongSelf.separatorNode.isHidden = !mergedBottom - strongSelf.topSeparatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: UIScreenPixel)) - strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: nodeLayout.contentSize.height - UIScreenPixel), size: CGSize(width: width - leftInset, height: UIScreenPixel)) + strongSelf.topSeparatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.width, height: UIScreenPixel)) + strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: nodeLayout.contentSize.height - UIScreenPixel), size: CGSize(width: params.width - leftInset, height: UIScreenPixel)) - strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: nodeLayout.size.height + UIScreenPixel)) + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.width, height: nodeLayout.size.height + UIScreenPixel)) } }) } } - override func setHighlighted(_ highlighted: Bool, animated: Bool) { - super.setHighlighted(highlighted, animated: animated) + override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { + super.setHighlighted(highlighted, at: point, animated: animated) if highlighted { self.highlightedBackgroundNode.alpha = 1.0 diff --git a/TelegramUI/HashtagSearchController.swift b/TelegramUI/HashtagSearchController.swift index 3469993b53..8afeb2ceeb 100644 --- a/TelegramUI/HashtagSearchController.swift +++ b/TelegramUI/HashtagSearchController.swift @@ -22,7 +22,9 @@ final class HashtagSearchController: TelegramController { self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } - super.init(account: account) + super.init(account: account, navigationBarTheme: NavigationBarTheme(rootControllerTheme: self.presentationData.theme), enableMediaAccessoryPanel: true) + + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style if let peerName = peerName { self.title = query + "@" + peerName @@ -39,10 +41,18 @@ final class HashtagSearchController: TelegramController { peerId = .single(nil) } + let chatListPresentationData = ChatListPresentationData(theme: self.presentationData.theme, strings: self.presentationData.strings, timeFormat: self.presentationData.timeFormat) let foundMessages: Signal<[ChatListSearchEntry], NoError> = peerId |> mapToSignal { peerId -> Signal<[ChatListSearchEntry], NoError> in - return searchMessages(account: account, peerId: peerId, query: query) - |> map { return $0.map({ .message($0, defaultPresentationTheme, defaultPresentationStrings) }) } + let location: SearchMessagesLocation + if let peerId = peerId { + location = .peer(peerId: peerId, fromId: nil, tags: nil) + } else { + location = .general + } + let search = searchMessages(account: account, location: location, query: query) + return search + |> map { return $0.map({ .message($0, chatListPresentationData) }) } } let interaction = ChatListNodeInteraction(activateSearch: { @@ -53,16 +63,18 @@ final class HashtagSearchController: TelegramController { if let peer = message.peers[message.id.peerId] { strongSelf.openMessageFromSearchDisposable.set((storedMessageFromSearchPeer(account: strongSelf.account, peer: peer) |> deliverOnMainQueue).start(completed: { if let strongSelf = self { - (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, peerId: message.id.peerId, messageId: message.id)) + (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, chatLocation: .peer(message.id.peerId), messageId: message.id)) } })) } strongSelf.controllerNode.listNode.clearHighlightAnimated(true) } + }, groupSelected: { _ in }, setPeerIdWithRevealedOptions: { _, _ in - }, setPeerPinned: { _, _ in + }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, deletePeer: { _ in + }, updatePeerGrouping: { _, _ in }) let previousSearchItems = Atomic<[ChatListSearchEntry]?>(value: nil) @@ -71,7 +83,7 @@ final class HashtagSearchController: TelegramController { let previousEntries = previousSearchItems.swap(entries) let firstTime = previousEntries == nil - let transition = chatListSearchContainerPreparedTransition(from: previousEntries ?? [], to: entries ?? [], displayingResults: entries != nil, account: account, enableHeaders: false, interaction: interaction) + let transition = chatListSearchContainerPreparedTransition(from: previousEntries ?? [], to: entries, displayingResults: true, account: account, enableHeaders: false, onlyWriteable: false, interaction: interaction) strongSelf.controllerNode.enqueueTransition(transition, firstTime: firstTime) } }) @@ -87,7 +99,7 @@ final class HashtagSearchController: TelegramController { } override func loadDisplayNode() { - self.displayNode = HashtagSearchControllerNode(account: self.account) + self.displayNode = HashtagSearchControllerNode(account: self.account, theme: self.presentationData.theme) self.displayNodeDidLoad() } diff --git a/TelegramUI/HashtagSearchControllerNode.swift b/TelegramUI/HashtagSearchControllerNode.swift index 1b964bcfd6..cebfd08b99 100644 --- a/TelegramUI/HashtagSearchControllerNode.swift +++ b/TelegramUI/HashtagSearchControllerNode.swift @@ -15,7 +15,7 @@ final class HashtagSearchControllerNode: ASDisplayNode { var navigationBar: NavigationBar? - init(account: Account) { + init(account: Account, theme: PresentationTheme) { self.account = account self.listNode = ListView() @@ -25,7 +25,7 @@ final class HashtagSearchControllerNode: ASDisplayNode { return UITracingLayerView() }) - self.backgroundColor = .white + self.backgroundColor = theme.chatList.backgroundColor self.listNode.isHidden = true self.addSubnode(self.listNode) diff --git a/TelegramUI/HorizontalListContextResultsChatInputContextPanelNode.swift b/TelegramUI/HorizontalListContextResultsChatInputContextPanelNode.swift index bba51a1756..46cfeca3a3 100644 --- a/TelegramUI/HorizontalListContextResultsChatInputContextPanelNode.swift +++ b/TelegramUI/HorizontalListContextResultsChatInputContextPanelNode.swift @@ -54,6 +54,8 @@ private func preparedTransition(from fromEntries: [HorizontalListContextResultsC } final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputContextPanelNode { + private var theme: PresentationTheme + private let listView: ListView private let separatorNode: ASDisplayNode private var currentResults: ChatContextResultCollection? @@ -62,19 +64,21 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont private var enqueuedTransitions: [(HorizontalListContextResultsChatInputContextPanelTransition, Bool)] = [] private var hasValidLayout = false - override init(account: Account) { + override init(account: Account, theme: PresentationTheme, strings: PresentationStrings) { + self.theme = theme + self.separatorNode = ASDisplayNode() self.separatorNode.isLayerBacked = true - self.separatorNode.backgroundColor = UIColor(rgb: 0xbdc2c7) + self.separatorNode.backgroundColor = theme.list.itemPlainSeparatorColor self.separatorNode.isHidden = true self.listView = ListView() self.listView.isOpaque = true - self.listView.backgroundColor = .white - self.listView.transform = CATransform3DMakeRotation(-CGFloat(M_PI / 2.0), 0.0, 0.0, 1.0) + self.listView.backgroundColor = theme.list.plainBackgroundColor + self.listView.transform = CATransform3DMakeRotation(-CGFloat(CGFloat.pi / 2.0), 0.0, 0.0, 1.0) self.listView.isHidden = true - super.init(account: account) + super.init(account: account, theme: theme, strings: strings) self.isOpaque = false self.clipsToBounds = true @@ -83,6 +87,12 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont self.addSubnode(self.separatorNode) } + override func didLoad() { + super.didLoad() + + self.listView.view.disablesInteractiveTransitionGestureRecognizer = true + } + func updateResults(_ results: ChatContextResultCollection) { self.currentResults = results var entries: [HorizontalListContextResultsChatInputContextPanelEntry] = [] @@ -146,7 +156,7 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont } } - override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) { + override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) { let listHeight: CGFloat = 105.0 transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: size.height - listHeight), size: CGSize(width: size.width, height: UIScreenPixel))) @@ -156,7 +166,9 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont transition.updatePosition(node: self.listView, position: CGPoint(x: size.width / 2.0, y: size.height - listHeight / 2.0)) - let insets = UIEdgeInsets() + var insets = UIEdgeInsets() + insets.top = leftInset + insets.bottom = rightInset var duration: Double = 0.0 var curve: UInt = 0 switch transition { @@ -165,10 +177,10 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont case let .animated(animationDuration, animationCurve): duration = animationDuration switch animationCurve { - case .easeInOut: - break - case .spring: - curve = 7 + case .easeInOut: + break + case .spring: + curve = 7 } } diff --git a/TelegramUI/HorizontalListContextResultsChatInputPanelItem.swift b/TelegramUI/HorizontalListContextResultsChatInputPanelItem.swift index e398c04958..3e90fb42e7 100644 --- a/TelegramUI/HorizontalListContextResultsChatInputPanelItem.swift +++ b/TelegramUI/HorizontalListContextResultsChatInputPanelItem.swift @@ -18,13 +18,13 @@ final class HorizontalListContextResultsChatInputPanelItem: ListViewItem { self.resultSelected = resultSelected } - public func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { let configure = { () -> Void in let node = HorizontalListContextResultsChatInputPanelItemNode() let nodeLayout = node.asyncLayout() let (top, bottom) = (previousItem != nil, nextItem != nil) - let (layout, apply) = nodeLayout(self, width, top, bottom) + let (layout, apply) = nodeLayout(self, params, top, bottom) node.contentSize = layout.contentSize node.insets = layout.insets @@ -42,7 +42,7 @@ final class HorizontalListContextResultsChatInputPanelItem: ListViewItem { } } - public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { if let node = node as? HorizontalListContextResultsChatInputPanelItemNode { Queue.mainQueue().async { let nodeLayout = node.asyncLayout() @@ -50,7 +50,7 @@ final class HorizontalListContextResultsChatInputPanelItem: ListViewItem { async { let (top, bottom) = (previousItem != nil, nextItem != nil) - let (layout, apply) = nodeLayout(self, width, top, bottom) + let (layout, apply) = nodeLayout(self, params, top, bottom) Queue.mainQueue().async { completion(layout, { apply(animation) @@ -76,9 +76,63 @@ private let iconTextBackgroundImage = generateStretchableFilledCircleImage(radiu final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode { private let imageNodeBackground: ASDisplayNode private let imageNode: TransformImageNode - private let videoNode: ManagedVideoNode + private var videoLayer: (SoftwareVideoThumbnailLayer, SoftwareVideoLayerFrameManager, SampleBufferLayer)? private var currentImageResource: TelegramMediaResource? - private var currentVideoResource: TelegramMediaResource? + private var currentVideoFile: TelegramMediaFile? + + override var visibility: ListViewItemNodeVisibility { + didSet { + switch visibility { + case .visible: + self.ticking = true + default: + self.ticking = false + } + } + } + + private let timebase: CMTimebase + + private var displayLink: CADisplayLink? + private var ticking: Bool = false { + didSet { + if self.ticking != oldValue { + if self.ticking { + class DisplayLinkProxy: NSObject { + weak var target: HorizontalListContextResultsChatInputPanelItemNode? + init(target: HorizontalListContextResultsChatInputPanelItemNode) { + self.target = target + } + + @objc func displayLinkEvent() { + self.target?.displayLinkEvent() + } + } + + let displayLink = CADisplayLink(target: DisplayLinkProxy(target: self), selector: #selector(DisplayLinkProxy.displayLinkEvent)) + self.displayLink = displayLink + displayLink.add(to: RunLoop.main, forMode: RunLoopMode.commonModes) + if #available(iOS 10.0, *) { + displayLink.preferredFramesPerSecond = 25 + } else { + displayLink.frameInterval = 2 + } + displayLink.isPaused = false + CMTimebaseSetRate(self.timebase, 1.0) + } else if let displayLink = self.displayLink { + self.displayLink = nil + displayLink.isPaused = true + displayLink.invalidate() + CMTimebaseSetRate(self.timebase, 0.0) + } + } + } + } + + private func displayLinkEvent() { + let timestamp = CMTimebaseGetTime(self.timebase).seconds + self.videoLayer?.1.tick(timestamp: timestamp) + } init() { self.imageNodeBackground = ASDisplayNode() @@ -86,58 +140,69 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode self.imageNodeBackground.backgroundColor = UIColor(white: 0.9, alpha: 1.0) self.imageNode = TransformImageNode() + self.imageNode.contentAnimations = [.subsequentUpdates] self.imageNode.isLayerBacked = true self.imageNode.displaysAsynchronously = false - self.videoNode = ManagedVideoNode() + var timebase: CMTimebase? + CMTimebaseCreateWithMasterClock(nil, CMClockGetHostTimeClock(), &timebase) + CMTimebaseSetRate(timebase!, 0.0) + self.timebase = timebase! super.init(layerBacked: false, dynamicBounce: false) - self.backgroundColor = .white - self.addSubnode(self.imageNodeBackground) - self.imageNode.transform = CATransform3DMakeRotation(CGFloat(M_PI / 2.0), 0.0, 0.0, 1.0) - self.imageNode.alphaTransitionOnFirstUpdate = true + self.imageNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) + self.imageNode.contentAnimations = [.firstUpdate, .subsequentUpdates] self.addSubnode(self.imageNode) - - self.videoNode.transform = CATransform3DMakeRotation(CGFloat(M_PI / 2.0), 0.0, 0.0, 1.0) - self.videoNode.clipsToBounds = true - self.addSubnode(self.videoNode) } - override public func layoutForWidth(_ width: CGFloat, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { + deinit { + if let displayLink = self.displayLink { + displayLink.isPaused = true + displayLink.invalidate() + } + } + + override public func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { if let item = item as? HorizontalListContextResultsChatInputPanelItem { let doLayout = self.asyncLayout() let merged = (top: previousItem != nil, bottom: nextItem != nil) - let (layout, apply) = doLayout(item, width, merged.top, merged.bottom) + let (layout, apply) = doLayout(item, params, merged.top, merged.bottom) self.contentSize = layout.contentSize self.insets = layout.insets apply(.None) } } - func asyncLayout() -> (_ item: HorizontalListContextResultsChatInputPanelItem, _ width: CGFloat, _ mergedTop: Bool, _ mergedBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { + func asyncLayout() -> (_ item: HorizontalListContextResultsChatInputPanelItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { let imageLayout = self.imageNode.asyncLayout() let currentImageResource = self.currentImageResource - let currentVideoResource = self.currentVideoResource + let currentVideoFile = self.currentVideoFile - return { [weak self] item, height, mergedTop, mergedBottom in + return { [weak self] item, params, mergedTop, mergedBottom in + let height = params.width + let sideInset: CGFloat = 4.0 var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? var imageResource: TelegramMediaResource? - var videoResource: TelegramMediaResource? + var videoFile: TelegramMediaFile? var imageDimensions: CGSize? switch item.result { - case let .externalReference(_, _, title, _, url, thumbnailUrl, contentUrl, _, dimensions, _, _): + case let .externalReference(_, type, title, _, url, thumbnailUrl, contentUrl, _, dimensions, _, _): if let contentUrl = contentUrl { imageResource = HttpReferenceMediaResource(url: contentUrl, size: nil) } else if let thumbnailUrl = thumbnailUrl { imageResource = HttpReferenceMediaResource(url: thumbnailUrl, size: nil) } imageDimensions = dimensions + if type == "gif", let contentUrl = contentUrl, let thumbnailResource = imageResource, let dimensions = dimensions { + videoFile = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), resource: HttpReferenceMediaResource(url: contentUrl, size: nil), previewRepresentations: [TelegramMediaImageRepresentation(dimensions: dimensions, resource: thumbnailResource)], mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [])]) + imageResource = nil + } case let .internalReference(_, _, title, _, image, file, _): if let image = image { if let largestRepresentation = largestImageRepresentation(image.representations) { @@ -155,7 +220,8 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode if let file = file { if file.isVideo && file.isAnimated { - videoResource = file.resource + videoFile = file + imageResource = nil } } } @@ -170,11 +236,9 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode croppedImageDimensions = fittedImageDimensions.cropped(CGSize(width: floor(height * 4.0 / 3.0), height: 1000.0)) var imageApply: (() -> Void)? - var transformArguments: TransformImageArguments? - if let imageResource = imageResource { + if let _ = imageResource { let imageCorners = ImageCorners() let arguments = TransformImageArguments(corners: imageCorners, imageSize: fittedImageDimensions, boundingSize: croppedImageDimensions, intrinsicInsets: UIEdgeInsets()) - transformArguments = arguments imageApply = imageLayout(arguments) } @@ -187,21 +251,21 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode updatedImageResource = true } - var updatedVideoResource = false - if let currentVideoResource = currentVideoResource, let videoResource = videoResource { - if !currentVideoResource.isEqual(to: videoResource) { - updatedVideoResource = true + var updatedVideoFile = false + if let currentVideoFile = currentVideoFile, let videoFile = videoFile { + if !currentVideoFile.isEqual(videoFile) { + updatedVideoFile = true } - } else if (currentVideoResource != nil) != (videoResource != nil) { - updatedVideoResource = true + } else if (currentVideoFile != nil) != (videoFile != nil) { + updatedVideoFile = true } if updatedImageResource { if let imageResource = imageResource { let tmpRepresentation = TelegramMediaImageRepresentation(dimensions: CGSize(width: fittedImageDimensions.width * 2.0, height: fittedImageDimensions.height * 2.0), resource: imageResource) - let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [tmpRepresentation]) + let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [tmpRepresentation], reference: nil) //updateImageSignal = chatWebpageSnippetPhoto(account: item.account, photo: tmpImage) - updateImageSignal = chatMessagePhoto(account: item.account, photo: tmpImage) + updateImageSignal = chatMessagePhoto(postbox: item.account.postbox, photo: tmpImage) } else { updateImageSignal = .complete() } @@ -212,34 +276,56 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode return (nodeLayout, { _ in if let strongSelf = self { strongSelf.currentImageResource = imageResource - strongSelf.currentVideoResource = videoResource + strongSelf.currentVideoFile = videoFile if let imageApply = imageApply { if let updateImageSignal = updateImageSignal { - strongSelf.imageNode.setSignal(account: item.account, signal: updateImageSignal) + strongSelf.imageNode.setSignal(updateImageSignal) } strongSelf.imageNode.bounds = CGRect(origin: CGPoint(), size: CGSize(width: croppedImageDimensions.width, height: croppedImageDimensions.height)) strongSelf.imageNode.position = CGPoint(x: height / 2.0, y: (nodeLayout.contentSize.height - sideInset) / 2.0 + sideInset) - strongSelf.videoNode.bounds = CGRect(origin: CGPoint(), size: CGSize(width: croppedImageDimensions.width, height: croppedImageDimensions.height)) - strongSelf.videoNode.position = CGPoint(x: height / 2.0, y: (nodeLayout.contentSize.height - sideInset) / 2.0 + sideInset) - strongSelf.imageNodeBackground.frame = CGRect(origin: CGPoint(x: sideInset, y: sideInset), size: CGSize(width: croppedImageDimensions.height, height: croppedImageDimensions.width)) + imageApply() + } - if updatedVideoResource { - if let videoResource = videoResource { - if let applicationContext = item.account.applicationContext as? TelegramApplicationContext { - strongSelf.videoNode.acquireContext(account: item.account, mediaManager: applicationContext.mediaManager, id: ChatContextResultManagedMediaId(result: item.result), resource: videoResource, priority: 1) - } - } else { - strongSelf.videoNode.clearContext() - } + if updatedVideoFile { + if let (thumbnailLayer, _, layer) = strongSelf.videoLayer { + strongSelf.videoLayer = nil + thumbnailLayer.removeFromSuperlayer() + layer.layer.removeFromSuperlayer() } - imageApply() - - strongSelf.videoNode.transformArguments = transformArguments + if let videoFile = videoFile { + let thumbnailLayer = SoftwareVideoThumbnailLayer(account: item.account, file: videoFile) + thumbnailLayer.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) + strongSelf.layer.addSublayer(thumbnailLayer) + let layerHolder = takeSampleBufferLayer() + layerHolder.layer.videoGravity = AVLayerVideoGravity.resizeAspectFill + layerHolder.layer.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) + strongSelf.layer.addSublayer(layerHolder.layer) + let manager = SoftwareVideoLayerFrameManager(account: item.account, resource: videoFile.resource, layerHolder: layerHolder) + strongSelf.videoLayer = (thumbnailLayer, manager, layerHolder) + thumbnailLayer.ready = { [weak thumbnailLayer, weak manager] in + if let strongSelf = self, let thumbnailLayer = thumbnailLayer, let manager = manager { + if strongSelf.videoLayer?.0 === thumbnailLayer && strongSelf.videoLayer?.1 === manager { + manager.start() + } + } + } + + /*if let applicationContext = item.account.applicationContext as? TelegramApplicationContext { + strongSelf.videoNode.acquireContext(account: item.account, mediaManager: applicationContext.mediaManager, id: ChatContextResultManagedMediaId(result: item.result), resource: videoResource, priority: 1) + }*/ + } + } + + if let (thumbnailLayer, _, layer) = strongSelf.videoLayer { + thumbnailLayer.bounds = CGRect(origin: CGPoint(), size: CGSize(width: croppedImageDimensions.width, height: croppedImageDimensions.height)) + thumbnailLayer.position = CGPoint(x: height / 2.0, y: (nodeLayout.contentSize.height - sideInset) / 2.0 + sideInset) + layer.layer.bounds = CGRect(origin: CGPoint(), size: CGSize(width: croppedImageDimensions.width, height: croppedImageDimensions.height)) + layer.layer.position = CGPoint(x: height / 2.0, y: (nodeLayout.contentSize.height - sideInset) / 2.0 + sideInset) } } }) diff --git a/TelegramUI/HorizontalPeerItem.swift b/TelegramUI/HorizontalPeerItem.swift index c5c1edf6cc..acad4e7220 100644 --- a/TelegramUI/HorizontalPeerItem.swift +++ b/TelegramUI/HorizontalPeerItem.swift @@ -17,25 +17,27 @@ final class HorizontalPeerItem: ListViewItem { let account: Account let peer: Peer let action: (Peer) -> Void + let longTapAction: (Peer) -> Void let isPeerSelected: (PeerId) -> Bool let customWidth: CGFloat? - init(theme: PresentationTheme, strings: PresentationStrings, mode: HorizontalPeerItemMode, account: Account, peer: Peer, action: @escaping (Peer) -> Void, isPeerSelected: @escaping (PeerId) -> Bool, customWidth: CGFloat?) { + init(theme: PresentationTheme, strings: PresentationStrings, mode: HorizontalPeerItemMode, account: Account, peer: Peer, action: @escaping (Peer) -> Void, longTapAction: @escaping (Peer) -> Void, isPeerSelected: @escaping (PeerId) -> Bool, customWidth: CGFloat?) { self.theme = theme self.strings = strings self.mode = mode self.account = account self.peer = peer self.action = action + self.longTapAction = longTapAction self.isPeerSelected = isPeerSelected self.customWidth = customWidth } - func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { async { let node = HorizontalPeerItemNode() - let (nodeLayout, apply) = node.asyncLayout()(self, width) + let (nodeLayout, apply) = node.asyncLayout()(self, params) node.insets = nodeLayout.insets node.contentSize = nodeLayout.contentSize @@ -48,13 +50,13 @@ final class HorizontalPeerItem: ListViewItem { } } - func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { assert(node is HorizontalPeerItemNode) if let node = node as? HorizontalPeerItemNode { Queue.mainQueue().async { let layout = node.asyncLayout() async { - let (nodeLayout, apply) = layout(self, width) + let (nodeLayout, apply) = layout(self, params) Queue.mainQueue().async { completion(nodeLayout, { apply(animation.isAnimated) @@ -82,6 +84,11 @@ final class HorizontalPeerItemNode: ListViewItemNode { item.action(item.peer) } } + self.peerNode.longTapAction = { [weak self] in + if let item = self?.item { + item.longTapAction(item.peer) + } + } } override func didLoad() { @@ -90,22 +97,23 @@ final class HorizontalPeerItemNode: ListViewItemNode { self.layer.sublayerTransform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) } - func asyncLayout() -> (HorizontalPeerItem, CGFloat) -> (ListViewItemNodeLayout, (Bool) -> Void) { - return { [weak self] item, width in + func asyncLayout() -> (HorizontalPeerItem, ListViewItemLayoutParams) -> (ListViewItemNodeLayout, (Bool) -> Void) { + return { [weak self] item, params in let itemLayout = ListViewItemNodeLayout(contentSize: CGSize(width: 92.0, height: item.customWidth ?? 80.0), insets: UIEdgeInsets()) + + let itemTheme: SelectablePeerNodeTheme + switch item.mode { + case .list: + itemTheme = SelectablePeerNodeTheme(textColor: item.theme.list.itemPrimaryTextColor, secretTextColor: .green, selectedTextColor: item.theme.list.itemAccentColor, checkBackgroundColor: item.theme.list.plainBackgroundColor, checkFillColor: item.theme.list.itemAccentColor, checkColor: item.theme.list.plainBackgroundColor) + case .actionSheet: + itemTheme = SelectablePeerNodeTheme(textColor: item.theme.actionSheet.primaryTextColor, secretTextColor: .green, selectedTextColor: item.theme.actionSheet.controlAccentColor, checkBackgroundColor: item.theme.actionSheet.opaqueItemBackgroundColor, checkFillColor: item.theme.actionSheet.controlAccentColor, checkColor: item.theme.actionSheet.opaqueItemBackgroundColor) + } + return (itemLayout, { animated in - let textColor: UIColor - switch item.mode { - case .list: - textColor = item.theme.list.itemPrimaryTextColor - case .actionSheet: - textColor = .black - } - if let strongSelf = self { strongSelf.item = item - strongSelf.peerNode.textColor = textColor - strongSelf.peerNode.setup(account: item.account, peer: item.peer, chatPeer: nil, numberOfLines: 1) + strongSelf.peerNode.theme = itemTheme + strongSelf.peerNode.setup(account: item.account, strings: item.strings, peer: item.peer, chatPeer: nil, numberOfLines: 1) strongSelf.peerNode.frame = CGRect(origin: CGPoint(), size: itemLayout.size) strongSelf.peerNode.updateSelection(selected: item.isPeerSelected(item.peer.id), animated: false) } @@ -118,5 +126,23 @@ final class HorizontalPeerItemNode: ListViewItemNode { self.peerNode.updateSelection(selected: item.isPeerSelected(item.peer.id), animated: animated) } } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + super.animateInsertion(currentTimestamp, duration: duration, short: short) + + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + super.animateRemoved(currentTimestamp, duration: duration) + + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + } + + override func animateAdded(_ currentTimestamp: Double, duration: Double) { + super.animateAdded(currentTimestamp, duration: duration) + + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } } diff --git a/TelegramUI/HorizontalStickerGridItem.swift b/TelegramUI/HorizontalStickerGridItem.swift index 491381f458..513de8ff16 100644 --- a/TelegramUI/HorizontalStickerGridItem.swift +++ b/TelegramUI/HorizontalStickerGridItem.swift @@ -8,19 +8,21 @@ import Postbox final class HorizontalStickerGridItem: GridItem { let account: Account let file: TelegramMediaFile + let stickersInteraction: HorizontalStickersChatContextPanelInteraction let interfaceInteraction: ChatPanelInterfaceInteraction let section: GridSection? = nil - init(account: Account, file: TelegramMediaFile, interfaceInteraction: ChatPanelInterfaceInteraction) { + init(account: Account, file: TelegramMediaFile, stickersInteraction: HorizontalStickersChatContextPanelInteraction, interfaceInteraction: ChatPanelInterfaceInteraction) { self.account = account self.file = file + self.stickersInteraction = stickersInteraction self.interfaceInteraction = interfaceInteraction } func node(layout: GridNodeLayout) -> GridItemNode { let node = HorizontalStickerGridItemNode() - node.setup(account: self.account, file: self.file) + node.setup(account: self.account, item: self) node.interfaceInteraction = self.interfaceInteraction return node } @@ -30,25 +32,35 @@ final class HorizontalStickerGridItem: GridItem { assertionFailure() return } - node.setup(account: self.account, file: self.file) + node.setup(account: self.account, item: self) node.interfaceInteraction = self.interfaceInteraction } } final class HorizontalStickerGridItemNode: GridItemNode { - private var currentState: (Account, TelegramMediaFile, CGSize)? + private var currentState: (Account, HorizontalStickerGridItem, CGSize)? private let imageNode: TransformImageNode private let stickerFetchedDisposable = MetaDisposable() var interfaceInteraction: ChatPanelInterfaceInteraction? + private var currentIsPreviewing: Bool = false + + var stickerItem: StickerPackItem? { + if let (_, item, _) = self.currentState { + return StickerPackItem(index: ItemCollectionItemIndex(index: 0, id: 0), file: item.file, indexKeys: []) + } else { + return nil + } + } + override init() { self.imageNode = TransformImageNode() super.init() - self.imageNode.transform = CATransform3DMakeRotation(CGFloat(M_PI / 2.0), 0.0, 0.0, 1.0) + self.imageNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) self.addSubnode(self.imageNode) } @@ -62,19 +74,18 @@ final class HorizontalStickerGridItemNode: GridItemNode { self.imageNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.imageNodeTap(_:)))) } - func setup(account: Account, file: TelegramMediaFile) { - if self.currentState == nil || self.currentState!.0 !== account || self.currentState!.1.id != file.id { - if let dimensions = file.dimensions { - self.imageNode.setSignal(account: account, signal: chatMessageSticker(account: account, file: file, small: true)) - self.stickerFetchedDisposable.set(freeMediaFileInteractiveFetched(account: account, file: file).start()) + func setup(account: Account, item: HorizontalStickerGridItem) { + if self.currentState == nil || self.currentState!.0 !== account || self.currentState!.1.file.id != item.file.id { + if let dimensions = item.file.dimensions { + self.imageNode.setSignal(chatMessageSticker(account: account, file: item.file, small: true)) + self.stickerFetchedDisposable.set(freeMediaFileInteractiveFetched(account: account, file: item.file).start()) - self.currentState = (account, file, dimensions) + self.currentState = (account, item, dimensions) self.setNeedsLayout() } } - //self.updateSelectionState(animated: false) - //self.updateHiddenMedia() + self.updatePreviewing(animated: false) } override func layout() { @@ -92,20 +103,35 @@ final class HorizontalStickerGridItemNode: GridItemNode { } } - /*func transitionNode(id: MessageId, media: Media) -> ASDisplayNode? { - if self.messageId == id { - return self.imageNode - } else { - return nil - } - }*/ - @objc func imageNodeTap(_ recognizer: UITapGestureRecognizer) { if let interfaceInteraction = self.interfaceInteraction, let (_, item, _) = self.currentState, case .ended = recognizer.state { - interfaceInteraction.sendSticker(item) + interfaceInteraction.sendSticker(item.file) + } + } + + func transitionNode() -> ASDisplayNode? { + return self.imageNode + } + + func updatePreviewing(animated: Bool) { + var isPreviewing = false + if let (_, item, _) = self.currentState { + isPreviewing = item.stickersInteraction.previewedStickerItem == self.stickerItem + } + if self.currentIsPreviewing != isPreviewing { + self.currentIsPreviewing = isPreviewing + + if isPreviewing { + self.layer.sublayerTransform = CATransform3DMakeScale(0.8, 0.8, 1.0) + if animated { + self.layer.animateSpring(from: 1.0 as NSNumber, to: 0.8 as NSNumber, keyPath: "sublayerTransform.scale", duration: 0.4) + } + } else { + self.layer.sublayerTransform = CATransform3DIdentity + if animated { + self.layer.animateSpring(from: 0.8 as NSNumber, to: 1.0 as NSNumber, keyPath: "sublayerTransform.scale", duration: 0.5) + } + } } - /*if let controllerInteraction = self.controllerInteraction, let messageId = self.messageId, case .ended = recognizer.state { - controllerInteraction.openMessage(messageId) - }*/ } } diff --git a/TelegramUI/HorizontalStickersChatContextPanelNode.swift b/TelegramUI/HorizontalStickersChatContextPanelNode.swift index c02064a34e..90c8ecb06f 100644 --- a/TelegramUI/HorizontalStickersChatContextPanelNode.swift +++ b/TelegramUI/HorizontalStickersChatContextPanelNode.swift @@ -4,35 +4,9 @@ import Postbox import TelegramCore import Display -private let backgroundCenterImage = generateImage(CGSize(width: 30.0, height: 82.0), rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setStrokeColor(UIColor(rgb: 0xbfbfc4).cgColor) - context.setFillColor(UIColor.white.cgColor) - let lineWidth = UIScreenPixel - context.setLineWidth(lineWidth) - - context.translateBy(x: 460.5, y: 364) - let _ = try? drawSvgPath(context, path: "M-490.476836,-365 L-394.167708,-365 L-394.167708,-291.918214 C-394.167708,-291.918214 -383.538396,-291.918214 -397.691655,-291.918214 C-402.778486,-291.918214 -424.555168,-291.918214 -434.037301,-291.918214 C-440.297129,-291.918214 -440.780682,-283.5 -445.999879,-283.5 C-450.393041,-283.5 -452.491241,-291.918214 -456.502636,-291.918214 C-465.083339,-291.918214 -476.209155,-291.918214 -483.779021,-291.918214 C-503.033963,-291.918214 -490.476836,-291.918214 -490.476836,-291.918214 L-490.476836,-365 ") - context.fillPath() - context.translateBy(x: 0.0, y: lineWidth / 2.0) - let _ = try? drawSvgPath(context, path: "M-490.476836,-365 L-394.167708,-365 L-394.167708,-291.918214 C-394.167708,-291.918214 -383.538396,-291.918214 -397.691655,-291.918214 C-402.778486,-291.918214 -424.555168,-291.918214 -434.037301,-291.918214 C-440.297129,-291.918214 -440.780682,-283.5 -445.999879,-283.5 C-450.393041,-283.5 -452.491241,-291.918214 -456.502636,-291.918214 C-465.083339,-291.918214 -476.209155,-291.918214 -483.779021,-291.918214 C-503.033963,-291.918214 -490.476836,-291.918214 -490.476836,-291.918214 L-490.476836,-365 ") - context.strokePath() - context.translateBy(x: -460.5, y: -lineWidth / 2.0 - 364.0) - context.move(to: CGPoint(x: 0.0, y: lineWidth / 2.0)) - context.addLine(to: CGPoint(x: size.width, y: lineWidth / 2.0)) - context.strokePath() -}) - -private let backgroundLeftImage = generateImage(CGSize(width: 8.0, height: 16.0), rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setStrokeColor(UIColor(rgb: 0xbfbfc4).cgColor) - context.setFillColor(UIColor.white.cgColor) - let lineWidth = UIScreenPixel - context.setLineWidth(lineWidth) - - context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.height, height: size.height))) - context.strokeEllipse(in: CGRect(origin: CGPoint(x: lineWidth / 2.0, y: lineWidth / 2.0), size: CGSize(width: size.height - lineWidth, height: size.height - lineWidth))) -})?.stretchableImage(withLeftCapWidth: 8, topCapHeight: 8) +final class HorizontalStickersChatContextPanelInteraction { + var previewedStickerItem: StickerPackItem? +} private struct StickerEntry: Identifiable, Comparable { let index: Int @@ -50,9 +24,8 @@ private struct StickerEntry: Identifiable, Comparable { return lhs.index < rhs.index } - func item(account: Account, interfaceInteraction: ChatPanelInterfaceInteraction) -> GridItem { - let file = self.file - return HorizontalStickerGridItem(account: account, file: file, interfaceInteraction: interfaceInteraction) + func item(account: Account, stickersInteraction: HorizontalStickersChatContextPanelInteraction, interfaceInteraction: ChatPanelInterfaceInteraction) -> GridItem { + return HorizontalStickerGridItem(account: account, file: self.file, stickersInteraction: stickersInteraction, interfaceInteraction: interfaceInteraction) } } @@ -65,31 +38,69 @@ private struct StickerEntryTransition { let scrollToItem: GridNodeScrollToItem? } -private func preparedGridEntryTransition(account: Account, from fromEntries: [StickerEntry], to toEntries: [StickerEntry], interfaceInteraction: ChatPanelInterfaceInteraction) -> StickerEntryTransition { +private func preparedGridEntryTransition(account: Account, from fromEntries: [StickerEntry], to toEntries: [StickerEntry], stickersInteraction: HorizontalStickersChatContextPanelInteraction, interfaceInteraction: ChatPanelInterfaceInteraction) -> StickerEntryTransition { let stationaryItems: GridNodeStationaryItems = .none let scrollToItem: GridNodeScrollToItem? = nil let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices - let insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(account: account, interfaceInteraction: interfaceInteraction), previousIndex: $0.2) } - let updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, interfaceInteraction: interfaceInteraction)) } + let insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(account: account, stickersInteraction: stickersInteraction, interfaceInteraction: interfaceInteraction), previousIndex: $0.2) } + let updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, stickersInteraction: stickersInteraction, interfaceInteraction: interfaceInteraction)) } return StickerEntryTransition(deletions: deletions, insertions: insertions, updates: updates, updateFirstIndexInSectionOffset: nil, stationaryItems: stationaryItems, scrollToItem: scrollToItem) } final class HorizontalStickersChatContextPanelNode: ChatInputContextPanelNode { + private var theme: PresentationTheme + private let backgroundLeftNode: ASImageNode private let backgroundNode: ASImageNode private let backgroundRightNode: ASImageNode private let clippingNode: ASDisplayNode private let gridNode: GridNode - private var validLayout: (CGSize, ChatPresentationInterfaceState)? + private var validLayout: (CGSize, CGFloat, CGFloat, ChatPresentationInterfaceState)? private var currentEntries: [StickerEntry] = [] private var queuedTransitions: [StickerEntryTransition] = [] - override init(account: Account) { + private let stickersInteraction: HorizontalStickersChatContextPanelInteraction + + private var stickerPreviewController: StickerPreviewController? + + override init(account: Account, theme: PresentationTheme, strings: PresentationStrings) { + self.theme = theme + + let backgroundCenterImage = generateImage(CGSize(width: 30.0, height: 82.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setStrokeColor(theme.list.itemPlainSeparatorColor.cgColor) + context.setFillColor(theme.list.plainBackgroundColor.cgColor) + let lineWidth = UIScreenPixel + context.setLineWidth(lineWidth) + + context.translateBy(x: 460.5, y: 364) + let _ = try? drawSvgPath(context, path: "M-490.476836,-365 L-394.167708,-365 L-394.167708,-291.918214 C-394.167708,-291.918214 -383.538396,-291.918214 -397.691655,-291.918214 C-402.778486,-291.918214 -424.555168,-291.918214 -434.037301,-291.918214 C-440.297129,-291.918214 -440.780682,-283.5 -445.999879,-283.5 C-450.393041,-283.5 -452.491241,-291.918214 -456.502636,-291.918214 C-465.083339,-291.918214 -476.209155,-291.918214 -483.779021,-291.918214 C-503.033963,-291.918214 -490.476836,-291.918214 -490.476836,-291.918214 L-490.476836,-365 ") + context.fillPath() + context.translateBy(x: 0.0, y: lineWidth / 2.0) + let _ = try? drawSvgPath(context, path: "M-490.476836,-365 L-394.167708,-365 L-394.167708,-291.918214 C-394.167708,-291.918214 -383.538396,-291.918214 -397.691655,-291.918214 C-402.778486,-291.918214 -424.555168,-291.918214 -434.037301,-291.918214 C-440.297129,-291.918214 -440.780682,-283.5 -445.999879,-283.5 C-450.393041,-283.5 -452.491241,-291.918214 -456.502636,-291.918214 C-465.083339,-291.918214 -476.209155,-291.918214 -483.779021,-291.918214 C-503.033963,-291.918214 -490.476836,-291.918214 -490.476836,-291.918214 L-490.476836,-365 ") + context.strokePath() + context.translateBy(x: -460.5, y: -lineWidth / 2.0 - 364.0) + context.move(to: CGPoint(x: 0.0, y: lineWidth / 2.0)) + context.addLine(to: CGPoint(x: size.width, y: lineWidth / 2.0)) + context.strokePath() + }) + + let backgroundLeftImage = generateImage(CGSize(width: 8.0, height: 16.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setStrokeColor(theme.list.itemPlainSeparatorColor.cgColor) + context.setFillColor(theme.list.plainBackgroundColor.cgColor) + let lineWidth = UIScreenPixel + context.setLineWidth(lineWidth) + + context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.height, height: size.height))) + context.strokeEllipse(in: CGRect(origin: CGPoint(x: lineWidth / 2.0, y: lineWidth / 2.0), size: CGSize(width: size.height - lineWidth, height: size.height - lineWidth))) + })?.stretchableImage(withLeftCapWidth: 8, topCapHeight: 8) + self.backgroundNode = ASImageNode() self.backgroundNode.displayWithoutProcessing = true self.backgroundNode.displaysAsynchronously = false @@ -109,10 +120,12 @@ final class HorizontalStickersChatContextPanelNode: ChatInputContextPanelNode { self.clippingNode = ASDisplayNode() self.clippingNode.clipsToBounds = true self.gridNode = GridNode() - self.gridNode.transform = CATransform3DMakeRotation(-CGFloat(M_PI / 2.0), 0.0, 0.0, 1.0) + self.gridNode.transform = CATransform3DMakeRotation(-CGFloat.pi / 2.0, 0.0, 0.0, 1.0) self.gridNode.view.disablesInteractiveTransitionGestureRecognizer = true - super.init(account: account) + self.stickersInteraction = HorizontalStickersChatContextPanelInteraction() + + super.init(account: account, theme: theme, strings: strings) self.placement = .overTextInput self.isOpaque = false @@ -125,6 +138,19 @@ final class HorizontalStickersChatContextPanelNode: ChatInputContextPanelNode { self.clippingNode.addSubnode(self.gridNode) } + override func didLoad() { + super.didLoad() + + let longTapRecognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.previewGesture(_:))) + longTapRecognizer.tapActionAtPoint = { [weak self] location in + if let strongSelf = self, let _ = strongSelf.gridNode.itemNodeAtPoint(location) as? HorizontalStickerGridItemNode { + return .waitForHold(timeout: 0.2, acceptTap: false) + } + return .fail + } + self.gridNode.view.addGestureRecognizer(longTapRecognizer) + } + func updateResults(_ results: [TelegramMediaFile]) { let previousEntries = self.currentEntries var entries: [StickerEntry] = [] @@ -134,10 +160,10 @@ final class HorizontalStickersChatContextPanelNode: ChatInputContextPanelNode { self.currentEntries = entries if let validLayout = self.validLayout { - self.updateLayout(size: validLayout.0, transition: .immediate, interfaceState: validLayout.1) + self.updateLayout(size: validLayout.0, leftInset: validLayout.1, rightInset: validLayout.2, transition: .immediate, interfaceState: validLayout.3) } - let transition = preparedGridEntryTransition(account: self.account, from: previousEntries, to: entries, interfaceInteraction: self.interfaceInteraction!) + let transition = preparedGridEntryTransition(account: self.account, from: previousEntries, to: entries, stickersInteraction: self.stickersInteraction, interfaceInteraction: self.interfaceInteraction!) self.enqueueTransition(transition) } @@ -155,20 +181,20 @@ final class HorizontalStickersChatContextPanelNode: ChatInputContextPanelNode { } } - override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) { - let sideInsets: CGFloat = 10.0 + override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) { + let sideInsets: CGFloat = 10.0 + leftInset let contentWidth = min(size.width - sideInsets - sideInsets, max(24.0, CGFloat(self.currentEntries.count) * 66.0 + 6.0)) - var leftInset: CGFloat = 40.0 + var contentLeftInset: CGFloat = 40.0 var leftOffset: CGFloat = 0.0 - if sideInsets + floor(contentWidth / 2.0) < sideInsets + leftInset + 15.0 { + if sideInsets + floor(contentWidth / 2.0) < sideInsets + contentLeftInset + 15.0 { let updatedLeftInset = sideInsets + floor(contentWidth / 2.0) - 15.0 - sideInsets - leftOffset = leftInset - updatedLeftInset - leftInset = updatedLeftInset + leftOffset = contentLeftInset - updatedLeftInset + contentLeftInset = updatedLeftInset } let backgroundFrame = CGRect(origin: CGPoint(x: sideInsets + leftOffset, y: size.height - 82.0 + 4.0), size: CGSize(width: contentWidth, height: 82.0)) - let backgroundLeftFrame = CGRect(origin: backgroundFrame.origin, size: CGSize(width: leftInset, height: backgroundFrame.size.height - 10.0 + UIScreenPixel)) + let backgroundLeftFrame = CGRect(origin: backgroundFrame.origin, size: CGSize(width: contentLeftInset, height: backgroundFrame.size.height - 10.0 + UIScreenPixel)) let backgroundCenterFrame = CGRect(origin: CGPoint(x: backgroundLeftFrame.maxX, y: backgroundFrame.minY), size: CGSize(width: 30.0, height: 82.0)) let backgroundRightFrame = CGRect(origin: CGPoint(x: backgroundCenterFrame.maxX, y: backgroundFrame.minY), size: CGSize(width: max(0.0, backgroundFrame.minX + backgroundFrame.size.width - backgroundCenterFrame.maxX), height: backgroundFrame.size.height - 10.0 + UIScreenPixel)) transition.updateFrame(node: self.backgroundLeftNode, frame: backgroundLeftFrame) @@ -186,7 +212,7 @@ final class HorizontalStickersChatContextPanelNode: ChatInputContextPanelNode { self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: CGSize(width: gridFrame.size.height, height: gridFrame.size.width), insets: UIEdgeInsets(top: 3.0, left: 0.0, bottom: 3.0, right: 0.0), preloadSize: 100.0, type: .fixed(itemSize: CGSize(width: 66.0, height: 66.0), lineSpacing: 0.0)), transition: .immediate), itemTransition: .immediate, stationaryItems: .all, updateFirstIndexInSectionOffset: nil), completion: { _ in }) let dequeue = self.validLayout == nil - self.validLayout = (size, interfaceState) + self.validLayout = (size, leftInset, rightInset, interfaceState) if dequeue { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) @@ -203,4 +229,59 @@ final class HorizontalStickersChatContextPanelNode: ChatInputContextPanelNode { override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { return super.hitTest(point, with: event) } + + @objc func previewGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { + switch recognizer.state { + case .began: + if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation, case .hold = gesture { + if let itemNode = self.gridNode.itemNodeAtPoint(location) as? HorizontalStickerGridItemNode { + self.updatePreviewingItem(item: itemNode.stickerItem, animated: true) + } + } + case .ended, .cancelled: + self.updatePreviewingItem(item: nil, animated: true) + case .changed: + if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation, case .hold = gesture, let itemNode = self.gridNode.itemNodeAtPoint(location) as? HorizontalStickerGridItemNode { + self.updatePreviewingItem(item: itemNode.stickerItem, animated: true) + } + default: + break + } + } + + private func updatePreviewingItem(item: StickerPackItem?, animated: Bool) { + if self.stickersInteraction.previewedStickerItem != item { + self.stickersInteraction.previewedStickerItem = item + + self.gridNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? HorizontalStickerGridItemNode { + itemNode.updatePreviewing(animated: animated) + } + } + + if let item = item { + if let stickerPreviewController = self.stickerPreviewController { + stickerPreviewController.updateItem(item) + } else { + let stickerPreviewController = StickerPreviewController(account: self.account, item: item) + self.stickerPreviewController = stickerPreviewController + self.interfaceInteraction?.presentController(stickerPreviewController, StickerPreviewControllerPresentationArguments(transitionNode: { [weak self] item in + if let strongSelf = self { + var result: ASDisplayNode? + strongSelf.gridNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? HorizontalStickerGridItemNode, itemNode.stickerItem == item { + result = itemNode.transitionNode() + } + } + return result + } + return nil + })) + } + } else if let stickerPreviewController = self.stickerPreviewController { + stickerPreviewController.dismiss() + self.stickerPreviewController = nil + } + } + } } diff --git a/TelegramUI/IconButtonNode.swift b/TelegramUI/IconButtonNode.swift new file mode 100644 index 0000000000..a279aa01c0 --- /dev/null +++ b/TelegramUI/IconButtonNode.swift @@ -0,0 +1,54 @@ +import Foundation +import AsyncDisplayKit +import Display + +final class IconButtonNode: HighlightTrackingButtonNode { + private let iconNode: ASImageNode + + var icon: UIImage? { + didSet { + self.iconNode.image = self.icon + + self.setNeedsLayout() + } + } + + override var isEnabled: Bool { + didSet { + self.alpha = self.isEnabled ? 1.0 : 0.5 + } + } + + override init() { + self.iconNode = ASImageNode() + self.iconNode.isLayerBacked = true + self.iconNode.displaysAsynchronously = false + self.iconNode.displayWithoutProcessing = true + + super.init() + + self.addSubnode(self.iconNode) + + self.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + let transition: ContainedViewLayoutTransition = .animated(duration: 0.09, curve: .spring) + transition.updateSublayerTransformScale(node: strongSelf, scale: 0.8) + } else { + let transition: ContainedViewLayoutTransition = .animated(duration: 0.18, curve: .spring) + transition.updateSublayerTransformScale(node: strongSelf, scale: 1.0) + } + } + } + } + + override func layout() { + super.layout() + + let size = self.bounds.size + + if let image = self.iconNode.image { + self.iconNode.frame = CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size) + } + } +} diff --git a/TelegramUI/ImageNode.swift b/TelegramUI/ImageNode.swift index 3b6c5b415b..20a0054d99 100644 --- a/TelegramUI/ImageNode.swift +++ b/TelegramUI/ImageNode.swift @@ -8,7 +8,7 @@ private let dispatcher = displayLinkDispatcher public enum ImageCorner: Equatable { case Corner(CGFloat) - case Tail(CGFloat) + case Tail(CGFloat, Bool) public var extendedInsets: CGSize { switch self { @@ -23,7 +23,7 @@ public enum ImageCorner: Equatable { switch self { case .Corner: return self - case let .Tail(radius): + case let .Tail(radius, _): return .Corner(radius) } } @@ -32,7 +32,7 @@ public enum ImageCorner: Equatable { switch self { case let .Corner(radius): return radius - case let .Tail(radius): + case let .Tail(radius, _): return radius } } @@ -47,12 +47,11 @@ public func ==(lhs: ImageCorner, rhs: ImageCorner) -> Bool { default: return false } - case let .Tail(lhsRadius): - switch rhs { - case let .Tail(rhsRadius) where abs(lhsRadius - rhsRadius) < CGFloat.ulpOfOne: - return true - default: - return false + case let .Tail(lhsRadius, lhsEnabled): + if case let .Tail(rhsRadius, rhsEnabled) = rhs, lhsRadius.isEqual(to: rhsRadius), lhsEnabled == rhsEnabled { + return true + } else { + return false } } } diff --git a/TelegramUI/InstalledStickerPacksController.swift b/TelegramUI/InstalledStickerPacksController.swift index 7716034dff..a36c161dd0 100644 --- a/TelegramUI/InstalledStickerPacksController.swift +++ b/TelegramUI/InstalledStickerPacksController.swift @@ -68,7 +68,7 @@ private enum InstalledStickerPacksEntry: ItemListNodeEntry { case archived(PresentationTheme, String) case masks(PresentationTheme, String) case packsTitle(PresentationTheme, String) - case pack(Int32, PresentationTheme, StickerPackCollectionInfo, StickerPackItem?, String, Bool, ItemListStickerPackItemEditing) + case pack(Int32, PresentationTheme, PresentationStrings, StickerPackCollectionInfo, StickerPackItem?, String, Bool, ItemListStickerPackItemEditing) case packsInfo(PresentationTheme, String) var section: ItemListSectionId { @@ -90,7 +90,7 @@ private enum InstalledStickerPacksEntry: ItemListNodeEntry { return .index(2) case .packsTitle: return .index(3) - case let .pack(_, _, info, _, _, _, _): + case let .pack(_, _, _, info, _, _, _, _): return .pack(info.id) case .packsInfo: return .index(4) @@ -123,14 +123,17 @@ private enum InstalledStickerPacksEntry: ItemListNodeEntry { } else { return false } - case let .pack(lhsIndex, lhsTheme, lhsInfo, lhsTopItem, lhsCount, lhsEnabled, lhsEditing): - if case let .pack(rhsIndex, rhsTheme, rhsInfo, rhsTopItem, rhsCount, rhsEnabled, rhsEditing) = rhs { + case let .pack(lhsIndex, lhsTheme, lhsStrings, lhsInfo, lhsTopItem, lhsCount, lhsEnabled, lhsEditing): + if case let .pack(rhsIndex, rhsTheme, rhsStrings, rhsInfo, rhsTopItem, rhsCount, rhsEnabled, rhsEditing) = rhs { if lhsIndex != rhsIndex { return false } if lhsTheme !== rhsTheme { return false } + if lhsStrings !== rhsStrings { + return false + } if lhsInfo != rhsInfo { return false } @@ -189,9 +192,9 @@ private enum InstalledStickerPacksEntry: ItemListNodeEntry { default: return true } - case let .pack(lhsIndex, _, _, _, _, _, _): + case let .pack(lhsIndex, _, _, _, _, _, _, _): switch rhs { - case let .pack(rhsIndex, _, _, _, _, _, _): + case let .pack(rhsIndex, _, _, _, _, _, _, _): return lhsIndex < rhsIndex case .packsInfo: return true @@ -224,8 +227,8 @@ private enum InstalledStickerPacksEntry: ItemListNodeEntry { }) case let .packsTitle(theme, text): return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) - case let .pack(_, theme, info, topItem, count, enabled, editing): - return ItemListStickerPackItem(theme: theme, account: arguments.account, packInfo: info, itemCount: count, topItem: topItem, unread: false, control: .none, editing: editing, enabled: enabled, sectionId: self.section, action: { + case let .pack(_, theme, strings, info, topItem, count, enabled, editing): + return ItemListStickerPackItem(theme: theme, strings: strings, account: arguments.account, packInfo: info, itemCount: count, topItem: topItem, unread: false, control: .none, editing: editing, enabled: enabled, sectionId: self.section, action: { arguments.openStickerPack(info) }, setPackIdWithRevealedOptions: { current, previous in arguments.setPackIdWithRevealedOptions(current, previous) @@ -277,7 +280,7 @@ private struct InstalledStickerPacksControllerState: Equatable { private func namespaceForMode(_ mode: InstalledStickerPacksControllerMode) -> ItemCollectionId.Namespace { switch mode { - case .general: + case .general, .modal: return Namespaces.ItemCollection.CloudStickerPacks case .masks: return Namespaces.ItemCollection.CloudMaskPacks @@ -309,7 +312,7 @@ private func installedStickerPacksControllerEntries(presentationData: Presentati entries.append(.archived(presentationData.theme, presentationData.strings.StickerPacksSettings_ArchivedPacks)) entries.append(.masks(presentationData.theme, presentationData.strings.MaskStickerSettings_Title)) entries.append(.packsTitle(presentationData.theme, presentationData.strings.StickerPacksSettings_StickerPacksSection)) - case .masks: + case .masks, .modal: break } @@ -318,7 +321,7 @@ private func installedStickerPacksControllerEntries(presentationData: Presentati var index: Int32 = 0 for entry in packsEntries { if let info = entry.info as? StickerPackCollectionInfo { - entries.append(.pack(index, presentationData.theme, info, entry.firstItem as? StickerPackItem, stringForStickerCount(info.count == 0 ? entry.count : info.count), true, ItemListStickerPackItemEditing(editable: true, editing: state.editing, revealed: state.packIdWithRevealedOptions == entry.id))) + entries.append(.pack(index, presentationData.theme, presentationData.strings, info, entry.firstItem as? StickerPackItem, stringForStickerCount(info.count == 0 ? entry.count : info.count), true, ItemListStickerPackItemEditing(editable: true, editing: state.editing, revealed: state.packIdWithRevealedOptions == entry.id))) index += 1 } } @@ -326,7 +329,7 @@ private func installedStickerPacksControllerEntries(presentationData: Presentati } switch mode { - case .general: + case .general, .modal: entries.append(.packsInfo(presentationData.theme, presentationData.strings.StickerPacksSettings_ManagingHelp)) case .masks: entries.append(.packsInfo(presentationData.theme, presentationData.strings.MaskStickerSettings_Info)) @@ -337,12 +340,14 @@ private func installedStickerPacksControllerEntries(presentationData: Presentati public enum InstalledStickerPacksControllerMode { case general + case modal case masks } public func installedStickerPacksController(account: Account, mode: InstalledStickerPacksControllerMode) -> ViewController { - let statePromise = ValuePromise(InstalledStickerPacksControllerState(), ignoreRepeated: true) - let stateValue = Atomic(value: InstalledStickerPacksControllerState()) + let initialState = InstalledStickerPacksControllerState().withUpdatedEditing(mode == .modal) + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) let updateState: ((InstalledStickerPacksControllerState) -> InstalledStickerPacksControllerState) -> Void = { f in statePromise.set(stateValue.modify { f($0) }) } @@ -350,6 +355,7 @@ public func installedStickerPacksController(account: Account, mode: InstalledSti var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)? var pushControllerImpl: ((ViewController) -> Void)? var navigateToChatControllerImpl: ((PeerId) -> Void)? + var dismissImpl: (() -> Void)? let actionsDisposable = DisposableSet() @@ -374,12 +380,12 @@ public func installedStickerPacksController(account: Account, mode: InstalledSti } controller.setItemGroups([ ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: "Remove", color: .destructive, action: { + ActionSheetButtonItem(title: presentationData.strings.Common_Delete, color: .destructive, action: { dismissAction() let _ = removeStickerPackInteractively(postbox: account.postbox, id: id).start() }) ]), - ActionSheetItemGroup(items: [ActionSheetButtonItem(title: "Cancel", action: { dismissAction() })]) + ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) ]) presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) }, openStickersBot: { @@ -402,7 +408,7 @@ public func installedStickerPacksController(account: Account, mode: InstalledSti switch mode { case .general: featured.set(account.viewTracker.featuredStickerPacks()) - case .masks: + case .masks, .modal: featured.set(.single([])) } @@ -416,6 +422,13 @@ public func installedStickerPacksController(account: Account, mode: InstalledSti packCount = entries.count } + var leftNavigationButton: ItemListNavigationButton? + if case .modal = mode { + leftNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Cancel, style: .regular, enabled: true, action: { + dismissImpl?() + }) + } + var rightNavigationButton: ItemListNavigationButton? if let packCount = packCount, packCount != 0 { if state.editing { @@ -436,7 +449,7 @@ public func installedStickerPacksController(account: Account, mode: InstalledSti let previous = previousPackCount previousPackCount = packCount - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(mode == .general ? "Stickers" : "Masks"), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(mode == .general ? presentationData.strings.StickerPacksSettings_Title : presentationData.strings.MaskStickerSettings_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) let listState = ItemListNodeState(entries: installedStickerPacksControllerEntries(presentationData: presentationData, state: state, mode: mode, view: view, featured: featured), style: .blocks, animateChanges: previous != nil && packCount != nil && (previous! != 0 && previous! >= packCount! - 10)) return (controllerState, (listState, arguments)) @@ -455,9 +468,12 @@ public func installedStickerPacksController(account: Account, mode: InstalledSti } navigateToChatControllerImpl = { [weak controller] peerId in if let controller = controller, let navigationController = controller.navigationController as? NavigationController { - navigateToChatController(navigationController: navigationController, account: account, peerId: peerId) + navigateToChatController(navigationController: navigationController, account: account, chatLocation: .peer(peerId)) } } + dismissImpl = { [weak controller] in + controller?.dismiss() + } return controller } diff --git a/TelegramUI/InstantImageGalleryItem.swift b/TelegramUI/InstantImageGalleryItem.swift index b4e75a8802..94c9f894db 100644 --- a/TelegramUI/InstantImageGalleryItem.swift +++ b/TelegramUI/InstantImageGalleryItem.swift @@ -91,9 +91,8 @@ final class InstantImageGalleryItemNode: ZoomableContentGalleryItemNode { if self.accountAndMedia == nil || !self.accountAndMedia!.1.isEqual(image) { if let largestSize = largestRepresentationForPhoto(image) { let displaySize = largestSize.dimensions.fitted(CGSize(width: 1280.0, height: 1280.0)).dividedByScreenScale().integralFloor - self.imageNode.alphaTransitionOnFirstUpdate = false self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets()))() - self.imageNode.setSignal(account: account, signal: chatMessagePhoto(account: account, photo: image), dispatchOnDisplayLink: false) + self.imageNode.setSignal(chatMessagePhoto(postbox: account.postbox, photo: image), dispatchOnDisplayLink: false) self.zoomableContent = (largestSize.dimensions, self.imageNode) self.fetchDisposable.set(account.postbox.mediaBox.fetchedResource(largestSize.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .image)).start()) } else { @@ -106,10 +105,9 @@ final class InstantImageGalleryItemNode: ZoomableContentGalleryItemNode { func setFile(account: Account, file: TelegramMediaFile) { if self.accountAndMedia == nil || !self.accountAndMedia!.1.isEqual(file) { if let largestSize = file.dimensions { - self.imageNode.alphaTransitionOnFirstUpdate = false let displaySize = largestSize.dividedByScreenScale() self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets()))() - self.imageNode.setSignal(account: account, signal: chatMessageImageFile(account: account, file: file, progressive: false), dispatchOnDisplayLink: false) + self.imageNode.setSignal(chatMessageImageFile(account: account, file: file, thumbnail: false), dispatchOnDisplayLink: false) self.zoomableContent = (largestSize, self.imageNode) } else { self._ready.set(.single(Void())) diff --git a/TelegramUI/InstantPageAnchorItem.swift b/TelegramUI/InstantPageAnchorItem.swift index 25600dc8b4..4246c3c7e1 100644 --- a/TelegramUI/InstantPageAnchorItem.swift +++ b/TelegramUI/InstantPageAnchorItem.swift @@ -1,6 +1,7 @@ import Foundation import Postbox import TelegramCore +import AsyncDisplayKit final class InstantPageAnchorItem: InstantPageItem { let wantsNode: Bool = false @@ -21,7 +22,7 @@ final class InstantPageAnchorItem: InstantPageItem { func drawInTile(context: CGContext) { } - func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void) -> InstantPageNode? { + func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void) -> (InstantPageNode & ASDisplayNode)? { return nil } diff --git a/TelegramUI/InstantPageAudioItem.swift b/TelegramUI/InstantPageAudioItem.swift index 476a314d31..47b882fb2b 100644 --- a/TelegramUI/InstantPageAudioItem.swift +++ b/TelegramUI/InstantPageAudioItem.swift @@ -1,6 +1,7 @@ import Foundation import Postbox import TelegramCore +import AsyncDisplayKit final class InstantPageAudioItem: InstantPageItem { var frame: CGRect @@ -17,7 +18,7 @@ final class InstantPageAudioItem: InstantPageItem { self.medias = [media] } - func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void) -> InstantPageNode? { + func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void) -> (InstantPageNode & ASDisplayNode)? { return InstantPageAudioNode(account: account, strings: strings, theme: theme, webpage: self.webpage, media: self.media, openMedia: openMedia) } diff --git a/TelegramUI/InstantPageAudioNode.swift b/TelegramUI/InstantPageAudioNode.swift index cb86ace557..da8ea80def 100644 --- a/TelegramUI/InstantPageAudioNode.swift +++ b/TelegramUI/InstantPageAudioNode.swift @@ -91,7 +91,7 @@ final class InstantPageAudioNode: ASDisplayNode, InstantPageNode { if brightness > 0.5 { backgroundAlpha = 0.4 } - self.scrubbingNode = MediaPlayerScrubbingNode(lineHeight: 3.0, lineCap: .round, scrubberHandle: true, backgroundColor: theme.textCategories.paragraph.color.withAlphaComponent(backgroundAlpha), foregroundColor: theme.textCategories.paragraph.color) + self.scrubbingNode = MediaPlayerScrubbingNode(content: .standard(lineHeight: 3.0, lineCap: .round, scrubberHandle: .line, backgroundColor: theme.textCategories.paragraph.color.withAlphaComponent(backgroundAlpha), foregroundColor: theme.textCategories.paragraph.color)) super.init() @@ -147,7 +147,7 @@ final class InstantPageAudioNode: ASDisplayNode, InstantPageNode { switch status { case .paused: break - case let .buffering(whilePlaying): + case let .buffering(_, whilePlaying): isPlaying = whilePlaying case .playing: isPlaying = true diff --git a/TelegramUI/InstantPageController.swift b/TelegramUI/InstantPageController.swift index 4b0fb7173a..6991198176 100644 --- a/TelegramUI/InstantPageController.swift +++ b/TelegramUI/InstantPageController.swift @@ -74,9 +74,11 @@ final class InstantPageController: ViewController { override public func loadDisplayNode() { self.displayNode = InstantPageControllerNode(account: self.account, settings: self.settings, presentationTheme: self.presentationData.theme, strings: self.presentationData.strings, statusBar: self.statusBar, present: { [weak self] c, a in self?.present(c, in: .window(.root), with: a) - }, openPeer: { [weak self] peerId in + }, pushController: { [weak self] c in + (self?.navigationController as? NavigationController)?.pushViewController(c) + }, openPeer: { [weak self] peerId in if let strongSelf = self { - (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, peerId: peerId)) + (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, chatLocation: .peer(peerId))) } }, navigateBack: { [weak self] in self?.navigationController?.popViewController(animated: true) diff --git a/TelegramUI/InstantPageControllerNode.swift b/TelegramUI/InstantPageControllerNode.swift index 82dcc5f5fd..f6ff2f4e27 100644 --- a/TelegramUI/InstantPageControllerNode.swift +++ b/TelegramUI/InstantPageControllerNode.swift @@ -13,6 +13,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { private var strings: PresentationStrings private var theme: InstantPageTheme? private let present: (ViewController, Any?) -> Void + private let pushController: (ViewController) -> Void private let openPeer: (PeerId) -> Void private var webPage: TelegramMediaWebpage? @@ -43,16 +44,18 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { private let hiddenMediaDisposable = MetaDisposable() private let resolveUrlDisposable = MetaDisposable() + private let loadWebpageDisposable = MetaDisposable() - init(account: Account, settings: InstantPagePresentationSettings?, presentationTheme: PresentationTheme, strings: PresentationStrings, statusBar: StatusBar, present: @escaping (ViewController, Any?) -> Void, openPeer: @escaping (PeerId) -> Void, navigateBack: @escaping () -> Void) { + init(account: Account, settings: InstantPagePresentationSettings?, presentationTheme: PresentationTheme, strings: PresentationStrings, statusBar: StatusBar, present: @escaping (ViewController, Any?) -> Void, pushController: @escaping (ViewController) -> Void, openPeer: @escaping (PeerId) -> Void, navigateBack: @escaping () -> Void) { self.account = account self.presentationTheme = presentationTheme self.strings = strings self.settings = settings - self.theme = settings.flatMap(instantPageThemeForSettings) + self.theme = settings.flatMap { return instantPageThemeForSettingsAndTime(settings: $0, time: Date()) } self.statusBar = statusBar self.present = present + self.pushController = pushController self.openPeer = openPeer self.navigationBar = InstantPageNavigationBar(strings: strings) @@ -97,6 +100,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { deinit { self.hiddenMediaDisposable.dispose() self.resolveUrlDisposable.dispose() + self.loadWebpageDisposable.dispose() } func update(settings: InstantPagePresentationSettings, strings: PresentationStrings) { @@ -105,13 +109,13 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { var updateLayout = previousSettings == nil self.settings = settings - let theme = instantPageThemeForSettings(settings) + let theme = instantPageThemeForSettingsAndTime(settings: settings, time: Date()) self.theme = theme self.strings = strings var animated = false if let previousSettings = previousSettings { - if previousSettings.themeType != settings.themeType { + if previousSettings.themeType != settings.themeType || previousSettings.autoNightMode != settings.autoNightMode { updateLayout = true animated = true } @@ -183,6 +187,15 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { func updateWebPage(_ webPage: TelegramMediaWebpage?, anchor: String?) { if self.webPage != webPage { + if self.webPage != nil && self.currentLayout != nil { + if let snaphotView = self.scrollNode.view.snapshotView(afterScreenUpdates: false) { + self.scrollNode.view.superview?.insertSubview(snaphotView, aboveSubview: self.scrollNode.view) + snaphotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snaphotView] _ in + snaphotView?.removeFromSuperview() + }) + } + } + self.setupScrollOffsetOnLayout = self.webPage == nil self.webPage = webPage self.initialAnchor = anchor @@ -210,7 +223,15 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { } let statusBarHeight: CGFloat = layout.statusBarHeight ?? 0.0 - let scrollInsetTop = 44.0 + statusBarHeight + + let maxBarHeight: CGFloat + if !layout.safeInsets.top.isZero { + maxBarHeight = layout.safeInsets.top + 34.0 + } else { + maxBarHeight = (layout.statusBarHeight ?? 0.0) + 44.0 + } + + let scrollInsetTop = maxBarHeight let resetOffset = self.scrollNode.bounds.size.width.isZero || self.setupScrollOffsetOnLayout let widthUpdated = !self.scrollNode.bounds.size.width.isEqual(to: layout.size.width) @@ -228,9 +249,9 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { } if resetOffset { var contentOffset = CGPoint(x: 0.0, y: -self.scrollNode.view.contentInset.top) - if let anchor = self.initialAnchor, let items = self.currentLayout?.items { - self.setupScrollOffsetOnLayout = false - if !anchor.isEmpty { + if let anchor = self.initialAnchor, !anchor.isEmpty { + if let items = self.currentLayout?.items { + self.setupScrollOffsetOnLayout = false outer: for item in items { if let item = item as? InstantPageAnchorItem, item.anchor == anchor { contentOffset = CGPoint(x: 0.0, y: item.frame.origin.y - self.scrollNode.view.contentInset.top) @@ -238,6 +259,8 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { } } } + } else { + self.setupScrollOffsetOnLayout = false } self.scrollNode.view.contentOffset = contentOffset } @@ -251,7 +274,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { return } - let currentLayout = instantPageLayoutForWebPage(webPage, boundingWidth: containerLayout.size.width, strings: self.strings, theme: theme) + let currentLayout = instantPageLayoutForWebPage(webPage, boundingWidth: containerLayout.size.width, safeInset: containerLayout.safeInsets.left, strings: self.strings, theme: theme) for (_, tileNode) in self.visibleTiles { tileNode.removeFromSupernode() @@ -359,13 +382,13 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { }, openPeer: { [weak self] peerId in self?.openPeer(peerId) }) { - (itemNode as! ASDisplayNode).frame = item.frame + itemNode.frame = item.frame if let topNode = topNode { - self.scrollNode.insertSubnode(itemNode as! ASDisplayNode, aboveSubnode: topNode) + self.scrollNode.insertSubnode(itemNode, aboveSubnode: topNode) } else { - self.scrollNode.insertSubnode(itemNode as! ASDisplayNode, at: 0) + self.scrollNode.insertSubnode(itemNode, at: 0) } - topNode = itemNode as! ASDisplayNode + topNode = itemNode self.visibleItemsWithViews[itemIndex] = itemNode } } else { @@ -424,9 +447,23 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { } func updateNavigationBar(forceState: Bool = false) { + guard let containerLayout = self.containerLayout else { + return + } + let bounds = self.scrollNode.view.bounds let contentOffset = self.scrollNode.view.contentOffset + let maxBarHeight: CGFloat + let minBarHeight: CGFloat + if !containerLayout.safeInsets.top.isZero { + maxBarHeight = containerLayout.safeInsets.top + 34.0 + minBarHeight = containerLayout.safeInsets.top + 8.0 + } else { + maxBarHeight = (containerLayout.statusBarHeight ?? 0.0) + 44.0 + minBarHeight = 20.0 + } + var pageProgress: CGFloat = 0.0 if !self.scrollNode.view.contentSize.height.isZero { let value = (contentOffset.y + self.scrollNode.view.contentInset.top) / (self.scrollNode.view.contentSize.height - bounds.size.height + self.scrollNode.view.contentInset.top) @@ -445,35 +482,40 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { var navigationBarFrame = self.navigationBar.frame navigationBarFrame.size.width = bounds.size.width if navigationBarFrame.size.height.isZero { - navigationBarFrame.size.height = 64.0 + navigationBarFrame.size.height = maxBarHeight } if forceState { transition = .animated(duration: 0.3, curve: .spring) - if contentOffset.y <= -self.scrollNode.view.contentInset.top || CGFloat(32.0).isLess(than: navigationBarFrame.size.height) { - navigationBarFrame.size.height = 64.0 + let transitionFactor = (navigationBarFrame.size.height - minBarHeight) / (maxBarHeight - minBarHeight) + + if contentOffset.y <= -self.scrollNode.view.contentInset.top || transitionFactor > 0.4 { + navigationBarFrame.size.height = maxBarHeight } else { - navigationBarFrame.size.height = 20.0 + navigationBarFrame.size.height = minBarHeight } } else { if contentOffset.y <= -self.scrollNode.view.contentInset.top { - navigationBarFrame.size.height = 64.0 + navigationBarFrame.size.height = maxBarHeight } else { navigationBarFrame.size.height -= delta } - navigationBarFrame.size.height = max(20.0, min(64.0, navigationBarFrame.size.height)) + navigationBarFrame.size.height = max(minBarHeight, min(maxBarHeight, navigationBarFrame.size.height)) } - if navigationBarFrame.height.isEqual(to: 64.0) { - assert(true) - } + let transitionFactor = (navigationBarFrame.size.height - minBarHeight) / (maxBarHeight - minBarHeight) - var statusBarAlpha = min(1.0, max(0.0, (navigationBarFrame.size.height - 20.0) / 44.0)) - transition.updateAlpha(node: self.statusBar, alpha: statusBarAlpha * statusBarAlpha) - self.statusBar.verticalOffset = navigationBarFrame.size.height - 64.0 + if containerLayout.safeInsets.top.isZero { + let statusBarAlpha = min(1.0, max(0.0, transitionFactor)) + transition.updateAlpha(node: self.statusBar, alpha: statusBarAlpha * statusBarAlpha) + self.statusBar.verticalOffset = navigationBarFrame.size.height - maxBarHeight + } else { + transition.updateAlpha(node: self.statusBar, alpha: 1.0) + self.statusBar.verticalOffset = 0.0 + } transition.updateFrame(node: self.navigationBar, frame: navigationBarFrame) - self.navigationBar.updateLayout(size: navigationBarFrame.size, pageProgress: pageProgress, transition: transition) + self.navigationBar.updateLayout(size: navigationBarFrame.size, minHeight: minBarHeight, maxHeight: maxBarHeight, topInset: containerLayout.safeInsets.top, leftInset: containerLayout.safeInsets.left, rightInset: containerLayout.safeInsets.right, pageProgress: pageProgress, transition: transition) transition.animateView { self.scrollNode.view.scrollIndicatorInsets = UIEdgeInsets(top: navigationBarFrame.size.height, left: 0.0, bottom: 0.0, right: 0.0) @@ -556,7 +598,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { strongSelf.openUrl(url) } }), - ActionSheetButtonItem(title: self.strings.Web_CopyLink, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: self.strings.ShareMenu_CopyShareLink, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() UIPasteboard.general.string = url.url }), @@ -636,7 +678,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { } if let webPage = self.webPage, url.webpageId == webPage.id, let anchorRange = url.url.range(of: "#") { - let anchor = url.url.substring(from: anchorRange.upperBound) + let anchor = url.url[anchorRange.upperBound...] if !anchor.isEmpty { for item in items { if let item = item as? InstantPageAnchorItem, item.anchor == anchor { @@ -650,9 +692,20 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { self.resolveUrlDisposable.set((resolveUrl(account: self.account, url: url.url) |> deliverOnMainQueue).start(next: { [weak self] result in if let strongSelf = self { switch result { - case let .externalUrl(url): - if let applicationContext = strongSelf.account.applicationContext as? TelegramApplicationContext { - applicationContext.applicationBindings.openUrl(url) + + case let .externalUrl(externalUrl): + if let webpageId = url.webpageId { + var anchor: String? + if let anchorRange = externalUrl.range(of: "#") { + anchor = String(externalUrl[anchorRange.upperBound...]) + } + strongSelf.loadWebpageDisposable.set((webpagePreview(account: strongSelf.account, url: externalUrl, webpageId: webpageId) |> deliverOnMainQueue).start(next: { webpage in + if let strongSelf = self, let webpage = webpage { + strongSelf.pushController(InstantPageController(account: strongSelf.account, webPage: webpage, anchor: anchor)) + } + })) + } else { + strongSelf.account.telegramApplicationContext.applicationBindings.openUrl(externalUrl) } default: break @@ -700,7 +753,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { var entries: [InstantPageGalleryEntry] = [] for media in medias { - entries.append(InstantPageGalleryEntry(index: Int32(media.index), media: media, caption: media.caption ?? "", location: InstantPageGalleryEntryLocation(position: Int32(entries.count), totalCount: Int32(medias.count)))) + entries.append(InstantPageGalleryEntry(index: Int32(media.index), pageId: webPage.webpageId, media: media, caption: media.caption ?? "", location: InstantPageGalleryEntryLocation(position: Int32(entries.count), totalCount: Int32(medias.count)))) } var centralIndex: Int? @@ -717,20 +770,16 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { self.hiddenMediaDisposable.set((controller.hiddenMedia |> deliverOnMainQueue).start(next: { [weak self] entry in if let strongSelf = self { for (_, itemNode) in strongSelf.visibleItemsWithViews { - if let itemNode = itemNode as? InstantPageNode { - itemNode.updateHiddenMedia(media: entry?.media) - } + itemNode.updateHiddenMedia(media: entry?.media) } } })) self.present(controller, InstantPageGalleryControllerPresentationArguments(transitionArguments: { [weak self] entry -> GalleryTransitionArguments? in if let strongSelf = self { for (_, itemNode) in strongSelf.visibleItemsWithViews { - if let itemNode = itemNode as? InstantPageNode { - if let transitionNode = itemNode.transitionNode(media: entry.media) { - return GalleryTransitionArguments(transitionNode: transitionNode, addToTransitionSurface: { _ in - }) - } + if let transitionNode = itemNode.transitionNode(media: entry.media) { + return GalleryTransitionArguments(transitionNode: transitionNode, addToTransitionSurface: { _ in + }) } } } diff --git a/TelegramUI/InstantPageGalleryController.swift b/TelegramUI/InstantPageGalleryController.swift index a8bcd2f123..aadbcdc0c1 100644 --- a/TelegramUI/InstantPageGalleryController.swift +++ b/TelegramUI/InstantPageGalleryController.swift @@ -17,17 +17,20 @@ struct InstantPageGalleryEntryLocation: Equatable { struct InstantPageGalleryEntry: Equatable { let index: Int32 + let pageId: MediaId let media: InstantPageMedia let caption: String let location: InstantPageGalleryEntryLocation static func ==(lhs: InstantPageGalleryEntry, rhs: InstantPageGalleryEntry) -> Bool { - return lhs.index == rhs.index && lhs.media == rhs.media && lhs.caption == rhs.caption && lhs.location == rhs.location + return lhs.index == rhs.index && lhs.pageId == rhs.pageId && lhs.media == rhs.media && lhs.caption == rhs.caption && lhs.location == rhs.location } func item(account: Account, theme: PresentationTheme, strings: PresentationStrings) -> GalleryItem { if let image = self.media.media as? TelegramMediaImage { return InstantImageGalleryItem(account: account, theme: theme, strings: strings, image: image, caption: self.caption, location: self.location) + } else if let file = self.media.media as? TelegramMediaFile, file.isVideo { + return UniversalVideoGalleryItem(account: account, theme: theme, strings: strings, content: NativeVideoContent(id: .instantPage(self.pageId, file.fileId), file: file), originData: nil, indexData: GalleryItemIndexData(position: self.location.position, totalCount: self.location.totalCount), contentInfo: nil, caption: self.caption) } else { preconditionFailure() } @@ -82,7 +85,8 @@ class InstantPageGalleryController: ViewController { super.init(navigationBarTheme: GalleryController.darkNavigationTheme) - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)) + let backItem = UIBarButtonItem(backButtonAppearanceWithTitle: presentationData.strings.Common_Back, target: self, action: #selector(self.donePressed)) + self.navigationItem.leftBarButtonItem = backItem self.statusBar.statusBarStyle = .White diff --git a/TelegramUI/InstantPageGalleryFooterContentNode.swift b/TelegramUI/InstantPageGalleryFooterContentNode.swift index 9a40e4c4cf..1193043dd9 100644 --- a/TelegramUI/InstantPageGalleryFooterContentNode.swift +++ b/TelegramUI/InstantPageGalleryFooterContentNode.swift @@ -55,10 +55,10 @@ final class InstantPageGalleryFooterContentNode: GalleryFooterContentNode { } } - override func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { - var panelHeight: CGFloat = 44.0 + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { + var panelHeight: CGFloat = 44.0 + bottomInset if !self.textNode.isHidden { - let sideInset: CGFloat = 8.0 + let sideInset: CGFloat = leftInset + 8.0 let topInset: CGFloat = 8.0 let bottomInset: CGFloat = 8.0 let textSize = self.textNode.measure(CGSize(width: width - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude)) @@ -66,7 +66,7 @@ final class InstantPageGalleryFooterContentNode: GalleryFooterContentNode { transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: sideInset, y: topInset), size: textSize)) } - self.actionButton.frame = CGRect(origin: CGPoint(x: 0.0, y: panelHeight - 44.0), size: CGSize(width: 44.0, height: 44.0)) + self.actionButton.frame = CGRect(origin: CGPoint(x: leftInset, y: panelHeight - bottomInset - 44.0), size: CGSize(width: 44.0, height: 44.0)) return panelHeight } diff --git a/TelegramUI/InstantPageImageItem.swift b/TelegramUI/InstantPageImageItem.swift index 8928bb06e6..b01466b2f2 100644 --- a/TelegramUI/InstantPageImageItem.swift +++ b/TelegramUI/InstantPageImageItem.swift @@ -1,6 +1,7 @@ import Foundation import Postbox import TelegramCore +import AsyncDisplayKit final class InstantPageImageItem: InstantPageItem { var frame: CGRect @@ -24,7 +25,7 @@ final class InstantPageImageItem: InstantPageItem { self.fit = fit } - func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void) -> InstantPageNode? { + func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void) -> (InstantPageNode & ASDisplayNode)? { return InstantPageImageNode(account: account, media: self.media, interactive: self.interactive, roundCorners: self.roundCorners, fit: self.fit, openMedia: openMedia) } diff --git a/TelegramUI/InstantPageImageNode.swift b/TelegramUI/InstantPageImageNode.swift index df128b06cf..3168f429e7 100644 --- a/TelegramUI/InstantPageImageNode.swift +++ b/TelegramUI/InstantPageImageNode.swift @@ -31,14 +31,14 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode { super.init() - self.imageNode.alphaTransitionOnFirstUpdate = true self.addSubnode(self.imageNode) if let image = media.media as? TelegramMediaImage { - self.imageNode.setSignal(account: account, signal: chatMessagePhoto(account: account, photo: image)) + self.imageNode.setSignal(chatMessagePhoto(postbox: account.postbox + , photo: image)) self.fetchedDisposable.set(chatMessagePhotoInteractiveFetched(account: account, photo: image).start()) } else if let file = media.media as? TelegramMediaFile { - self.imageNode.setSignal(account: account, signal: chatMessageVideo(account: account, video: file)) + self.imageNode.setSignal(chatMessageVideo(postbox: account.postbox, video: file)) } } diff --git a/TelegramUI/InstantPageItem.swift b/TelegramUI/InstantPageItem.swift index 3f0a72f44d..e3fc7e6253 100644 --- a/TelegramUI/InstantPageItem.swift +++ b/TelegramUI/InstantPageItem.swift @@ -1,6 +1,7 @@ import Foundation import Postbox import TelegramCore +import AsyncDisplayKit protocol InstantPageItem { var frame: CGRect { get set } @@ -9,7 +10,7 @@ protocol InstantPageItem { func matchesAnchor(_ anchor: String) -> Bool func drawInTile(context: CGContext) - func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void) -> InstantPageNode? + func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void) -> (InstantPageNode & ASDisplayNode)? func matchesNode(_ node: InstantPageNode) -> Bool func linkSelectionRects(at point: CGPoint) -> [CGRect] diff --git a/TelegramUI/InstantPageLayout.swift b/TelegramUI/InstantPageLayout.swift index 706b9164ca..65b01afc5a 100644 --- a/TelegramUI/InstantPageLayout.swift +++ b/TelegramUI/InstantPageLayout.swift @@ -39,10 +39,10 @@ private func setupStyleStack(_ stack: InstantPageTextStyleStack, theme: InstantP } } -func layoutInstantPageBlock(webpage: TelegramMediaWebpage, block: InstantPageBlock, boundingWidth: CGFloat, horizontalInset: CGFloat, isCover: Bool, previousItems: [InstantPageItem], fillToWidthAndHeight: Bool, media: [MediaId: Media], mediaIndexCounter: inout Int, embedIndexCounter: inout Int, theme: InstantPageTheme) -> InstantPageLayout { +func layoutInstantPageBlock(webpage: TelegramMediaWebpage, block: InstantPageBlock, boundingWidth: CGFloat, horizontalInset: CGFloat, safeInset: CGFloat, isCover: Bool, previousItems: [InstantPageItem], fillToWidthAndHeight: Bool, media: [MediaId: Media], mediaIndexCounter: inout Int, embedIndexCounter: inout Int, theme: InstantPageTheme) -> InstantPageLayout { switch block { case let .cover(block): - return layoutInstantPageBlock(webpage: webpage, block: block, boundingWidth: boundingWidth, horizontalInset: horizontalInset, isCover: true, previousItems:previousItems, fillToWidthAndHeight: fillToWidthAndHeight, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, theme: theme) + return layoutInstantPageBlock(webpage: webpage, block: block, boundingWidth: boundingWidth, horizontalInset: horizontalInset, safeInset: safeInset, isCover: true, previousItems:previousItems, fillToWidthAndHeight: fillToWidthAndHeight, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, theme: theme) case let .title(text): let styleStack = InstantPageTextStyleStack() setupStyleStack(styleStack, theme: theme, category: .header, link: false) @@ -247,24 +247,24 @@ func layoutInstantPageBlock(webpage: TelegramMediaWebpage, block: InstantPageBlo case let .image(id, caption): if let image = media[id] as? TelegramMediaImage, let largest = largestImageRepresentation(image.representations) { let imageSize = largest.dimensions - var filledSize = imageSize.aspectFitted(CGSize(width: boundingWidth, height: 1200.0)) + var filledSize = imageSize.aspectFitted(CGSize(width: boundingWidth - safeInset * 2.0, height: 1200.0)) if fillToWidthAndHeight { - filledSize = CGSize(width: boundingWidth, height: boundingWidth) + filledSize = CGSize(width: boundingWidth - safeInset * 2.0, height: boundingWidth - safeInset * 2.0) } else if isCover { - filledSize = imageSize.aspectFilled(CGSize(width: boundingWidth, height: 1.0)) + filledSize = imageSize.aspectFilled(CGSize(width: boundingWidth - safeInset * 2.0, height: 1.0)) if !filledSize.height.isZero { - filledSize = filledSize.cropped(CGSize(width: boundingWidth, height: floor(boundingWidth * 3.0 / 5.0))) + filledSize = filledSize.cropped(CGSize(width: boundingWidth - safeInset * 2.0, height: floor((boundingWidth - safeInset * 2.0) * 3.0 / 5.0))) } } let mediaIndex = mediaIndexCounter mediaIndexCounter += 1 - var contentSize = CGSize(width: boundingWidth, height: 0.0) + var contentSize = CGSize(width: boundingWidth - safeInset * 2.0, height: 0.0) var items: [InstantPageItem] = [] - let mediaItem = InstantPageImageItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - filledSize.width) / 2.0), y: 0.0), size: filledSize), media: InstantPageMedia(index: mediaIndex, media: image, caption: caption.plainText), interactive: true, roundCorners: false, fit: false) + let mediaItem = InstantPageImageItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - safeInset * 2.0 - filledSize.width) / 2.0), y: 0.0), size: filledSize), media: InstantPageMedia(index: mediaIndex, media: image, caption: caption.plainText), interactive: true, roundCorners: false, fit: false) items.append(mediaItem) contentSize.height += filledSize.height @@ -291,29 +291,29 @@ func layoutInstantPageBlock(webpage: TelegramMediaWebpage, block: InstantPageBlo case let .video(id, caption, autoplay, loop): if let file = media[id] as? TelegramMediaFile, let dimensions = file.dimensions { let imageSize = dimensions - var filledSize = imageSize.aspectFitted(CGSize(width: boundingWidth, height: 1200.0)) + var filledSize = imageSize.aspectFitted(CGSize(width: boundingWidth - safeInset * 2.0, height: 1200.0)) if fillToWidthAndHeight { - filledSize = CGSize(width: boundingWidth, height: boundingWidth) + filledSize = CGSize(width: boundingWidth - safeInset * 2.0, height: boundingWidth - safeInset * 2.0) } else if isCover { - filledSize = imageSize.aspectFilled(CGSize(width: boundingWidth, height: 1.0)) + filledSize = imageSize.aspectFilled(CGSize(width: boundingWidth - safeInset * 2.0, height: 1.0)) if !filledSize.height.isZero { - filledSize = filledSize.cropped(CGSize(width: boundingWidth, height: floor(boundingWidth * 3.0 / 5.0))) + filledSize = filledSize.cropped(CGSize(width: boundingWidth - safeInset * 2.0, height: floor((boundingWidth - safeInset * 2.0) * 3.0 / 5.0))) } } let mediaIndex = mediaIndexCounter mediaIndexCounter += 1 - var contentSize = CGSize(width: boundingWidth, height: 0.0) + var contentSize = CGSize(width: boundingWidth - safeInset * 2.0, height: 0.0) var items: [InstantPageItem] = [] if autoplay { - let mediaItem = InstantPagePlayableVideoItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - filledSize.width) / 2.0), y: 0.0), size: filledSize), media: InstantPageMedia(index: mediaIndex, media: file, caption: caption.plainText), interactive: true) + let mediaItem = InstantPagePlayableVideoItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - safeInset * 2.0 - filledSize.width) / 2.0), y: 0.0), size: filledSize), media: InstantPageMedia(index: mediaIndex, media: file, caption: caption.plainText), interactive: true) items.append(mediaItem) } else { - let mediaItem = InstantPageImageItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - filledSize.width) / 2.0), y: 0.0), size: filledSize), media: InstantPageMedia(index: mediaIndex, media: file, caption: caption.plainText), interactive: true, roundCorners: false, fit: false) + let mediaItem = InstantPageImageItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - safeInset * 2.0 - filledSize.width) / 2.0), y: 0.0), size: filledSize), media: InstantPageMedia(index: mediaIndex, media: file, caption: caption.plainText), interactive: true, roundCorners: false, fit: false) items.append(mediaItem) } @@ -341,7 +341,7 @@ func layoutInstantPageBlock(webpage: TelegramMediaWebpage, block: InstantPageBlo case let .collage(items: innerItems, caption: caption): let spacing: CGFloat = 2.0 let itemsPerRow = 3 - let itemSize = floor((boundingWidth - spacing * max(0.0, CGFloat(itemsPerRow - 1))) / CGFloat(itemsPerRow)) + let itemSize = floor((boundingWidth - safeInset * 2.0 - spacing * max(0.0, CGFloat(itemsPerRow - 1))) / CGFloat(itemsPerRow)) var items: [InstantPageItem] = [] @@ -351,12 +351,12 @@ func layoutInstantPageBlock(webpage: TelegramMediaWebpage, block: InstantPageBlo nextItemOrigin.x = 0.0 nextItemOrigin.y += itemSize + spacing } - let subLayout = layoutInstantPageBlock(webpage: webpage, block: subItem, boundingWidth: itemSize, horizontalInset: 0.0, isCover: false, previousItems: items, fillToWidthAndHeight: true, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, theme: theme) + let subLayout = layoutInstantPageBlock(webpage: webpage, block: subItem, boundingWidth: itemSize, horizontalInset: 0.0, safeInset: 0.0, isCover: false, previousItems: items, fillToWidthAndHeight: true, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, theme: theme) items.append(contentsOf: subLayout.flattenedItemsWithOrigin(nextItemOrigin)) nextItemOrigin.x += itemSize + spacing } - var contentSize = CGSize(width: boundingWidth, height: nextItemOrigin.y + itemSize) + var contentSize = CGSize(width: boundingWidth - safeInset * 2.0, height: nextItemOrigin.y + itemSize) if case .empty = caption { } else { @@ -365,7 +365,7 @@ func layoutInstantPageBlock(webpage: TelegramMediaWebpage, block: InstantPageBlo let styleStack = InstantPageTextStyleStack() setupStyleStack(styleStack, theme: theme, category: .caption, link: false) - let captionItem = layoutTextItemWithString(attributedStringForRichText(caption, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0) + let captionItem = layoutTextItemWithString(attributedStringForRichText(caption, styleStack: styleStack), boundingWidth: boundingWidth - safeInset * 2.0 - horizontalInset * 2.0) captionItem.frame = captionItem.frame.offsetBy(dx: floor(boundingWidth - captionItem.frame.size.width) / 2.0, dy: contentSize.height) captionItem.alignment = .center @@ -432,7 +432,7 @@ func layoutInstantPageBlock(webpage: TelegramMediaWebpage, block: InstantPageBlo var previousBlock: InstantPageBlock? for subBlock in blocks { - let subLayout = layoutInstantPageBlock(webpage: webpage, block: subBlock, boundingWidth: boundingWidth - horizontalInset * 2.0 - lineInset, horizontalInset: 0.0, isCover: false, previousItems: items, fillToWidthAndHeight: false, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, theme: theme) + let subLayout = layoutInstantPageBlock(webpage: webpage, block: subBlock, boundingWidth: boundingWidth - horizontalInset * 2.0 - lineInset, horizontalInset: 0.0, safeInset: 0.0, isCover: false, previousItems: items, fillToWidthAndHeight: false, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, theme: theme) let spacing = spacingBetweenBlocks(upper: previousBlock, lower: subBlock) let blockItems = subLayout.flattenedItemsWithOrigin(CGPoint(x: horizontalInset + lineInset, y: contentSize.height + spacing)) @@ -575,7 +575,7 @@ func layoutInstantPageBlock(webpage: TelegramMediaWebpage, block: InstantPageBlo } } -func instantPageLayoutForWebPage(_ webPage: TelegramMediaWebpage, boundingWidth: CGFloat, strings: PresentationStrings, theme: InstantPageTheme) -> InstantPageLayout { +func instantPageLayoutForWebPage(_ webPage: TelegramMediaWebpage, boundingWidth: CGFloat, safeInset: CGFloat, strings: PresentationStrings, theme: InstantPageTheme) -> InstantPageLayout { var maybeLoadedContent: TelegramMediaWebpageLoadedContent? if case let .Loaded(content) = webPage.content { maybeLoadedContent = content @@ -599,7 +599,7 @@ func instantPageLayoutForWebPage(_ webPage: TelegramMediaWebpage, boundingWidth: var previousBlock: InstantPageBlock? for block in pageBlocks { - let blockLayout = layoutInstantPageBlock(webpage: webPage, block: block, boundingWidth: boundingWidth, horizontalInset: 17.0, isCover: false, previousItems: items, fillToWidthAndHeight: false, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, theme: theme) + let blockLayout = layoutInstantPageBlock(webpage: webPage, block: block, boundingWidth: boundingWidth, horizontalInset: 17.0 + safeInset, safeInset: safeInset, isCover: false, previousItems: items, fillToWidthAndHeight: false, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, theme: theme) let spacing = spacingBetweenBlocks(upper: previousBlock, lower: block) let blockItems = blockLayout.flattenedItemsWithOrigin(CGPoint(x: 0.0, y: contentSize.height + spacing)) items.append(contentsOf: blockItems) diff --git a/TelegramUI/InstantPageNavigationBar.swift b/TelegramUI/InstantPageNavigationBar.swift index 206b47c0e4..36d15d013d 100644 --- a/TelegramUI/InstantPageNavigationBar.swift +++ b/TelegramUI/InstantPageNavigationBar.swift @@ -103,30 +103,37 @@ final class InstantPageNavigationBar: ASDisplayNode { } } - func updateLayout(size: CGSize, pageProgress: CGFloat, transition: ContainedViewLayoutTransition) { - transition.updateFrame(node: self.pageProgressNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: floorToScreenPixels(size.width * pageProgress), height: size.height))) + func updateLayout(size: CGSize, minHeight: CGFloat, maxHeight: CGFloat, topInset: CGFloat, leftInset: CGFloat, rightInset: CGFloat, pageProgress: CGFloat, transition: ContainedViewLayoutTransition) { + let progressHeight: CGFloat + if !topInset.isZero { + progressHeight = size.height - topInset + 11.0 + } else { + progressHeight = size.height + } + transition.updateFrame(node: self.pageProgressNode, frame: CGRect(origin: CGPoint(x: 0.0, y: size.height - progressHeight), size: CGSize(width: floorToScreenPixels(size.width * pageProgress), height: progressHeight))) + + let transitionFactor = (size.height - minHeight) / (maxHeight - minHeight) transition.updateFrame(node: self.backButton, frame: CGRect(origin: CGPoint(x: 1.0, y: 0.0), size: CGSize(width: 100.0, height: size.height))) if let image = arrowNode.image { let arrowImageSize = image.size let arrowHeight: CGFloat - if size.height.isLess(than: 64.0) { - arrowHeight = 9.0 * size.height / 44.0 + 87.0 / 11.0; + if size.height.isLess(than: maxHeight) { + arrowHeight = floor(9.0 * transitionFactor + 12.0) } else { arrowHeight = 21.0 } let scaledArrowSize = CGSize(width: arrowImageSize.width * arrowHeight / arrowImageSize.height, height: arrowHeight) - transition.updateFrame(node: self.arrowNode, frame: CGRect(origin: CGPoint(x: 8.0, y: max(0.0, size.height - 44.0) + floor((min(size.height, 44.0) - scaledArrowSize.height) / 2.0)), size: scaledArrowSize)) + let arrowOffset = floor(8.0 * transitionFactor + 4.0) + transition.updateFrame(node: self.arrowNode, frame: CGRect(origin: CGPoint(x: leftInset + 8.0, y: size.height - arrowHeight - arrowOffset), size: scaledArrowSize)) } let offsetScaleFactor: CGFloat let buttonScaleFactor: CGFloat - if size.height.isLess(than: 64.0) { - offsetScaleFactor = max(size.height - 20.0, 0.0) / 44.0 - let k = (self.intrinsicMoreSize.height - self.intrinsicSmallMoreSize.height) / 44.0 - let b = self.intrinsicSmallMoreSize.height - k * 20.0; - buttonScaleFactor = (k * size.height + b) / self.intrinsicMoreSize.height + if size.height.isLess(than: maxHeight) { + offsetScaleFactor = transitionFactor + buttonScaleFactor = ((transitionFactor * self.intrinsicMoreSize.height) + ((1.0 - transitionFactor) * self.intrinsicSmallMoreSize.height)) / self.intrinsicMoreSize.height } else { offsetScaleFactor = 1.0 buttonScaleFactor = 1.0 @@ -138,14 +145,18 @@ final class InstantPageNavigationBar: ASDisplayNode { alphaFactor *= 0.5 } + let maxMoreOffset = self.intrinsicMoreSize.height / 2.0 + floor((44.0 - self.intrinsicMoreSize.height) / 2.0) + let minMoreOffset = self.intrinsicSmallMoreSize.height / 2.0 + floor((20.0 - self.intrinsicSmallMoreSize.height) / 2.0) + let moreOffset = (transitionFactor * maxMoreOffset) + ((1.0 - transitionFactor) * minMoreOffset) + transition.updateTransformScale(node: self.moreButton, scale: buttonScaleFactor) - transition.updatePosition(node: self.moreButton, position: CGPoint(x: size.width - buttonScaleFactor * self.intrinsicMoreSize.width / 2.0, y: offsetScaleFactor * 20.0 + buttonScaleFactor * self.intrinsicMoreSize.height / 2.0)) + transition.updatePosition(node: self.moreButton, position: CGPoint(x: size.width - rightInset - buttonScaleFactor * self.intrinsicMoreSize.width / 2.0, y: size.height - moreOffset)) transition.updateAlpha(node: self.moreButton, alpha: alphaFactor) transition.updateTransformScale(node: self.actionButton, scale: buttonScaleFactor) - transition.updatePosition(node: self.actionButton, position: CGPoint(x: size.width - buttonScaleFactor * self.intrinsicMoreSize.width - buttonScaleFactor * self.intrinsicActionSize.width / 2.0, y: offsetScaleFactor * 20.0 + buttonScaleFactor * self.intrinsicActionSize.height / 2.0)) + transition.updatePosition(node: self.actionButton, position: CGPoint(x: size.width - rightInset - buttonScaleFactor * self.intrinsicMoreSize.width - buttonScaleFactor * self.intrinsicActionSize.width / 2.0, y: size.height - moreOffset)) transition.updateAlpha(node: self.actionButton, alpha: alphaFactor) - transition.updateFrame(node: self.scrollToTopButton, frame: CGRect(origin: CGPoint(x: 64.0, y: 0.0), size: CGSize(width: size.width - 64.0, height: size.height))) + transition.updateFrame(node: self.scrollToTopButton, frame: CGRect(origin: CGPoint(x: leftInset + 64.0, y: 0.0), size: CGSize(width: size.width - leftInset - rightInset - 64.0, height: size.height))) } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { diff --git a/TelegramUI/InstantPageNode.swift b/TelegramUI/InstantPageNode.swift index 1b0aeff187..5f87117859 100644 --- a/TelegramUI/InstantPageNode.swift +++ b/TelegramUI/InstantPageNode.swift @@ -8,18 +8,3 @@ protocol InstantPageNode { func updateHiddenMedia(media: InstantPageMedia?) func update(strings: PresentationStrings, theme: InstantPageTheme) } - -/*@class TGInstantPageMedia; - -@protocol TGInstantPageDisplayView - -- (void)setIsVisible:(bool)isVisible; - -@optional - -- (void)setOpenMedia:(void (^)(id))openMedia; -- (void)setOpenFeedback:(void (^)())openFeedback; -- (UIView *)transitionViewForMedia:(TGInstantPageMedia *)media; -- (void)updateHiddenMedia:(TGInstantPageMedia *)media; - -@end*/ diff --git a/TelegramUI/InstantPagePeerReferenceItem.swift b/TelegramUI/InstantPagePeerReferenceItem.swift index 4c70086867..8157c53ce9 100644 --- a/TelegramUI/InstantPagePeerReferenceItem.swift +++ b/TelegramUI/InstantPagePeerReferenceItem.swift @@ -1,6 +1,7 @@ import Foundation import Postbox import TelegramCore +import AsyncDisplayKit final class InstantPagePeerReferenceItem: InstantPageItem { var frame: CGRect @@ -16,7 +17,7 @@ final class InstantPagePeerReferenceItem: InstantPageItem { self.rtl = rtl } - func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void) -> InstantPageNode? { + func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void) -> (InstantPageNode & ASDisplayNode)? { return InstantPagePeerReferenceNode(account: account, strings: strings, theme: theme, initialPeer: self.initialPeer, rtl: self.rtl, openPeer: openPeer) } diff --git a/TelegramUI/InstantPagePeerReferenceNode.swift b/TelegramUI/InstantPagePeerReferenceNode.swift index 0361a548ca..0d727ce7ce 100644 --- a/TelegramUI/InstantPagePeerReferenceNode.swift +++ b/TelegramUI/InstantPagePeerReferenceNode.swift @@ -85,7 +85,7 @@ final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode { self.joinNode = HighlightableButtonNode() self.joinNode.hitTestSlop = UIEdgeInsets(top: -17.0, left: -17.0, bottom: -17.0, right: -17.0) - self.activityIndicator = ActivityIndicator(type: .custom(theme.panelAccentColor)) + self.activityIndicator = ActivityIndicator(type: .custom(theme.panelAccentColor, 22.0)) self.checkNode = ASImageNode() self.checkNode.isLayerBacked = true @@ -177,7 +177,7 @@ final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode { if themeUpdated { self.checkNode.image = generateTintedImage(image: UIImage(bundleImageName: "Instant View/PanelCheck"), color: self.theme.panelSecondaryColor) - self.activityIndicator.type = .custom(self.theme.panelAccentColor) + self.activityIndicator.type = .custom(self.theme.panelAccentColor, 22.0) } self.setNeedsLayout() } diff --git a/TelegramUI/InstantPagePlayableVideoItem.swift b/TelegramUI/InstantPagePlayableVideoItem.swift index f0acb02b92..8e7dcdad8c 100644 --- a/TelegramUI/InstantPagePlayableVideoItem.swift +++ b/TelegramUI/InstantPagePlayableVideoItem.swift @@ -1,6 +1,7 @@ import Foundation import Postbox import TelegramCore +import AsyncDisplayKit final class InstantPagePlayableVideoItem: InstantPageItem { var frame: CGRect @@ -20,7 +21,7 @@ final class InstantPagePlayableVideoItem: InstantPageItem { self.interactive = interactive } - func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void) -> InstantPageNode? { + func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void) -> (InstantPageNode & ASDisplayNode)? { return InstantPagePlayableVideoNode(account: account, media: self.media, interactive: self.interactive, openMedia: openMedia) } diff --git a/TelegramUI/InstantPagePlayableVideoNode.swift b/TelegramUI/InstantPagePlayableVideoNode.swift index 3ae996225e..b882b0e746 100644 --- a/TelegramUI/InstantPagePlayableVideoNode.swift +++ b/TelegramUI/InstantPagePlayableVideoNode.swift @@ -31,12 +31,12 @@ final class InstantPagePlayableVideoNode: ASDisplayNode, InstantPageNode { super.init() - self.imageNode.alphaTransitionOnFirstUpdate = true + self.imageNode.contentAnimations = [.firstUpdate] self.addSubnode(self.imageNode) self.addSubnode(self.videoNode) if let file = media.media as? TelegramMediaFile { - self.imageNode.setSignal(account: account, signal: chatMessageVideo(account: account, video: file)) + self.imageNode.setSignal(chatMessageVideo(postbox: account.postbox, video: file)) self.fetchedDisposable.set(freeMediaFileInteractiveFetched(account: account, file: file).start()) } } diff --git a/TelegramUI/InstantPageSettingsNode.swift b/TelegramUI/InstantPageSettingsNode.swift index 34427ca7bc..02959113a9 100644 --- a/TelegramUI/InstantPageSettingsNode.swift +++ b/TelegramUI/InstantPageSettingsNode.swift @@ -186,11 +186,16 @@ final class InstantPageSettingsNode: ASDisplayNode { } func animateIn() { - self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.layer.allowsGroupOpacity = true + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, completion: { [weak self] _ in + self?.layer.allowsGroupOpacity = false + }) } func animateOut(completion: @escaping () -> Void) { - self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in + self.layer.allowsGroupOpacity = true + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak self] _ in + self?.layer.allowsGroupOpacity = false completion() }) } diff --git a/TelegramUI/InstantPageShapeItem.swift b/TelegramUI/InstantPageShapeItem.swift index 0fb3e40477..197efbb264 100644 --- a/TelegramUI/InstantPageShapeItem.swift +++ b/TelegramUI/InstantPageShapeItem.swift @@ -1,6 +1,7 @@ import Foundation import Postbox import TelegramCore +import AsyncDisplayKit enum InstantPageShape { case rect @@ -55,7 +56,7 @@ final class InstantPageShapeItem: InstantPageItem { return false } - func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void) -> InstantPageNode? { + func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void) -> (InstantPageNode & ASDisplayNode)? { return nil } diff --git a/TelegramUI/InstantPageSlideshowItem.swift b/TelegramUI/InstantPageSlideshowItem.swift index 4c0937b49d..e67dcce888 100644 --- a/TelegramUI/InstantPageSlideshowItem.swift +++ b/TelegramUI/InstantPageSlideshowItem.swift @@ -1,6 +1,7 @@ import Foundation import Postbox import TelegramCore +import AsyncDisplayKit final class InstantPageSlideshowItem: InstantPageItem { var frame: CGRect @@ -12,7 +13,7 @@ final class InstantPageSlideshowItem: InstantPageItem { self.medias = medias } - func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void) -> InstantPageNode? { + func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void) -> (InstantPageNode & ASDisplayNode)? { return InstantPageSlideshowNode(account: account, medias: self.medias, openMedia: openMedia) } diff --git a/TelegramUI/InstantPageSlideshowItemNode.swift b/TelegramUI/InstantPageSlideshowItemNode.swift index c2ef6dbea4..6ccf5180cc 100644 --- a/TelegramUI/InstantPageSlideshowItemNode.swift +++ b/TelegramUI/InstantPageSlideshowItemNode.swift @@ -395,7 +395,7 @@ final class InstantPageSlideshowNode: ASDisplayNode, InstantPageNode { super.layout() self.pagerNode.frame = self.bounds - self.pagerNode.containerLayoutUpdated(ContainerViewLayout(size: self.bounds.size, metrics: LayoutMetrics(), intrinsicInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil), transition: .immediate) + self.pagerNode.containerLayoutUpdated(ContainerViewLayout(size: self.bounds.size, metrics: LayoutMetrics(), intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false), transition: .immediate) self.pageControlNode.frame = CGRect(origin: CGPoint(x: 0.0, y: self.bounds.size.height - 20.0), size: CGSize(width: self.bounds.size.width, height: 20.0)) } diff --git a/TelegramUI/InstantPageTextItem.swift b/TelegramUI/InstantPageTextItem.swift index e953de5200..e735659421 100644 --- a/TelegramUI/InstantPageTextItem.swift +++ b/TelegramUI/InstantPageTextItem.swift @@ -1,6 +1,7 @@ import Foundation import TelegramCore import Postbox +import AsyncDisplayKit final class InstantPageUrlItem { let url: String @@ -240,7 +241,7 @@ final class InstantPageTextItem: InstantPageItem { return false } - func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void) -> InstantPageNode? { + func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void) -> (InstantPageNode & ASDisplayNode)? { return nil } diff --git a/TelegramUI/InstantPageTheme.swift b/TelegramUI/InstantPageTheme.swift index f87385ff85..f8dd8a713e 100644 --- a/TelegramUI/InstantPageTheme.swift +++ b/TelegramUI/InstantPageTheme.swift @@ -177,7 +177,20 @@ private func fontSizeMultiplierForVariant(_ variant: InstantPagePresentationFont } } -func instantPageThemeForSettings(_ settings: InstantPagePresentationSettings) -> InstantPageTheme { +func instantPageThemeForSettingsAndTime(settings: InstantPagePresentationSettings, time: Date) -> InstantPageTheme { + if settings.autoNightMode { + switch settings.themeType { + case .light, .sepia, .gray: + let calendar = Calendar.current + let hour = calendar.component(.hour, from: time) + if hour <= 8 || hour >= 22 { + return darkTheme.withUpdatedFontStyles(sizeMultiplier: fontSizeMultiplierForVariant(settings.fontSize), forceSerif: settings.forceSerif) + } + case .dark: + break + } + } + switch settings.themeType { case .light: return lightTheme.withUpdatedFontStyles(sizeMultiplier: fontSizeMultiplierForVariant(settings.fontSize), forceSerif: settings.forceSerif) diff --git a/TelegramUI/InstantPageWebEmbedItem.swift b/TelegramUI/InstantPageWebEmbedItem.swift index ff998c2d74..14546e807b 100644 --- a/TelegramUI/InstantPageWebEmbedItem.swift +++ b/TelegramUI/InstantPageWebEmbedItem.swift @@ -1,6 +1,7 @@ import Foundation import Postbox import TelegramCore +import AsyncDisplayKit final class InstantPageWebEmbedItem: InstantPageItem { var frame: CGRect @@ -18,7 +19,7 @@ final class InstantPageWebEmbedItem: InstantPageItem { self.enableScrolling = enableScrolling } - func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void) -> InstantPageNode? { + func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void) -> (InstantPageNode & ASDisplayNode)? { return instantPageWebEmbedNode(frame: self.frame, url: self.url, html: self.html, enableScrolling: self.enableScrolling) } diff --git a/TelegramUI/InstantPageWebEmbedNode.swift b/TelegramUI/InstantPageWebEmbedNode.swift index 2f2de22d9d..8060e3ee8a 100644 --- a/TelegramUI/InstantPageWebEmbedNode.swift +++ b/TelegramUI/InstantPageWebEmbedNode.swift @@ -21,12 +21,20 @@ final class instantPageWebEmbedNode: ASDisplayNode, InstantPageNode { self.webView.loadHTMLString(html, baseURL: nil) } else if let url = url, let parsedUrl = URL(string: url) { var request = URLRequest(url: parsedUrl) - let referrer = "\(parsedUrl.scheme)://\(parsedUrl.host)" - request.setValue(referrer, forHTTPHeaderField: "Referer") + if let scheme = parsedUrl.scheme, let host = parsedUrl.host { + let referrer = "\(scheme)://\(host)" + request.setValue(referrer, forHTTPHeaderField: "Referer") + } self.webView.load(request) } } + override func didLoad() { + super.didLoad() + + self.view.addSubview(self.webView) + } + override func layout() { super.layout() diff --git a/TelegramUI/InstantVideoNode.swift b/TelegramUI/InstantVideoNode.swift index 63150c2c13..d6b2167a75 100644 --- a/TelegramUI/InstantVideoNode.swift +++ b/TelegramUI/InstantVideoNode.swift @@ -49,12 +49,17 @@ private final class SharedInstantVideoContext: SharedVideoContext { func setSoundEnabled(_ value: Bool) { assert(Queue.mainQueue().isCurrent()) if value { - self.player.playOnceWithSound() + self.player.playOnceWithSound(playAndRecord: true) } else { self.player.continuePlayingWithoutSound() } } + func setForceAudioToSpeaker(_ value: Bool) { + assert(Queue.mainQueue().isCurrent()) + self.player.setForceAudioToSpeaker(value) + } + func seek(_ timestamp: Double) { assert(Queue.mainQueue().isCurrent()) self.player.seek(timestamp: timestamp) @@ -104,6 +109,7 @@ final class InstantVideoNode: OverlayMediaItemNode { private let postbox: Postbox private var soundEnabled: Bool + private var forceAudioToSpeaker: Bool private var contextId: Int32? @@ -138,14 +144,15 @@ final class InstantVideoNode: OverlayMediaItemNode { return true } - init(theme: PresentationTheme, manager: MediaManager, account: Account, source: InstantVideoNodeSource, priority: Int32, withSound: Bool) { + init(theme: PresentationTheme, manager: MediaManager, postbox: Postbox, source: InstantVideoNodeSource, priority: Int32, withSound: Bool, forceAudioToSpeaker: Bool) { self.theme = theme self.manager = manager self.source = source self.priority = priority self.withSound = withSound + self.forceAudioToSpeaker = forceAudioToSpeaker self.soundEnabled = withSound - self.postbox = account.postbox + self.postbox = postbox self.backgroundNode = ASImageNode() self.backgroundNode.displayWithoutProcessing = true @@ -161,7 +168,7 @@ final class InstantVideoNode: OverlayMediaItemNode { self.addSubnode(self.backgroundNode) self.addSubnode(self.imageNode) - self.imageNode.setSignal(account: account, signal: chatMessageVideo(account: account, video: source.file)) + self.imageNode.setSignal(chatMessageVideo(postbox: postbox, video: source.file)) self.manager.sharedVideoContextManager.withSharedVideoContext(id: self.source.id, { [weak self] context in if let strongSelf = self, let context = context as? SharedInstantVideoContext { @@ -298,6 +305,15 @@ final class InstantVideoNode: OverlayMediaItemNode { }) } + func setForceAudioToSpeaker(_ value: Bool) { + self.forceAudioToSpeaker = value + self.manager.sharedVideoContextManager.withSharedVideoContext(id: self.source.id, { context in + if let context = context as? SharedInstantVideoContext { + context.setForceAudioToSpeaker(value) + } + }) + } + func seek(_ timestamp: Double) { self.manager.sharedVideoContextManager.withSharedVideoContext(id: self.source.id, { context in if let context = context as? SharedInstantVideoContext { @@ -312,6 +328,7 @@ final class InstantVideoNode: OverlayMediaItemNode { self.contextId = self.manager.sharedVideoContextManager.attachSharedVideoContext(id: source.id, priority: self.priority, create: { let context = SharedInstantVideoContext(audioSessionManager: manager.audioSession, postbox: self.postbox, resource: self.source.resource) context.setSoundEnabled(self.soundEnabled) + context.setForceAudioToSpeaker(self.forceAudioToSpeaker) context.play() return context }, update: { [weak self] context in diff --git a/TelegramUI/InstantVideoRadialStatusNode.swift b/TelegramUI/InstantVideoRadialStatusNode.swift new file mode 100644 index 0000000000..de0db2307c --- /dev/null +++ b/TelegramUI/InstantVideoRadialStatusNode.swift @@ -0,0 +1,157 @@ +import Foundation +import AsyncDisplayKit +import Display +import SwiftSignalKit + +import LegacyComponents + +private final class InstantVideoRadialStatusNodeParameters: NSObject { + let color: UIColor + let progress: CGFloat + + init(color: UIColor, progress: CGFloat) { + self.color = color + self.progress = progress + } +} + +final class InstantVideoRadialStatusNode: ASDisplayNode { + private let color: UIColor + + private var effectiveProgress: CGFloat = 0.0 { + didSet { + self.setNeedsDisplay() + } + } + + private var _statusValue: MediaPlayerStatus? + private var statusValue: MediaPlayerStatus? { + get { + return self._statusValue + } set(value) { + if value != self._statusValue { + self._statusValue = value + self.updateProgress() + } + } + } + + private var statusDisposable: Disposable? + private var statusValuePromise = Promise() + + var status: Signal? { + didSet { + if let status = self.status { + self.statusValuePromise.set(status |> map { $0 }) + } else { + self.statusValuePromise.set(.single(nil)) + } + } + } + + init(color: UIColor) { + self.color = color + + super.init() + + self.isOpaque = false + + self.statusDisposable = (self.statusValuePromise.get() + |> deliverOnMainQueue).start(next: { [weak self] status in + if let strongSelf = self { + strongSelf.statusValue = status + } + }) + } + + deinit { + self.statusDisposable?.dispose() + } + + override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { + return InstantVideoRadialStatusNodeParameters(color: self.color, progress: self.effectiveProgress) + } + + @objc override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) { + let context = UIGraphicsGetCurrentContext()! + + if !isRasterizing { + context.setBlendMode(.copy) + context.setFillColor(UIColor.clear.cgColor) + context.fill(bounds) + } + + if let parameters = parameters as? InstantVideoRadialStatusNodeParameters { + context.setStrokeColor(parameters.color.cgColor) + + var progress = parameters.progress + let startAngle = -CGFloat.pi / 2.0 + let endAngle = CGFloat(progress) * 2.0 * CGFloat.pi + startAngle + + progress = min(1.0, progress) + + let lineWidth: CGFloat = 4.0 + + let pathDiameter = bounds.size.width - lineWidth + + let path = UIBezierPath(arcCenter: CGPoint(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0), radius: pathDiameter / 2.0, startAngle: startAngle, endAngle: endAngle, clockwise:true) + path.lineWidth = lineWidth + path.lineCapStyle = .round + path.stroke() + } + } + + private func updateProgress() { + let timestampAndDuration: (timestamp: Double, duration: Double)? + if let statusValue = self.statusValue, Double(0.0).isLess(than: statusValue.duration) { + timestampAndDuration = (statusValue.timestamp, statusValue.duration) + } else { + timestampAndDuration = nil + } + + if let (timestamp, duration) = timestampAndDuration, let statusValue = self.statusValue { + let progress = CGFloat(timestamp / duration) + + if progress.isNaN || !progress.isFinite || statusValue.generationTimestamp.isZero { + self.pop_removeAnimation(forKey: "progress") + self.effectiveProgress = 0.0 + } else if statusValue.status != .playing { + self.pop_removeAnimation(forKey: "progress") + self.effectiveProgress = progress + } else { + self.pop_removeAnimation(forKey: "progress") + + let animation = POPBasicAnimation() + animation.property = POPAnimatableProperty.property(withName: "progress", initializer: { property in + property?.readBlock = { node, values in + values?.pointee = (node as! InstantVideoRadialStatusNode).effectiveProgress + } + property?.writeBlock = { node, values in + (node as! InstantVideoRadialStatusNode).effectiveProgress = values!.pointee + } + property?.threshold = 0.01 + }) as! POPAnimatableProperty + animation.fromValue = progress as NSNumber + animation.toValue = 1.0 as NSNumber + animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear) + animation.duration = max(0.0, duration - timestamp) + animation.completionBlock = { [weak self] _, _ in + + } + animation.beginTime = statusValue.generationTimestamp + //animation.offset = timestamp + self.pop_add(animation, forKey: "progress") + + /*let fromBounds = CGRect(origin: CGPoint(), size: fromRect.size) + let toBounds = CGRect(origin: CGPoint(), size: toRect.size) + + foregroundNode.frame = toRect + foregroundNode.layer.add(self.preparedAnimation(keyPath: "bounds", from: NSValue(cgRect: fromBounds), to: NSValue(cgRect: toBounds), duration: duration, beginTime: statusValue.generationTimestamp, offset: timestamp, speed: statusValue.status == .playing ? 1.0 : 0.0), forKey: "playback-bounds") + foregroundNode.layer.add(self.preparedAnimation(keyPath: "position", from: NSValue(cgPoint: CGPoint(x: fromRect.midX, y: fromRect.midY)), to: NSValue(cgPoint: CGPoint(x: toRect.midX, y: toRect.midY)), duration: duration, beginTime: statusValue.generationTimestamp, offset: timestamp, speed: statusValue.status == .playing ? 1.0 : 0.0), forKey: "playback-position")*/ + } + } else { + self.pop_removeAnimation(forKey: "progress") + self.effectiveProgress = 0.0 + } + } +} diff --git a/TelegramUI/ItemListActionItem.swift b/TelegramUI/ItemListActionItem.swift index 744948202d..759c9e952f 100644 --- a/TelegramUI/ItemListActionItem.swift +++ b/TelegramUI/ItemListActionItem.swift @@ -25,12 +25,8 @@ class ItemListActionItem: ListViewItem, ItemListItem { let action: () -> Void let tag: Any? - init(theme: PresentationTheme? = nil, title: String, kind: ItemListActionKind, alignment: ItemListActionAlignment, sectionId: ItemListSectionId, style: ItemListStyle, action: @escaping () -> Void, tag: Any? = nil) { - if let theme = theme { - self.theme = theme - } else { - self.theme = defaultPresentationTheme - } + init(theme: PresentationTheme, title: String, kind: ItemListActionKind, alignment: ItemListActionAlignment, sectionId: ItemListSectionId, style: ItemListStyle, action: @escaping () -> Void, tag: Any? = nil) { + self.theme = theme self.title = title self.kind = kind self.alignment = alignment @@ -40,10 +36,10 @@ class ItemListActionItem: ListViewItem, ItemListItem { self.tag = tag } - func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { async { let node = ItemListActionItemNode() - let (layout, apply) = node.asyncLayout()(self, width, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) node.contentSize = layout.contentSize node.insets = layout.insets @@ -54,13 +50,13 @@ class ItemListActionItem: ListViewItem, ItemListItem { } } - func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { if let node = node as? ItemListActionItemNode { Queue.mainQueue().async { let makeLayout = node.asyncLayout() async { - let (layout, apply) = makeLayout(self, width, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) Queue.mainQueue().async { completion(layout, { apply() @@ -119,12 +115,12 @@ class ItemListActionItemNode: ListViewItemNode { self.addSubnode(self.titleNode) } - func asyncLayout() -> (_ item: ItemListActionItem, _ width: CGFloat, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + func asyncLayout() -> (_ item: ItemListActionItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let currentItem = self.item - return { item, width, neighbors in + return { item, params, neighbors in var updatedTheme: PresentationTheme? if currentItem?.theme !== item.theme { @@ -143,18 +139,24 @@ class ItemListActionItemNode: ListViewItemNode { textColor = item.theme.list.itemDisabledTextColor } - let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: item.title, font: titleFont, textColor: textColor), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: textColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let contentSize: CGSize let insets: UIEdgeInsets let separatorHeight = UIScreenPixel + let itemBackgroundColor: UIColor + let itemSeparatorColor: UIColor switch item.style { case .plain: - contentSize = CGSize(width: width, height: 44.0) + itemBackgroundColor = item.theme.list.plainBackgroundColor + itemSeparatorColor = item.theme.list.itemPlainSeparatorColor + contentSize = CGSize(width: params.width, height: 44.0) insets = itemListNeighborsPlainInsets(neighbors) case .blocks: - contentSize = CGSize(width: width, height: 44.0) + itemBackgroundColor = item.theme.list.itemBlocksBackgroundColor + itemSeparatorColor = item.theme.list.itemBlocksSeparatorColor + contentSize = CGSize(width: params.width, height: 44.0) insets = itemListNeighborsGroupedInsets(neighbors) } @@ -166,9 +168,9 @@ class ItemListActionItemNode: ListViewItemNode { strongSelf.item = item if let _ = updatedTheme { - strongSelf.topStripeNode.backgroundColor = item.theme.list.itemSeparatorColor - strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemSeparatorColor - strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBackgroundColor + strongSelf.topStripeNode.backgroundColor = itemSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor + strongSelf.backgroundNode.backgroundColor = itemBackgroundColor strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor } @@ -178,7 +180,7 @@ class ItemListActionItemNode: ListViewItemNode { switch item.style { case .plain: - leftInset = 35.0 + leftInset = 35.0 + params.leftInset if strongSelf.backgroundNode.supernode != nil { strongSelf.backgroundNode.removeFromSupernode() @@ -190,9 +192,9 @@ class ItemListActionItemNode: ListViewItemNode { strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0) } - strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: width - leftInset, height: separatorHeight)) + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight)) case .blocks: - leftInset = 16.0 + leftInset = 16.0 + params.leftInset if strongSelf.backgroundNode.supernode == nil { strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) @@ -213,32 +215,32 @@ class ItemListActionItemNode: ListViewItemNode { let bottomStripeOffset: CGFloat switch neighbors.bottom { case .sameSection(false): - bottomStripeInset = 16.0 + bottomStripeInset = 16.0 + params.leftInset bottomStripeOffset = -separatorHeight default: bottomStripeInset = 0.0 bottomStripeOffset = 0.0 } - strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) - strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) - strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight)) + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight)) } switch item.alignment { case .natural: strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 11.0), size: titleLayout.size) case .center: - strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: floor((width - titleLayout.size.width) / 2.0), y: 11.0), size: titleLayout.size) + strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: params.leftInset + floor((params.width - params.leftInset - params.rightInset - titleLayout.size.width) / 2.0), y: 11.0), size: titleLayout.size) } - strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: width, height: 44.0 + UIScreenPixel + UIScreenPixel)) + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: 44.0 + UIScreenPixel + UIScreenPixel)) } }) } } - override func setHighlighted(_ highlighted: Bool, animated: Bool) { - super.setHighlighted(highlighted, animated: animated) + override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { + super.setHighlighted(highlighted, at: point, animated: animated) if highlighted { self.highlightedBackgroundNode.alpha = 1.0 diff --git a/TelegramUI/ItemListActivityTextItem.swift b/TelegramUI/ItemListActivityTextItem.swift index 17971c4faf..39fe56f760 100644 --- a/TelegramUI/ItemListActivityTextItem.swift +++ b/TelegramUI/ItemListActivityTextItem.swift @@ -5,21 +5,23 @@ import SwiftSignalKit class ItemListActivityTextItem: ListViewItem, ItemListItem { let displayActivity: Bool + let theme: PresentationTheme let text: NSAttributedString let sectionId: ItemListSectionId let isAlwaysPlain: Bool = true - init(displayActivity: Bool, text: NSAttributedString, sectionId: ItemListSectionId) { + init(displayActivity: Bool, theme: PresentationTheme, text: NSAttributedString, sectionId: ItemListSectionId) { self.displayActivity = displayActivity + self.theme = theme self.text = text self.sectionId = sectionId } - func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { async { let node = ItemListActivityTextItemNode() - let (layout, apply) = node.asyncLayout()(self, width, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) node.contentSize = layout.contentSize node.insets = layout.insets @@ -30,7 +32,7 @@ class ItemListActivityTextItem: ListViewItem, ItemListItem { } } - func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { guard let node = node as? ItemListActivityTextItemNode else { assertionFailure() return @@ -40,7 +42,7 @@ class ItemListActivityTextItem: ListViewItem, ItemListItem { let makeLayout = node.asyncLayout() async { - let (layout, apply) = makeLayout(self, width, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) Queue.mainQueue().async { completion(layout, { apply() @@ -55,7 +57,7 @@ private let titleFont = Font.regular(14.0) class ItemListActivityTextItemNode: ListViewItemNode { private let titleNode: TextNode - private var activityIndicator: UIActivityIndicatorView? + private let activityIndicator: ActivityIndicator private var item: ItemListActivityTextItem? @@ -65,34 +67,19 @@ class ItemListActivityTextItemNode: ListViewItemNode { self.titleNode.contentMode = .left self.titleNode.contentsScale = UIScreen.main.scale + self.activityIndicator = ActivityIndicator(type: ActivityIndicatorType.custom(.black, 16.0)) + super.init(layerBacked: false, dynamicBounce: false) self.addSubnode(self.titleNode) + self.addSubnode(self.activityIndicator) } - override func didLoad() { - super.didLoad() - - let activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: .gray) - self.activityIndicator = activityIndicator - self.view.addSubview(activityIndicator) - activityIndicator.frame = CGRect(origin: CGPoint(x: 15.0, y: 6.0), size: activityIndicator.bounds.size) - - if let item = self.item { - if item.displayActivity { - activityIndicator.isHidden = false - activityIndicator.startAnimating() - } else { - activityIndicator.isHidden = true - } - } - } - - func asyncLayout() -> (_ item: ItemListActivityTextItem, _ width: CGFloat, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + func asyncLayout() -> (_ item: ItemListActivityTextItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) - return { item, width, neighbors in - let leftInset: CGFloat = 15.0 + return { item, params, neighbors in + let leftInset: CGFloat = 12.0 + params.leftInset let verticalInset: CGFloat = 7.0 var activityWidth: CGFloat = 0.0 @@ -104,12 +91,12 @@ class ItemListActivityTextItemNode: ListViewItemNode { titleString.removeAttribute(NSAttributedStringKey.font, range: NSMakeRange(0, titleString.length)) titleString.addAttributes([NSAttributedStringKey.font: titleFont], range: NSMakeRange(0, titleString.length)) - let (titleLayout, titleApply) = makeTitleLayout(titleString, nil, 0, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, TextNodeCutout(position: .TopLeft, size: CGSize(width: activityWidth, height: 4.0)), UIEdgeInsets()) + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - 20.0 - 22.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: TextNodeCutout(position: .TopLeft, size: CGSize(width: activityWidth, height: 4.0)), insets: UIEdgeInsets())) let contentSize: CGSize let insets: UIEdgeInsets - contentSize = CGSize(width: width, height: titleLayout.size.height + verticalInset + verticalInset) + contentSize = CGSize(width: params.width, height: titleLayout.size.height + verticalInset + verticalInset) insets = itemListNeighborsPlainInsets(neighbors) let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) @@ -121,15 +108,14 @@ class ItemListActivityTextItemNode: ListViewItemNode { let _ = titleApply() strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: titleLayout.size) + strongSelf.activityIndicator.frame = CGRect(origin: CGPoint(x: leftInset, y: 7.0), size: CGSize(width: 16.0, height: 16.0)) - if let activityIndicator = strongSelf.activityIndicator, activityIndicator.isHidden != !item.displayActivity { - if item.displayActivity { - activityIndicator.isHidden = false - activityIndicator.startAnimating() - } else { - activityIndicator.isHidden = true - activityIndicator.stopAnimating() - } + strongSelf.activityIndicator.type = .custom(item.theme.list.itemAccentColor, 16.0) + + if item.displayActivity { + strongSelf.activityIndicator.isHidden = false + } else { + strongSelf.activityIndicator.isHidden = true } } }) diff --git a/TelegramUI/ItemListAvatarAndNameItem.swift b/TelegramUI/ItemListAvatarAndNameItem.swift index 164dea4784..dc86ff760e 100644 --- a/TelegramUI/ItemListAvatarAndNameItem.swift +++ b/TelegramUI/ItemListAvatarAndNameItem.swift @@ -7,16 +7,27 @@ import SwiftSignalKit private let updatingAvatarOverlayImage = generateFilledCircleImage(diameter: 66.0, color: UIColor(white: 1.0, alpha: 0.5), backgroundColor: nil) +enum ItemListAvatarAndNameInfoItemTitleType { + case group + case channel +} + enum ItemListAvatarAndNameInfoItemName: Equatable { case personName(firstName: String, lastName: String) - case title(title: String) + case title(title: String, type: ItemListAvatarAndNameInfoItemTitleType) - init(_ name: PeerIndexNameRepresentation) { - switch name { + init(_ peer: Peer) { + switch peer.indexName { case let .personName(first, last, _, _): self = .personName(firstName: first, lastName: last) case let .title(title, _): - self = .title(title: title) + let type: ItemListAvatarAndNameInfoItemTitleType + if let peer = peer as? TelegramChannel, case .broadcast = peer.info { + type = .channel + } else { + type = .group + } + self = .title(title: title, type: type) } } @@ -30,7 +41,7 @@ enum ItemListAvatarAndNameInfoItemName: Equatable { } else { return lastName } - case let .title(title): + case let .title(title, _): return title } } @@ -39,7 +50,7 @@ enum ItemListAvatarAndNameInfoItemName: Equatable { switch self { case let .personName(firstName, _): return firstName.isEmpty - case let .title(title): + case let .title(title, _): return title.isEmpty } } @@ -52,8 +63,8 @@ enum ItemListAvatarAndNameInfoItemName: Equatable { } else { return false } - case let .title(title): - if case .title(title) = rhs { + case let .title(title, type): + if case .title(title, type) = rhs { return true } else { return false @@ -91,10 +102,38 @@ enum ItemListAvatarAndNameInfoItemStyle { case blocks(withTopInset: Bool) } +enum ItemListAvatarAndNameInfoItemUpdatingAvatar: Equatable { + case image(TelegramMediaImageRepresentation) + case none + + static func ==(lhs: ItemListAvatarAndNameInfoItemUpdatingAvatar, rhs: ItemListAvatarAndNameInfoItemUpdatingAvatar) -> Bool { + switch lhs { + case let .image(representation): + if case .image(representation) = rhs { + return true + } else { + return false + } + case .none: + if case .none = rhs { + return true + } else { + return false + } + } + } +} + +enum ItemListAvatarAndNameInfoItemMode { + case generic + case settings +} + class ItemListAvatarAndNameInfoItem: ListViewItem, ItemListItem { let account: Account let theme: PresentationTheme let strings: PresentationStrings + let mode: ItemListAvatarAndNameInfoItemMode let peer: Peer? let presence: PeerPresence? let cachedData: CachedPeerData? @@ -104,13 +143,18 @@ class ItemListAvatarAndNameInfoItem: ListViewItem, ItemListItem { let editingNameUpdated: (ItemListAvatarAndNameInfoItemName) -> Void let avatarTapped: () -> Void let context: ItemListAvatarAndNameInfoItemContext? - let updatingImage: TelegramMediaImageRepresentation? + let updatingImage: ItemListAvatarAndNameInfoItemUpdatingAvatar? let call: (() -> Void)? + let action: (() -> Void)? + let tag: ItemListItemTag? + + let selectable: Bool - init(account: Account, theme: PresentationTheme, strings: PresentationStrings, peer: Peer?, presence: PeerPresence?, cachedData: CachedPeerData?, state: ItemListAvatarAndNameInfoItemState, sectionId: ItemListSectionId, style: ItemListAvatarAndNameInfoItemStyle, editingNameUpdated: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, avatarTapped: @escaping () -> Void, context: ItemListAvatarAndNameInfoItemContext? = nil, updatingImage: TelegramMediaImageRepresentation? = nil, call: (() -> Void)? = nil) { + init(account: Account, theme: PresentationTheme, strings: PresentationStrings, mode: ItemListAvatarAndNameInfoItemMode, peer: Peer?, presence: PeerPresence?, cachedData: CachedPeerData?, state: ItemListAvatarAndNameInfoItemState, sectionId: ItemListSectionId, style: ItemListAvatarAndNameInfoItemStyle, editingNameUpdated: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, avatarTapped: @escaping () -> Void, context: ItemListAvatarAndNameInfoItemContext? = nil, updatingImage: ItemListAvatarAndNameInfoItemUpdatingAvatar? = nil, call: (() -> Void)? = nil, action: (() -> Void)? = nil, tag: ItemListItemTag? = nil) { self.account = account self.theme = theme self.strings = strings + self.mode = mode self.peer = peer self.presence = presence self.cachedData = cachedData @@ -122,12 +166,20 @@ class ItemListAvatarAndNameInfoItem: ListViewItem, ItemListItem { self.context = context self.updatingImage = updatingImage self.call = call + self.action = action + self.tag = tag + + if case .settings = mode { + self.selectable = true + } else { + self.selectable = false + } } - func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { async { let node = ItemListAvatarAndNameInfoItemNode() - let (layout, apply) = node.asyncLayout()(self, width, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) node.contentSize = layout.contentSize node.insets = layout.insets @@ -138,7 +190,7 @@ class ItemListAvatarAndNameInfoItem: ListViewItem, ItemListItem { } } - func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { if let node = node as? ItemListAvatarAndNameInfoItemNode { var animated = true if case .None = animation { @@ -148,7 +200,7 @@ class ItemListAvatarAndNameInfoItem: ListViewItem, ItemListItem { let makeLayout = node.asyncLayout() async { - let (layout, apply) = makeLayout(self, width, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) Queue.mainQueue().async { completion(layout, { apply(animated) @@ -158,14 +210,20 @@ class ItemListAvatarAndNameInfoItem: ListViewItem, ItemListItem { } } } + + func selected(listView: ListView){ + listView.clearHighlightAnimated(true) + self.action?() + } } private let avatarFont: UIFont = UIFont(name: "ArialRoundedMTBold", size: 28.0)! private let nameFont = Font.medium(19.0) private let statusFont = Font.regular(15.0) -class ItemListAvatarAndNameInfoItemNode: ListViewItemNode { +class ItemListAvatarAndNameInfoItemNode: ListViewItemNode, ItemListItemNode, ItemListItemFocusableNode { private let backgroundNode: ASDisplayNode + private let highlightedBackgroundNode: ASDisplayNode private let topStripeNode: ASDisplayNode private let bottomStripeNode: ASDisplayNode @@ -178,20 +236,28 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode { private var verificationIconNode: ASImageNode? private let statusNode: TextNode + private let arrowNode: ASImageNode + private var inputSeparator: ASDisplayNode? private var inputFirstField: UITextField? private var inputSecondField: UITextField? private var item: ItemListAvatarAndNameInfoItem? - private var layoutWidthAndNeighbors: (width: CGFloat, neighbors: ItemListNeighbors)? + private var layoutWidthAndNeighbors: (width: ListViewItemLayoutParams, neighbors: ItemListNeighbors)? private var peerPresenceManager: PeerPresenceStatusManager? private let hiddenAvatarRepresentationDisposable = MetaDisposable() + var tag: ItemListItemTag? { + return self.item?.tag + } + init() { self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true - self.backgroundNode.backgroundColor = .white + + self.highlightedBackgroundNode = ASDisplayNode() + self.highlightedBackgroundNode.isLayerBacked = true self.topStripeNode = ASDisplayNode() self.topStripeNode.isLayerBacked = true @@ -215,6 +281,11 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode { self.statusNode.contentMode = .left self.statusNode.contentsScale = UIScreen.main.scale + self.arrowNode = ASImageNode() + self.arrowNode.isLayerBacked = true + self.arrowNode.displaysAsynchronously = false + self.arrowNode.displayWithoutProcessing = true + self.callButton = HighlightableButtonNode() super.init(layerBacked: false, dynamicBounce: false) @@ -243,14 +314,16 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode { self.avatarNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.avatarTapGesture(_:)))) } - func asyncLayout() -> (_ item: ItemListAvatarAndNameInfoItem, _ width: CGFloat, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) { + func asyncLayout() -> (_ item: ItemListAvatarAndNameInfoItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) { let layoutNameNode = TextNode.asyncLayout(self.nameNode) let layoutStatusNode = TextNode.asyncLayout(self.statusNode) let currentOverlayImage = self.updatingAvatarOverlay.image let currentItem = self.item - return { item, width, neighbors in + return { item, params, neighbors in + let baseWidth = params.width - params.leftInset - params.rightInset + var updatedTheme: PresentationTheme? if currentItem?.theme !== item.theme { @@ -272,9 +345,9 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode { if let updatingName = item.state.updatingName { displayTitle = updatingName } else if let peer = item.peer { - displayTitle = ItemListAvatarAndNameInfoItemName(peer.indexName) + displayTitle = ItemListAvatarAndNameInfoItemName(peer) } else { - displayTitle = .title(title: "") + displayTitle = .title(title: "", type: .group) } var additionalTitleInset: CGFloat = 0.0 @@ -282,19 +355,33 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode { additionalTitleInset += 3.0 + verificationIconImage.size.width } - let (nameNodeLayout, nameNodeApply) = layoutNameNode(NSAttributedString(string: displayTitle.composedTitle, font: nameFont, textColor: item.theme.list.itemPrimaryTextColor), nil, 1, .end, CGSize(width: width - 20 - 94.0 - (item.call != nil ? 36.0 : 0.0) - additionalTitleInset, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let (nameNodeLayout, nameNodeApply) = layoutNameNode(TextNodeLayoutArguments(attributedString: NSAttributedString(string: displayTitle.composedTitle, font: nameFont, textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: baseWidth - 20 - 94.0 - (item.call != nil ? 36.0 : 0.0) - additionalTitleInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let statusText: String + var statusText: String = "" let statusColor: UIColor - if let _ = item.peer as? TelegramUser { - let presence = (item.presence as? TelegramUserPresence) ?? TelegramUserPresence(status: .none) - let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 - let (string, activity) = stringAndActivityForUserPresence(strings: item.strings, presence: presence, relativeTo: Int32(timestamp)) - statusText = string - if activity { - statusColor = item.theme.list.itemAccentColor - } else { - statusColor = item.theme.list.itemSecondaryTextColor + if let peer = item.peer as? TelegramUser { + switch item.mode { + case .settings: + if let phone = peer.phone, !phone.isEmpty { + statusText += formatPhoneNumber(phone) + } + if let username = peer.username, !username.isEmpty { + if !statusText.isEmpty { + statusText += "\n" + } + statusText += "@\(username)" + } + statusColor = item.theme.list.itemSecondaryTextColor + case .generic: + let presence = (item.presence as? TelegramUserPresence) ?? TelegramUserPresence(status: .none) + let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 + let (string, activity) = stringAndActivityForUserPresence(strings: item.strings, timeFormat: .regular, presence: presence, relativeTo: Int32(timestamp)) + statusText = string + if activity { + statusColor = item.theme.list.itemAccentColor + } else { + statusColor = item.theme.list.itemSecondaryTextColor + } } } else if let channel = item.peer as? TelegramChannel { if let cachedChannelData = item.cachedData as? CachedChannelData, let memberCount = cachedChannelData.participantsSummary.memberCount { @@ -318,18 +405,24 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode { statusColor = item.theme.list.itemPrimaryTextColor } - let (statusNodeLayout, statusNodeApply) = layoutStatusNode(NSAttributedString(string: statusText, font: statusFont, textColor: statusColor), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let (statusNodeLayout, statusNodeApply) = layoutStatusNode(TextNodeLayoutArguments(attributedString: NSAttributedString(string: statusText, font: statusFont, textColor: statusColor), backgroundColor: nil, maximumNumberOfLines: 3, truncationType: .end, constrainedSize: CGSize(width: baseWidth - 20, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let separatorHeight = UIScreenPixel let contentSize: CGSize let insets: UIEdgeInsets + let itemBackgroundColor: UIColor + let itemSeparatorColor: UIColor switch item.style { case .plain: - contentSize = CGSize(width: width, height: 96.0) + itemBackgroundColor = item.theme.list.plainBackgroundColor + itemSeparatorColor = item.theme.list.itemPlainSeparatorColor + contentSize = CGSize(width: params.width, height: 96.0) insets = itemListNeighborsPlainInsets(neighbors) case let .blocks(withTopInset): - contentSize = CGSize(width: width, height: 92.0) + itemBackgroundColor = item.theme.list.itemBlocksBackgroundColor + itemSeparatorColor = item.theme.list.itemBlocksSeparatorColor + contentSize = CGSize(width: params.width, height: 92.0) if withTopInset { insets = itemListNeighborsGroupedInsets(neighbors) } else { @@ -356,15 +449,20 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode { if let strongSelf = self { strongSelf.item = item - strongSelf.layoutWidthAndNeighbors = (width, neighbors) + strongSelf.layoutWidthAndNeighbors = (params, neighbors) + + var updatedArrowImage: UIImage? if let _ = updatedTheme { - strongSelf.topStripeNode.backgroundColor = item.theme.list.itemSeparatorColor - strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemSeparatorColor - strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBackgroundColor + strongSelf.topStripeNode.backgroundColor = itemSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor + strongSelf.backgroundNode.backgroundColor = itemBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor - strongSelf.inputSeparator?.backgroundColor = item.theme.list.itemSeparatorColor + strongSelf.inputSeparator?.backgroundColor = itemSeparatorColor strongSelf.callButton.setImage(PresentationResourcesChat.chatInfoCallButtonImage(item.theme), for: []) + + updatedArrowImage = PresentationResourcesItemList.disclosureArrowImage(item.theme) } if item.updatingImage != nil { @@ -391,7 +489,7 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode { if item.call != nil { strongSelf.addSubnode(strongSelf.callButton) - strongSelf.callButton.frame = CGRect(origin: CGPoint(x: width - 44.0 - 10.0, y: floor((contentSize.height - 44.0) / 2.0) - 2.0), size: CGSize(width: 44.0, height: 44.0)) + strongSelf.callButton.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 44.0 - 10.0, y: floor((contentSize.height - 44.0) / 2.0) - 2.0), size: CGSize(width: 44.0, height: 44.0)) } else if strongSelf.callButton.supernode != nil { strongSelf.callButton.removeFromSupernode() } @@ -432,12 +530,13 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode { let bottomStripeInset: CGFloat switch neighbors.bottom { case .sameSection: - bottomStripeInset = 16.0 + bottomStripeInset = params.leftInset + 16.0 case .none, .otherSection: bottomStripeInset = 0.0 } - strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: contentSize.height)) + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.width, height: contentSize.height)) + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: contentSize.height + UIScreenPixel)) strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layoutSize.width, height: separatorHeight)) strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: layoutSize.height - insets.top - separatorHeight), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) } @@ -446,14 +545,29 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode { let _ = statusNodeApply() if let peer = item.peer { - strongSelf.avatarNode.setPeer(account: item.account, peer: peer, temporaryRepresentation: item.updatingImage) + var overrideImage: AvatarNodeImageOverride? + if let updatingImage = item.updatingImage { + switch updatingImage { + case .none: + overrideImage = AvatarNodeImageOverride.none + case let .image(representation): + overrideImage = .image(representation) + } + } + strongSelf.avatarNode.setPeer(account: item.account, peer: peer, overrideImage: overrideImage) } - let avatarFrame = CGRect(origin: CGPoint(x: 15.0, y: avatarOriginY), size: CGSize(width: 66.0, height: 66.0)) + let avatarFrame = CGRect(origin: CGPoint(x: params.leftInset + 15.0, y: avatarOriginY), size: CGSize(width: 66.0, height: 66.0)) strongSelf.avatarNode.frame = avatarFrame strongSelf.updatingAvatarOverlay.frame = avatarFrame - let nameFrame = CGRect(origin: CGPoint(x: 94.0, y: 25.0), size: nameNodeLayout.size) + let nameY: CGFloat + if statusText.isEmpty { + nameY = floor((layout.contentSize.height - nameNodeLayout.size.height) / 2.0) + } else { + nameY = floor((layout.contentSize.height - nameNodeLayout.size.height - 3.0 - statusNodeLayout.size.height) / 2.0) + } + let nameFrame = CGRect(origin: CGPoint(x: params.leftInset + 94.0, y: nameY), size: nameNodeLayout.size) strongSelf.nameNode.frame = nameFrame if let verificationIconImage = verificationIconImage { @@ -473,7 +587,7 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode { verificationIconNode.removeFromSupernode() } - strongSelf.statusNode.frame = CGRect(origin: CGPoint(x: 94.0, y: 25.0 + nameNodeLayout.size.height + 4.0), size: statusNodeLayout.size) + strongSelf.statusNode.frame = CGRect(origin: CGPoint(x: params.leftInset + 94.0, y: nameFrame.maxY + 3.0), size: statusNodeLayout.size) if let editingName = item.state.editingName { var animateIn = false @@ -484,7 +598,7 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode { case let .personName(firstName, lastName): if strongSelf.inputSeparator == nil { let inputSeparator = ASDisplayNode() - inputSeparator.backgroundColor = item.theme.list.itemSeparatorColor + inputSeparator.backgroundColor = itemSeparatorColor inputSeparator.isLayerBacked = true strongSelf.addSubnode(inputSeparator) strongSelf.inputSeparator = inputSeparator @@ -520,19 +634,19 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode { strongSelf.inputSecondField?.text = lastName } - strongSelf.inputSeparator?.frame = CGRect(origin: CGPoint(x: 100.0, y: 46.0), size: CGSize(width: width - 100.0, height: separatorHeight)) - strongSelf.inputFirstField?.frame = CGRect(origin: CGPoint(x: 111.0, y: 12.0), size: CGSize(width: width - 111.0 - 8.0, height: 30.0)) - strongSelf.inputSecondField?.frame = CGRect(origin: CGPoint(x: 111.0, y: 52.0), size: CGSize(width: width - 111.0 - 8.0, height: 30.0)) + strongSelf.inputSeparator?.frame = CGRect(origin: CGPoint(x: params.leftInset + 100.0, y: 46.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 100.0, height: separatorHeight)) + strongSelf.inputFirstField?.frame = CGRect(origin: CGPoint(x: params.leftInset + 111.0, y: 12.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 111.0 - 8.0, height: 30.0)) + strongSelf.inputSecondField?.frame = CGRect(origin: CGPoint(x: params.leftInset + 111.0, y: 52.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 111.0 - 8.0, height: 30.0)) if animated && animateIn { strongSelf.inputSeparator?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) strongSelf.inputFirstField?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) strongSelf.inputSecondField?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) } - case let .title(title): + case let .title(title, type): if strongSelf.inputSeparator == nil { let inputSeparator = ASDisplayNode() - inputSeparator.backgroundColor = item.theme.list.itemSeparatorColor + inputSeparator.backgroundColor = itemSeparatorColor inputSeparator.isLayerBacked = true strongSelf.addSubnode(inputSeparator) strongSelf.inputSeparator = inputSeparator @@ -544,7 +658,14 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode { inputFirstField.textColor = item.theme.list.itemPrimaryTextColor inputFirstField.keyboardAppearance = item.theme.chatList.searchBarKeyboardColor.keyboardAppearance inputFirstField.autocorrectionType = .no - inputFirstField.attributedPlaceholder = NSAttributedString(string: item.strings.GroupInfo_GroupNamePlaceholder, font: Font.regular(19.0), textColor: item.theme.list.itemPlaceholderTextColor) + let placeholder: String + switch type { + case .group: + placeholder = item.strings.GroupInfo_GroupNamePlaceholder + case .channel: + placeholder = item.strings.GroupInfo_ChannelListNamePlaceholder + } + inputFirstField.attributedPlaceholder = NSAttributedString(string: placeholder, font: Font.regular(19.0), textColor: item.theme.list.itemPlaceholderTextColor) inputFirstField.attributedText = NSAttributedString(string: title, font: Font.regular(19.0), textColor: item.theme.list.itemPrimaryTextColor) strongSelf.inputFirstField = inputFirstField strongSelf.view.addSubview(inputFirstField) @@ -553,8 +674,8 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode { strongSelf.inputFirstField?.text = title } - strongSelf.inputSeparator?.frame = CGRect(origin: CGPoint(x: 100.0, y: 62.0), size: CGSize(width: width - 100.0, height: separatorHeight)) - strongSelf.inputFirstField?.frame = CGRect(origin: CGPoint(x: 102.0, y: 26.0), size: CGSize(width: width - 102.0 - 8.0, height: 35.0)) + strongSelf.inputSeparator?.frame = CGRect(origin: CGPoint(x: params.leftInset + 100.0, y: 62.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 100.0, height: separatorHeight)) + strongSelf.inputFirstField?.frame = CGRect(origin: CGPoint(x: params.leftInset + 102.0, y: 26.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 102.0 - 8.0, height: 35.0)) if animated && animateIn { strongSelf.inputSeparator?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) @@ -639,18 +760,71 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode { } strongSelf.updateAvatarHidden() + + if let updatedArrowImage = updatedArrowImage { + strongSelf.arrowNode.image = updatedArrowImage + } + + if case .settings = item.mode, let arrowImage = strongSelf.arrowNode.image { + if strongSelf.arrowNode.supernode == nil { + strongSelf.addSubnode(strongSelf.arrowNode) + } + strongSelf.arrowNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 15.0 - arrowImage.size.width, y: floor((layout.contentSize.height - arrowImage.size.height) / 2.0)), size: arrowImage.size) + } else if strongSelf.arrowNode.supernode != nil { + strongSelf.arrowNode.removeFromSupernode() + } } }) } } + override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { + super.setHighlighted(highlighted, at: point, animated: animated) + + if highlighted { + self.highlightedBackgroundNode.alpha = 1.0 + if self.highlightedBackgroundNode.supernode == nil { + var anchorNode: ASDisplayNode? + if self.bottomStripeNode.supernode != nil { + anchorNode = self.bottomStripeNode + } else if self.topStripeNode.supernode != nil { + anchorNode = self.topStripeNode + } else if self.backgroundNode.supernode != nil { + anchorNode = self.backgroundNode + } + if let anchorNode = anchorNode { + self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode) + } else { + self.addSubnode(self.highlightedBackgroundNode) + } + } + } else { + if self.highlightedBackgroundNode.supernode != nil { + if animated { + self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in + if let strongSelf = self { + if completed { + strongSelf.highlightedBackgroundNode.removeFromSupernode() + } + } + }) + self.highlightedBackgroundNode.alpha = 0.0 + } else { + self.highlightedBackgroundNode.removeFromSupernode() + } + } + } + } + @objc func textFieldDidChange(_ inputField: UITextField) { - if let item = self.item { + if let item = self.item, let currentEditingName = item.state.editingName { var editingName: ItemListAvatarAndNameInfoItemName? if let inputFirstField = self.inputFirstField, let inputSecondField = self.inputSecondField { editingName = .personName(firstName: inputFirstField.text ?? "", lastName: inputSecondField.text ?? "") } else if let inputFirstField = self.inputFirstField { - editingName = .title(title: inputFirstField.text ?? "") + if case let .title(_, type) = currentEditingName { + editingName = .title(title: inputFirstField.text ?? "", type: type) + } } if let editingName = editingName { item.editingNameUpdated(editingName) @@ -683,4 +857,8 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode { @objc func callButtonPressed() { self.item?.call?() } + + func focus() { + self.inputFirstField?.becomeFirstResponder() + } } diff --git a/TelegramUI/ItemListCheckboxItem.swift b/TelegramUI/ItemListCheckboxItem.swift index 96290ab96a..218171eb5c 100644 --- a/TelegramUI/ItemListCheckboxItem.swift +++ b/TelegramUI/ItemListCheckboxItem.swift @@ -11,12 +11,8 @@ class ItemListCheckboxItem: ListViewItem, ItemListItem { let sectionId: ItemListSectionId let action: () -> Void - init(theme: PresentationTheme? = nil, title: String, checked: Bool, zeroSeparatorInsets: Bool, sectionId: ItemListSectionId, action: @escaping () -> Void) { - if let theme = theme { - self.theme = theme - } else { - self.theme = defaultPresentationTheme - } + init(theme: PresentationTheme, title: String, checked: Bool, zeroSeparatorInsets: Bool, sectionId: ItemListSectionId, action: @escaping () -> Void) { + self.theme = theme self.title = title self.checked = checked self.zeroSeparatorInsets = zeroSeparatorInsets @@ -24,10 +20,10 @@ class ItemListCheckboxItem: ListViewItem, ItemListItem { self.action = action } - func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { async { let node = ItemListCheckboxItemNode() - let (layout, apply) = node.asyncLayout()(self, width, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) node.contentSize = layout.contentSize node.insets = layout.insets @@ -38,13 +34,13 @@ class ItemListCheckboxItem: ListViewItem, ItemListItem { } } - func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { if let node = node as? ItemListCheckboxItemNode { Queue.mainQueue().async { let makeLayout = node.asyncLayout() async { - let (layout, apply) = makeLayout(self, width, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) Queue.mainQueue().async { completion(layout, { apply() @@ -105,20 +101,20 @@ class ItemListCheckboxItemNode: ListViewItemNode { self.addSubnode(self.titleNode) } - func asyncLayout() -> (_ item: ItemListCheckboxItem, _ width: CGFloat, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + func asyncLayout() -> (_ item: ItemListCheckboxItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let currentItem = self.item - return { item, width, neighbors in - let leftInset: CGFloat = 44.0 + return { item, params, neighbors in + let leftInset: CGFloat = 44.0 + params.leftInset - let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: item.title, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let separatorHeight = UIScreenPixel let insets = itemListNeighborsGroupedInsets(neighbors) - let contentSize = CGSize(width: width, height: 44.0) + let contentSize = CGSize(width: params.width, height: 44.0) let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) let layoutSize = layout.size @@ -140,16 +136,16 @@ class ItemListCheckboxItemNode: ListViewItemNode { } if let _ = updatedTheme { - strongSelf.topStripeNode.backgroundColor = item.theme.list.itemSeparatorColor - strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemSeparatorColor - strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBackgroundColor + strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor } let _ = titleApply() if let image = strongSelf.iconNode.image { - strongSelf.iconNode.frame = CGRect(origin: CGPoint(x: floor((leftInset - image.size.width) / 2.0), y: floor((contentSize.height - image.size.height) / 2.0)), size: image.size) + strongSelf.iconNode.frame = CGRect(origin: CGPoint(x: params.leftInset + floor((leftInset - params.leftInset - image.size.width) / 2.0), y: floor((contentSize.height - image.size.height) / 2.0)), size: image.size) } strongSelf.iconNode.isHidden = !item.checked @@ -179,20 +175,20 @@ class ItemListCheckboxItemNode: ListViewItemNode { bottomStripeInset = 0.0 } } - strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) - strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) - strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight)) + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight)) strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 11.0), size: titleLayout.size) - strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: width, height: 44.0 + UIScreenPixel + UIScreenPixel)) + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: 44.0 + UIScreenPixel + UIScreenPixel)) } }) } } - override func setHighlighted(_ highlighted: Bool, animated: Bool) { - super.setHighlighted(highlighted, animated: animated) + override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { + super.setHighlighted(highlighted, at: point, animated: animated) if highlighted { self.highlightedBackgroundNode.alpha = 1.0 diff --git a/TelegramUI/ItemListControllerNode.swift b/TelegramUI/ItemListControllerNode.swift index 39105d7c96..d04a6ce8c3 100644 --- a/TelegramUI/ItemListControllerNode.swift +++ b/TelegramUI/ItemListControllerNode.swift @@ -94,7 +94,6 @@ class ItemListControllerNode: ASDisplayNode, UIScrollV let listNode: ListView private var emptyStateItem: ItemListControllerEmptyStateItem? private var emptyStateNode: ItemListControllerEmptyStateItemNode? - private let scrollNode: ASScrollNode private let transitionDisposable = MetaDisposable() @@ -111,7 +110,6 @@ class ItemListControllerNode: ASDisplayNode, UIScrollV var enableInteractiveDismiss = false { didSet { - self.scrollNode.view.isScrollEnabled = self.enableInteractiveDismiss } } @@ -119,7 +117,6 @@ class ItemListControllerNode: ASDisplayNode, UIScrollV self.updateNavigationOffset = updateNavigationOffset self.listNode = ListView() - self.scrollNode = ASScrollNode() super.init() @@ -130,23 +127,7 @@ class ItemListControllerNode: ASDisplayNode, UIScrollV self.backgroundColor = nil self.isOpaque = false - self.scrollNode.view.showsVerticalScrollIndicator = false - self.scrollNode.view.showsHorizontalScrollIndicator = false - self.scrollNode.view.alwaysBounceHorizontal = false - self.scrollNode.view.alwaysBounceVertical = false - self.scrollNode.view.clipsToBounds = false - self.scrollNode.view.delegate = self - self.scrollNode.view.scrollsToTop = false - self.scrollNode.view.isScrollEnabled = false - self.addSubnode(self.scrollNode) - - self.scrollNode.backgroundColor = nil - self.scrollNode.isOpaque = false - - self.scrollNode.addSubnode(self.listNode) - self.addSubnode(self.scrollNode) - - self.listNode.backgroundColor = UIColor(rgb: 0xefeff4) + self.addSubnode(self.listNode) self.listNode.displayedItemRangeChanged = { [weak self] displayedRange, opaqueTransactionState in if let strongSelf = self, let visibleEntriesUpdated = strongSelf.visibleEntriesUpdated, let mergedEntries = (opaqueTransactionState as? ItemListNodeOpaqueState)?.mergedEntries { @@ -191,10 +172,6 @@ class ItemListControllerNode: ASDisplayNode, UIScrollV override func didLoad() { super.didLoad() - - if #available(iOSApplicationExtension 11.0, *) { - self.scrollNode.view.contentInsetAdjustmentBehavior = .never - } } func animateIn() { @@ -211,18 +188,6 @@ class ItemListControllerNode: ASDisplayNode, UIScrollV } func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { - let previousContentHeight = self.scrollNode.view.contentSize.height - let previousVerticalOffset = self.scrollNode.view.contentOffset.y - - self.scrollNode.frame = CGRect(origin: CGPoint(), size: layout.size) - self.scrollNode.view.contentSize = CGSize(width: 0.0, height: layout.size.height * 3.0) - - if previousContentHeight.isEqual(to: 0.0) { - self.scrollNode.view.contentOffset = CGPoint(x: 0.0, y: self.scrollNode.view.contentSize.height / 3.0) - } else { - self.scrollNode.view.contentOffset = CGPoint(x: 0.0, y: previousVerticalOffset * self.scrollNode.view.contentSize.height / previousContentHeight) - } - var duration: Double = 0.0 var curve: UInt = 0 switch transition { @@ -247,9 +212,11 @@ class ItemListControllerNode: ASDisplayNode, UIScrollV var insets = layout.insets(options: [.input]) insets.top += navigationBarHeight + insets.left += layout.safeInsets.left + insets.right += layout.safeInsets.right self.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) - self.listNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height + layout.size.height / 2.0) + self.listNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0) self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: listViewCurve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) @@ -311,6 +278,9 @@ class ItemListControllerNode: ASDisplayNode, UIScrollV options.insert(.PreferSynchronousResourceLoading) options.insert(.PreferSynchronousDrawing) options.insert(.AnimateAlpha) + } else { + options.insert(.Synchronous) + options.insert(.PreferSynchronousDrawing) } let focusItemTag = transition.focusItemTag self.listNode.transaction(deleteIndices: transition.entries.deletions, insertIndicesAndItems: transition.entries.insertions, updateIndicesAndItems: transition.entries.updates, options: options, updateOpaqueState: ItemListNodeOpaqueState(mergedEntries: transition.mergedEntries), completion: { [weak self] _ in @@ -382,8 +352,6 @@ class ItemListControllerNode: ASDisplayNode, UIScrollV if abs(scrollVelocity.y) > 200.0 { self.animateOut() - } else { - self.scrollNode.view.setContentOffset(CGPoint(x: 0.0, y: self.scrollNode.view.contentSize.height / 3.0), animated: true) } } } diff --git a/TelegramUI/ItemListDisclosureItem.swift b/TelegramUI/ItemListDisclosureItem.swift index 95a1bc6ab0..4807476266 100644 --- a/TelegramUI/ItemListDisclosureItem.swift +++ b/TelegramUI/ItemListDisclosureItem.swift @@ -10,6 +10,7 @@ enum ItemListDisclosureStyle { class ItemListDisclosureItem: ListViewItem, ItemListItem { let theme: PresentationTheme + let icon: UIImage? let title: String let label: String let sectionId: ItemListSectionId @@ -17,12 +18,9 @@ class ItemListDisclosureItem: ListViewItem, ItemListItem { let disclosureStyle: ItemListDisclosureStyle let action: (() -> Void)? - init(theme: PresentationTheme? = nil, title: String, label: String, sectionId: ItemListSectionId, style: ItemListStyle, disclosureStyle: ItemListDisclosureStyle = .arrow, action: (() -> Void)?) { - if let theme = theme { - self.theme = theme - } else { - self.theme = defaultPresentationTheme - } + init(theme: PresentationTheme, icon: UIImage? = nil, title: String, label: String, sectionId: ItemListSectionId, style: ItemListStyle, disclosureStyle: ItemListDisclosureStyle = .arrow, action: (() -> Void)?) { + self.theme = theme + self.icon = icon self.title = title self.label = label self.sectionId = sectionId @@ -31,10 +29,10 @@ class ItemListDisclosureItem: ListViewItem, ItemListItem { self.action = action } - func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { async { let node = ItemListDisclosureItemNode() - let (layout, apply) = node.asyncLayout()(self, width, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) node.contentSize = layout.contentSize node.insets = layout.insets @@ -45,13 +43,13 @@ class ItemListDisclosureItem: ListViewItem, ItemListItem { } } - func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { if let node = node as? ItemListDisclosureItemNode { Queue.mainQueue().async { let makeLayout = node.asyncLayout() async { - let (layout, apply) = makeLayout(self, width, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) Queue.mainQueue().async { completion(layout, { apply() @@ -78,6 +76,7 @@ class ItemListDisclosureItemNode: ListViewItemNode { private let bottomStripeNode: ASDisplayNode private let highlightedBackgroundNode: ASDisplayNode + let iconNode: ASImageNode let titleNode: TextNode let labelNode: TextNode let arrowNode: ASImageNode @@ -103,6 +102,10 @@ class ItemListDisclosureItemNode: ListViewItemNode { self.bottomStripeNode = ASDisplayNode() self.bottomStripeNode.isLayerBacked = true + self.iconNode = ASImageNode() + self.iconNode.isLayerBacked = true + self.iconNode.displaysAsynchronously = false + self.titleNode = TextNode() self.titleNode.isLayerBacked = true @@ -124,20 +127,19 @@ class ItemListDisclosureItemNode: ListViewItemNode { self.addSubnode(self.arrowNode) } - func asyncLayout() -> (_ item: ItemListDisclosureItem, _ width: CGFloat, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + func asyncLayout() -> (_ item: ItemListDisclosureItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeLabelLayout = TextNode.asyncLayout(self.labelNode) let currentItem = self.item - return { item, width, neighbors in - var rightInset: CGFloat = 34.0 - + return { item, params, neighbors in + let rightInset: CGFloat switch item.disclosureStyle { case .none: - rightInset = 16.0 + rightInset = 16.0 + params.rightInset case .arrow: - rightInset = 34.0 + rightInset = 34.0 + params.rightInset } var updateArrowImage: UIImage? @@ -148,21 +150,40 @@ class ItemListDisclosureItemNode: ListViewItemNode { updateArrowImage = PresentationResourcesItemList.disclosureArrowImage(item.theme) } + var updateIcon = false + if currentItem?.icon != item.icon { + updateIcon = true + } + let contentSize: CGSize let insets: UIEdgeInsets let separatorHeight = UIScreenPixel + let itemBackgroundColor: UIColor + let itemSeparatorColor: UIColor + + var leftInset: CGFloat = params.leftInset switch item.style { case .plain: - contentSize = CGSize(width: width, height: 44.0) + leftInset += 35.0 + itemBackgroundColor = item.theme.list.plainBackgroundColor + itemSeparatorColor = item.theme.list.itemPlainSeparatorColor + contentSize = CGSize(width: params.width, height: 44.0) insets = itemListNeighborsPlainInsets(neighbors) case .blocks: - contentSize = CGSize(width: width, height: 44.0) + leftInset += 16.0 + itemBackgroundColor = item.theme.list.itemBlocksBackgroundColor + itemSeparatorColor = item.theme.list.itemBlocksSeparatorColor + contentSize = CGSize(width: params.width, height: 44.0) insets = itemListNeighborsGroupedInsets(neighbors) } - let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: item.title, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) - let (labelLayout, labelApply) = makeLabelLayout(NSAttributedString(string: item.label, font: titleFont, textColor: item.theme.list.itemSecondaryTextColor), nil, 1, .end, CGSize(width: width - 40 - titleLayout.size.width - 10.0, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + if let _ = item.icon { + leftInset += 43.0 + } + + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - 20.0 - leftInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.label, font: titleFont, textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - leftInset - 40.0 - titleLayout.size.width - 10.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) let layoutSize = layout.size @@ -171,26 +192,35 @@ class ItemListDisclosureItemNode: ListViewItemNode { if let strongSelf = self { strongSelf.item = item + if let icon = item.icon { + if strongSelf.iconNode.supernode == nil { + strongSelf.addSubnode(strongSelf.iconNode) + } + if updateIcon { + strongSelf.iconNode.image = icon + } + strongSelf.iconNode.frame = CGRect(origin: CGPoint(x: params.leftInset + floor((leftInset - params.leftInset - icon.size.width) / 2.0), y: floor((layout.contentSize.height - icon.size.height) / 2.0)), size: icon.size) + } else if strongSelf.iconNode.supernode != nil { + strongSelf.iconNode.image = nil + strongSelf.iconNode.removeFromSupernode() + } + if let updateArrowImage = updateArrowImage { strongSelf.arrowNode.image = updateArrowImage } if let _ = updatedTheme { - strongSelf.topStripeNode.backgroundColor = item.theme.list.itemSeparatorColor - strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemSeparatorColor - strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBackgroundColor + strongSelf.topStripeNode.backgroundColor = itemSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor + strongSelf.backgroundNode.backgroundColor = itemBackgroundColor strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor } let _ = titleApply() let _ = labelApply() - let leftInset: CGFloat - switch item.style { case .plain: - leftInset = 35.0 - if strongSelf.backgroundNode.supernode != nil { strongSelf.backgroundNode.removeFromSupernode() } @@ -201,10 +231,8 @@ class ItemListDisclosureItemNode: ListViewItemNode { strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0) } - strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: width - leftInset, height: separatorHeight)) + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight)) case .blocks: - leftInset = 16.0 - if strongSelf.backgroundNode.supernode == nil { strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) } @@ -223,21 +251,21 @@ class ItemListDisclosureItemNode: ListViewItemNode { let bottomStripeInset: CGFloat switch neighbors.bottom { case .sameSection(false): - bottomStripeInset = 16.0 + bottomStripeInset = leftInset default: bottomStripeInset = 0.0 } - strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) - strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) - strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight)) + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight)) } strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 11.0), size: titleLayout.size) - strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: width - rightInset - labelLayout.size.width, y: 11.0), size: labelLayout.size) + strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: params.width - rightInset - labelLayout.size.width, y: 11.0), size: labelLayout.size) if let arrowImage = strongSelf.arrowNode.image { - strongSelf.arrowNode.frame = CGRect(origin: CGPoint(x: width - 15.0 - arrowImage.size.width, y: 15.0), size: arrowImage.size) + strongSelf.arrowNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 15.0 - arrowImage.size.width, y: 15.0), size: arrowImage.size) } switch item.disclosureStyle { @@ -247,14 +275,14 @@ class ItemListDisclosureItemNode: ListViewItemNode { strongSelf.arrowNode.isHidden = false } - strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: width, height: 44.0 + UIScreenPixel + UIScreenPixel)) + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: 44.0 + UIScreenPixel)) } }) } } - override func setHighlighted(_ highlighted: Bool, animated: Bool) { - super.setHighlighted(highlighted, animated: animated) + override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { + super.setHighlighted(highlighted, at: point, animated: animated) if highlighted { self.highlightedBackgroundNode.alpha = 1.0 diff --git a/TelegramUI/ItemListEditableDeleteControlNode.swift b/TelegramUI/ItemListEditableDeleteControlNode.swift index 6c068a4fa3..67f947bf95 100644 --- a/TelegramUI/ItemListEditableDeleteControlNode.swift +++ b/TelegramUI/ItemListEditableDeleteControlNode.swift @@ -2,16 +2,6 @@ import Foundation import AsyncDisplayKit import Display -private let deleteIndicator = generateImage(CGSize(width: 22.0, height: 26.0), contextGenerator: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(UIColor(white: 0.0, alpha: 0.06).cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 22.0, height: 22.0))) - context.setFillColor(UIColor(rgb: 0xfc2125).cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: 2.0), size: CGSize(width: 22.0, height: 22.0))) - context.setFillColor(UIColor.white.cgColor) - context.fill(CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - 11.0) / 2.0), y: 2.0 + floorToScreenPixels((size.width - 1.0) / 2.0)), size: CGSize(width: 11.0, height: 1.0))) -}) - final class ItemListEditableControlNode: ASDisplayNode { var tapped: (() -> Void)? private let iconNode: ASImageNode @@ -31,9 +21,9 @@ final class ItemListEditableControlNode: ASDisplayNode { self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) } - static func asyncLayout(_ node: ItemListEditableControlNode?) -> (_ height: CGFloat) -> (CGSize, () -> ItemListEditableControlNode) { - return { height in - let image = deleteIndicator + static func asyncLayout(_ node: ItemListEditableControlNode?) -> (_ height: CGFloat, _ theme: PresentationTheme, _ hidden: Bool) -> (CGSize, () -> ItemListEditableControlNode) { + return { height, theme, hidden in + let image = PresentationResourcesItemList.itemListDeleteIndicatorIcon(theme) let resultNode: ItemListEditableControlNode if let node = node { @@ -46,6 +36,7 @@ final class ItemListEditableControlNode: ASDisplayNode { return (CGSize(width: 38.0, height: height), { if let image = image { resultNode.iconNode.frame = CGRect(origin: CGPoint(x: 12.0, y: floor((height - image.size.height) / 2.0)), size: image.size) + resultNode.iconNode.isHidden = hidden } return resultNode }) diff --git a/TelegramUI/ItemListEditableItem.swift b/TelegramUI/ItemListEditableItem.swift index b724e780ff..afdded4eb0 100644 --- a/TelegramUI/ItemListEditableItem.swift +++ b/TelegramUI/ItemListEditableItem.swift @@ -54,6 +54,8 @@ final class ItemListRevealOptionsGestureRecognizer: UIPanGestureRecognizer { } class ItemListRevealOptionsItemNode: ListViewItemNode { + private var validLayout: (CGSize, CGFloat, CGFloat)? + private var revealNode: ItemListRevealOptionsNode? private var revealOptions: [ItemListRevealOption] = [] @@ -104,12 +106,15 @@ class ItemListRevealOptionsItemNode: ListViewItemNode { } @objc func revealGesture(_ recognizer: ItemListRevealOptionsGestureRecognizer) { + guard let (size, leftInset, rightInset) = self.validLayout else { + return + } switch recognizer.state { case .began: if let revealNode = self.revealNode { let revealSize = revealNode.calculatedSize let location = recognizer.location(in: self.view) - if location.x > self.bounds.size.width - revealSize.width { + if location.x > size.width - revealSize.width { recognizer.becomeCancelled() } else { self.initialRevealOffset = self.revealOffset @@ -170,29 +175,38 @@ class ItemListRevealOptionsItemNode: ListViewItemNode { revealNode.setOptions(self.revealOptions) self.revealNode = revealNode - let revealSize = revealNode.measure(CGSize(width: CGFloat.greatestFiniteMagnitude, height: self.bounds.size.height)) - revealNode.frame = CGRect(origin: CGPoint(x: self.bounds.size.width + max(self.revealOffset, -revealSize.width), y: 0.0), size: revealSize) + if let (size, _, rightInset) = self.validLayout { + let revealSize = revealNode.measure(CGSize(width: CGFloat.greatestFiniteMagnitude, height: size.height)) + + revealNode.frame = CGRect(origin: CGPoint(x: size.width + max(self.revealOffset, -revealSize.width - rightInset), y: 0.0), size: revealSize) + revealNode.updateRevealOffset(offset: 0.0, rightInset: rightInset, transition: .immediate) + } self.addSubnode(revealNode) } } - override func layout() { + func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) { + self.validLayout = (size, leftInset, rightInset) + if let revealNode = self.revealNode { - let height = self.contentSize.height - let revealSize = revealNode.measure(CGSize(width: CGFloat.greatestFiniteMagnitude, height: height)) - revealNode.frame = CGRect(origin: CGPoint(x: self.bounds.size.width + max(self.revealOffset, -revealSize.width), y: 0.0), size: revealSize) + let revealSize = revealNode.measure(CGSize(width: CGFloat.greatestFiniteMagnitude, height: size.height)) + revealNode.frame = CGRect(origin: CGPoint(x: size.width - rightInset + max(self.revealOffset, -revealSize.width - rightInset), y: 0.0), size: revealSize) } } func updateRevealOffsetInternal(offset: CGFloat, transition: ContainedViewLayoutTransition) { self.revealOffset = offset + guard let (size, _, rightInset) = self.validLayout else { + return + } + if let revealNode = self.revealNode { let revealSize = revealNode.calculatedSize - let revealFrame = CGRect(origin: CGPoint(x: self.bounds.size.width + max(self.revealOffset, -revealSize.width), y: 0.0), size: revealSize) - let revealNodeOffset = -max(self.revealOffset, -revealSize.width) - revealNode.updateRevealOffset(offset: revealNodeOffset, transition: transition) + let revealFrame = CGRect(origin: CGPoint(x: size.width + max(self.revealOffset, -revealSize.width), y: 0.0), size: revealSize) + let revealNodeOffset = -max(self.revealOffset, -revealSize.width - rightInset) + revealNode.updateRevealOffset(offset: revealNodeOffset, rightInset: rightInset, transition: transition) if CGFloat(0.0).isLessThanOrEqualTo(offset) { self.revealNode = nil @@ -239,10 +253,10 @@ class ItemListRevealOptionsItemNode: ListViewItemNode { if value { if self.revealNode == nil { self.setupAndAddRevealNode() - if let revealNode = self.revealNode { + if let revealNode = self.revealNode, revealNode.isNodeLoaded, let (_, _, rightInset) = self.validLayout { revealNode.layout() let revealSize = revealNode.calculatedSize - self.updateRevealOffsetInternal(offset: -revealSize.width, transition: transition) + self.updateRevealOffsetInternal(offset: -revealSize.width - rightInset, transition: transition) } } } else if !self.revealOffset.isZero { @@ -253,4 +267,14 @@ class ItemListRevealOptionsItemNode: ListViewItemNode { func revealOptionSelected(_ option: ItemListRevealOption) { } + + override var preventsTouchesToOtherItems: Bool { + return self.isDisplayingRevealedOptions + } + + override func touchesToOtherItemsPrevented() { + if self.isDisplayingRevealedOptions { + self.setRevealOptionsOpened(false, animated: true) + } + } } diff --git a/TelegramUI/ItemListLoadingIndicatorEmptyStateItem.swift b/TelegramUI/ItemListLoadingIndicatorEmptyStateItem.swift index 471283f4e2..f4e5963220 100644 --- a/TelegramUI/ItemListLoadingIndicatorEmptyStateItem.swift +++ b/TelegramUI/ItemListLoadingIndicatorEmptyStateItem.swift @@ -3,45 +3,52 @@ import AsyncDisplayKit import Display final class ItemListLoadingIndicatorEmptyStateItem: ItemListControllerEmptyStateItem { + let theme: PresentationTheme + + init(theme: PresentationTheme) { + self.theme = theme + } + func isEqual(to: ItemListControllerEmptyStateItem) -> Bool { return to is ItemListLoadingIndicatorEmptyStateItem } func node(current: ItemListControllerEmptyStateItemNode?) -> ItemListControllerEmptyStateItemNode { if let current = current as? ItemListLoadingIndicatorEmptyStateItemNode { + current.theme = self.theme return current } else { - return ItemListLoadingIndicatorEmptyStateItemNode() + return ItemListLoadingIndicatorEmptyStateItemNode(theme: self.theme) } } } final class ItemListLoadingIndicatorEmptyStateItemNode: ItemListControllerEmptyStateItemNode { - private var indicator: UIActivityIndicatorView? + var theme: PresentationTheme { + didSet { + self.indicator.type = .custom(self.theme.list.itemAccentColor, 40.0) + } + } + private let indicator: ActivityIndicator private var validLayout: (ContainerViewLayout, CGFloat)? - override func didLoad() { - super.didLoad() + init(theme: PresentationTheme) { + self.theme = theme + self.indicator = ActivityIndicator(type: .custom(theme.list.itemAccentColor, 40.0)) - let indicator = UIActivityIndicatorView(activityIndicatorStyle: .gray) - self.indicator = indicator - self.view.addSubview(indicator) - if let layout = self.validLayout { - self.updateLayout(layout: layout.0, navigationBarHeight: layout.1, transition: .immediate) - } - indicator.startAnimating() + super.init() + + self.addSubnode(self.indicator) } override func updateLayout(layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { self.validLayout = (layout, navigationBarHeight) - if let indicator = self.indicator { - self.validLayout = (layout, navigationBarHeight) - var insets = layout.insets(options: [.statusBar]) - insets.top += navigationBarHeight - - let size = indicator.bounds.size - indicator.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - size.width) / 2.0), y: insets.top + floor((layout.size.height - insets.top - insets.bottom - size.height) / 2.0)), size: size) - } + + var insets = layout.insets(options: [.statusBar]) + insets.top += navigationBarHeight + + let size = CGSize(width: 40.0, height: 40.0) + transition.updateFrame(node: self.indicator, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - size.width) / 2.0), y: insets.top + floor((layout.size.height - insets.top - insets.bottom - size.height) / 2.0)), size: size)) } } diff --git a/TelegramUI/ItemListMultilineInputItem.swift b/TelegramUI/ItemListMultilineInputItem.swift index 7a8737c29c..72f871f758 100644 --- a/TelegramUI/ItemListMultilineInputItem.swift +++ b/TelegramUI/ItemListMultilineInputItem.swift @@ -11,21 +11,23 @@ class ItemListMultilineInputItem: ListViewItem, ItemListItem { let style: ItemListStyle let action: () -> Void let textUpdated: (String) -> Void + let maxLength: Int? - init(theme: PresentationTheme, text: String, placeholder: String, sectionId: ItemListSectionId, style: ItemListStyle, textUpdated: @escaping (String) -> Void, action: @escaping () -> Void) { + init(theme: PresentationTheme, text: String, placeholder: String, maxLength: Int?, sectionId: ItemListSectionId, style: ItemListStyle, textUpdated: @escaping (String) -> Void, action: @escaping () -> Void) { self.theme = theme self.text = text self.placeholder = placeholder + self.maxLength = maxLength self.sectionId = sectionId self.style = style self.textUpdated = textUpdated self.action = action } - func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { async { let node = ItemListMultilineInputItemNode() - let (layout, apply) = node.asyncLayout()(self, width, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) node.contentSize = layout.contentSize node.insets = layout.insets @@ -36,13 +38,13 @@ class ItemListMultilineInputItem: ListViewItem, ItemListItem { } } - func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { if let node = node as? ItemListMultilineInputItemNode { Queue.mainQueue().async { let makeLayout = node.asyncLayout() async { - let (layout, apply) = makeLayout(self, width, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) Queue.mainQueue().async { completion(layout, { apply() @@ -65,19 +67,19 @@ class ItemListMultilineInputItemNode: ListViewItemNode, ASEditableTextNodeDelega private let textNode: ASEditableTextNode private let measureTextNode: TextNode + private let limitTextNode: TextNode + private var item: ItemListMultilineInputItem? + private var layoutParams: ListViewItemLayoutParams? init() { self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true - self.backgroundNode.backgroundColor = .white self.topStripeNode = ASDisplayNode() - self.topStripeNode.backgroundColor = UIColor(rgb: 0xc8c7cc) self.topStripeNode.isLayerBacked = true self.bottomStripeNode = ASDisplayNode() - self.bottomStripeNode.backgroundColor = UIColor(rgb: 0xc8c7cc) self.bottomStripeNode.isLayerBacked = true self.textClippingNode = ASDisplayNode() @@ -86,6 +88,8 @@ class ItemListMultilineInputItemNode: ListViewItemNode, ASEditableTextNodeDelega self.textNode = ASEditableTextNode() self.measureTextNode = TextNode() + self.limitTextNode = TextNode() + super.init(layerBacked: false, dynamicBounce: false) self.textClippingNode.addSubnode(self.textNode) @@ -105,23 +109,43 @@ class ItemListMultilineInputItemNode: ListViewItemNode, ASEditableTextNodeDelega self.textNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0) } - func asyncLayout() -> (_ item: ItemListMultilineInputItem, _ width: CGFloat, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + func asyncLayout() -> (_ item: ItemListMultilineInputItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { let makeTextLayout = TextNode.asyncLayout(self.measureTextNode) + let makeLimitTextLayout = TextNode.asyncLayout(self.limitTextNode) let currentItem = self.item - return { item, width, neighbors in + return { item, params, neighbors in var updatedTheme: PresentationTheme? if currentItem?.theme !== item.theme { updatedTheme = item.theme } + let itemBackgroundColor: UIColor + let itemSeparatorColor: UIColor + let leftInset: CGFloat switch item.style { case .blocks: - leftInset = 16.0 + itemBackgroundColor = item.theme.list.itemBlocksBackgroundColor + itemSeparatorColor = item.theme.list.itemBlocksSeparatorColor + leftInset = 16.0 + params.leftInset case .plain: - leftInset = 35.0 + itemBackgroundColor = item.theme.list.plainBackgroundColor + itemSeparatorColor = item.theme.list.itemPlainSeparatorColor + leftInset = 35.0 + params.rightInset + } + + var limitTextString: NSAttributedString? + if let maxLength = item.maxLength { + limitTextString = NSAttributedString(string: "\(max(0, maxLength - item.text.count))", font: titleFont, textColor: item.theme.list.itemSecondaryTextColor) + } + + let (limitTextLayout, limitTextApply) = makeLimitTextLayout(TextNodeLayoutArguments(attributedString: limitTextString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: 100.0), alignment: .left, cutout: nil, insets: UIEdgeInsets())) + + var rightInset: CGFloat = params.rightInset + if !limitTextLayout.size.width.isZero { + rightInset += limitTextLayout.size.width + 4.0 } var measureText = item.text @@ -130,14 +154,14 @@ class ItemListMultilineInputItemNode: ListViewItemNode, ASEditableTextNodeDelega } let attributedMeasureText = NSAttributedString(string: measureText, font: Font.regular(17.0), textColor: .black) let attributedText = NSAttributedString(string: item.text, font: Font.regular(17.0), textColor: item.theme.list.itemPrimaryTextColor) - let (textLayout, textApply) = makeTextLayout(attributedMeasureText, nil, 0, .end, CGSize(width: width - 8 - leftInset, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedMeasureText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - 16.0 - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let separatorHeight = UIScreenPixel let textTopInset: CGFloat = 11.0 let textBottomInset: CGFloat = 11.0 - let contentSize = CGSize(width: width, height: textLayout.size.height + textTopInset + textBottomInset) + let contentSize = CGSize(width: params.width, height: textLayout.size.height + textTopInset + textBottomInset) let insets = itemListNeighborsGroupedInsets(neighbors) let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) @@ -148,11 +172,12 @@ class ItemListMultilineInputItemNode: ListViewItemNode, ASEditableTextNodeDelega return (layout, { [weak self] in if let strongSelf = self { strongSelf.item = item + strongSelf.layoutParams = params if let _ = updatedTheme { - strongSelf.topStripeNode.backgroundColor = item.theme.list.itemSeparatorColor - strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemSeparatorColor - strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBackgroundColor + strongSelf.topStripeNode.backgroundColor = itemSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor + strongSelf.backgroundNode.backgroundColor = itemBackgroundColor if strongSelf.isNodeLoaded { strongSelf.textNode.typingAttributes = [NSAttributedStringKey.font.rawValue: Font.regular(17.0), NSAttributedStringKey.foregroundColor.rawValue: item.theme.list.itemPrimaryTextColor] @@ -190,17 +215,28 @@ class ItemListMultilineInputItemNode: ListViewItemNode, ASEditableTextNodeDelega default: bottomStripeInset = 0.0 } - strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) - if strongSelf.textNode.attributedPlaceholderText == nil || !strongSelf.textNode.attributedPlaceholderText!.isEqual(to: attributedPlaceholderText) { strongSelf.textNode.attributedPlaceholderText = attributedPlaceholderText } - strongSelf.textClippingNode.frame = CGRect(origin: CGPoint(x: leftInset, y: textTopInset), size: CGSize(width: width - leftInset, height: textLayout.size.height)) - strongSelf.textNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: width - leftInset - 8.0, height: textLayout.size.height)) + strongSelf.textNode.keyboardAppearance = item.theme.chatList.searchBarKeyboardColor.keyboardAppearance + + strongSelf.textClippingNode.frame = CGRect(origin: CGPoint(x: leftInset, y: textTopInset), size: CGSize(width: params.width - leftInset - params.rightInset, height: textLayout.size.height)) + strongSelf.textNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: params.width - leftInset - 16.0 - rightInset, height: textLayout.size.height)) + + let _ = limitTextApply() + strongSelf.limitTextNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 16.0 - limitTextLayout.size.width, y: textTopInset), size: limitTextLayout.size) + if limitTextString != nil { + if strongSelf.limitTextNode.supernode == nil { + strongSelf.addSubnode(strongSelf.limitTextNode) + } + } else if strongSelf.limitTextNode.supernode != nil { + strongSelf.limitTextNode.removeFromSupernode() + } } }) } @@ -217,34 +253,42 @@ class ItemListMultilineInputItemNode: ListViewItemNode, ASEditableTextNodeDelega override func animateFrameTransition(_ progress: CGFloat, _ currentValue: CGFloat) { super.animateFrameTransition(progress, currentValue) + guard let params = self.layoutParams else { + return + } + let separatorHeight = UIScreenPixel let insets = self.insets - let width = self.bounds.size.width - let contentSize = CGSize(width: width, height: max(1.0, currentValue - insets.top - insets.bottom)) + let contentSize = CGSize(width: params.width, height: max(1.0, currentValue - insets.top - insets.bottom)) if let item = self.item { let leftInset: CGFloat switch item.style { case .blocks: - leftInset = 16.0 + leftInset = 16.0 + params.leftInset case .plain: - leftInset = 35.0 + leftInset = 35.0 + params.leftInset } let textTopInset: CGFloat = 11.0 let textBottomInset: CGFloat = 11.0 - self.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + self.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) self.bottomStripeNode.frame = CGRect(origin: CGPoint(x: self.bottomStripeNode.frame.minX, y: contentSize.height), size: CGSize(width: self.bottomStripeNode.frame.size.width, height: separatorHeight)) - self.textClippingNode.frame = CGRect(origin: CGPoint(x: leftInset, y: textTopInset), size: CGSize(width: max(0.0, width - leftInset), height: max(0.0, contentSize.height - textTopInset - textBottomInset))) + self.textClippingNode.frame = CGRect(origin: CGPoint(x: leftInset, y: textTopInset), size: CGSize(width: max(0.0, params.width - leftInset - params.rightInset), height: max(0.0, contentSize.height - textTopInset - textBottomInset))) } } func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) { if let item = self.item { if let text = self.textNode.attributedText?.string { - item.textUpdated(text) + var updatedText = text + if let maxLength = item.maxLength, updatedText.count > maxLength { + updatedText = String(updatedText[.. Void)? + let longTapAction: (() -> Void)? + let linkItemAction: ((TextLinkItemActionType, TextLinkItem) -> Void)? - init(theme: PresentationTheme, text: String, sectionId: ItemListSectionId, style: ItemListStyle) { + let tag: Any? + + let selectable: Bool + + init(theme: PresentationTheme, text: String, enabledEntitiyTypes: EnabledEntityTypes, sectionId: ItemListSectionId, style: ItemListStyle, action: (() -> Void)? = nil, longTapAction: (() -> Void)? = nil, linkItemAction: ((TextLinkItemActionType, TextLinkItem) -> Void)? = nil, tag: Any? = nil) { self.theme = theme self.text = text + self.enabledEntitiyTypes = enabledEntitiyTypes self.sectionId = sectionId self.style = style + self.action = action + self.longTapAction = longTapAction + self.linkItemAction = linkItemAction + self.tag = tag + + self.selectable = action != nil } - func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { async { let node = ItemListMultilineTextItemNode() - let (layout, apply) = node.asyncLayout()(self, width, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) node.contentSize = layout.contentSize node.insets = layout.insets @@ -30,13 +56,13 @@ class ItemListMultilineTextItem: ListViewItem, ItemListItem { } } - func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { if let node = node as? ItemListMultilineTextItemNode { Queue.mainQueue().async { let makeLayout = node.asyncLayout() async { - let (layout, apply) = makeLayout(self, width, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) Queue.mainQueue().async { completion(layout, { apply() @@ -46,31 +72,45 @@ class ItemListMultilineTextItem: ListViewItem, ItemListItem { } } } + + func selected(listView: ListView){ + listView.clearHighlightAnimated(true) + self.action?() + } } private let titleFont = Font.regular(17.0) +private let titleBoldFont = Font.medium(17.0) +private let titleFixedFont = Font.regular(17.0) class ItemListMultilineTextItemNode: ListViewItemNode { private let backgroundNode: ASDisplayNode private let topStripeNode: ASDisplayNode private let bottomStripeNode: ASDisplayNode private let highlightedBackgroundNode: ASDisplayNode + private var linkHighlightingNode: LinkHighlightingNode? private let textNode: TextNode private var item: ItemListMultilineTextItem? + var tag: Any? { + return self.item?.tag + } + + override var canBeLongTapped: Bool { + return true + } + init() { self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true self.backgroundNode.backgroundColor = .white self.topStripeNode = ASDisplayNode() - self.topStripeNode.backgroundColor = UIColor(rgb: 0xc8c7cc) self.topStripeNode.isLayerBacked = true self.bottomStripeNode = ASDisplayNode() - self.bottomStripeNode.backgroundColor = UIColor(rgb: 0xc8c7cc) self.bottomStripeNode.isLayerBacked = true self.textNode = TextNode() @@ -79,7 +119,6 @@ class ItemListMultilineTextItemNode: ListViewItemNode { self.textNode.contentsScale = UIScreen.main.scale self.highlightedBackgroundNode = ASDisplayNode() - self.highlightedBackgroundNode.backgroundColor = UIColor(rgb: 0xd9d9d9) self.highlightedBackgroundNode.isLayerBacked = true super.init(layerBacked: false, dynamicBounce: false) @@ -87,12 +126,30 @@ class ItemListMultilineTextItemNode: ListViewItemNode { self.addSubnode(self.textNode) } - func asyncLayout() -> (_ item: ItemListMultilineTextItem, _ width: CGFloat, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + override func didLoad() { + super.didLoad() + + let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:))) + recognizer.tapActionAtPoint = { [weak self] point in + if let strongSelf = self, strongSelf.linkItemAtPoint(point) != nil { + return .waitForSingleTap + } + return .fail + } + recognizer.highlight = { [weak self] point in + if let strongSelf = self { + strongSelf.updateTouchesAtPoint(point) + } + } + self.view.addGestureRecognizer(recognizer) + } + + func asyncLayout() -> (_ item: ItemListMultilineTextItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { let makeTextLayout = TextNode.asyncLayout(self.textNode) let currentItem = self.item - return { item, width, neighbors in + return { item, params, neighbors in var updatedTheme: PresentationTheme? if currentItem?.theme !== item.theme { @@ -102,15 +159,24 @@ class ItemListMultilineTextItemNode: ListViewItemNode { let textColor: UIColor = item.theme.list.itemPrimaryTextColor let leftInset: CGFloat + let itemBackgroundColor: UIColor + let itemSeparatorColor: UIColor switch item.style { case .plain: - leftInset = 35.0 + itemBackgroundColor = item.theme.list.plainBackgroundColor + itemSeparatorColor = item.theme.list.itemPlainSeparatorColor + leftInset = 35.0 + params.leftInset case .blocks: - leftInset = 16.0 + itemBackgroundColor = item.theme.list.itemBlocksBackgroundColor + itemSeparatorColor = item.theme.list.itemBlocksSeparatorColor + leftInset = 16.0 + params.rightInset } - let (titleLayout, titleApply) = makeTextLayout(NSAttributedString(string: item.text, font: titleFont, textColor: textColor), nil, 0, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let entities = generateTextEntities(item.text, enabledTypes: item.enabledEntitiyTypes) + let string = stringWithAppliedEntities(item.text, entities: entities, baseColor: textColor, linkColor: item.theme.list.itemAccentColor, baseFont: titleFont, boldFont: titleBoldFont, fixedFont: titleFixedFont) + + let (titleLayout, titleApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: string, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let contentSize: CGSize let insets: UIEdgeInsets @@ -118,10 +184,10 @@ class ItemListMultilineTextItemNode: ListViewItemNode { switch item.style { case .plain: - contentSize = CGSize(width: width, height: titleLayout.size.height + 22.0) + contentSize = CGSize(width: params.width, height: titleLayout.size.height + 22.0) insets = itemListNeighborsPlainInsets(neighbors) case .blocks: - contentSize = CGSize(width: width, height: titleLayout.size.height + 22.0) + contentSize = CGSize(width: params.width, height: titleLayout.size.height + 22.0) insets = itemListNeighborsGroupedInsets(neighbors) } @@ -133,9 +199,9 @@ class ItemListMultilineTextItemNode: ListViewItemNode { strongSelf.item = item if let _ = updatedTheme { - strongSelf.topStripeNode.backgroundColor = item.theme.list.itemSeparatorColor - strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemSeparatorColor - strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBackgroundColor + strongSelf.topStripeNode.backgroundColor = itemSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor + strongSelf.backgroundNode.backgroundColor = itemBackgroundColor strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor } @@ -153,7 +219,7 @@ class ItemListMultilineTextItemNode: ListViewItemNode { strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0) } - strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: width - leftInset, height: separatorHeight)) + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight)) case .blocks: if strongSelf.backgroundNode.supernode == nil { strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) @@ -165,38 +231,38 @@ class ItemListMultilineTextItemNode: ListViewItemNode { strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) } switch neighbors.top { - case .sameSection(false): - strongSelf.topStripeNode.isHidden = true - default: - strongSelf.topStripeNode.isHidden = false + case .sameSection(false): + strongSelf.topStripeNode.isHidden = true + default: + strongSelf.topStripeNode.isHidden = false } let bottomStripeInset: CGFloat let bottomStripeOffset: CGFloat switch neighbors.bottom { - case .sameSection(false): - bottomStripeInset = 16.0 - bottomStripeOffset = -separatorHeight - default: - bottomStripeInset = 0.0 - bottomStripeOffset = 0.0 + case .sameSection(false): + bottomStripeInset = 16.0 + bottomStripeOffset = -separatorHeight + default: + bottomStripeInset = 0.0 + bottomStripeOffset = 0.0 } - strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) } strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 11.0), size: titleLayout.size) - strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: width, height: 44.0 + UIScreenPixel + UIScreenPixel)) + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: layout.contentSize.height + UIScreenPixel + UIScreenPixel)) } }) } } - override func setHighlighted(_ highlighted: Bool, animated: Bool) { - super.setHighlighted(highlighted, animated: animated) + override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { + super.setHighlighted(highlighted, at: point, animated: animated) - if highlighted { + if highlighted && self.linkItemAtPoint(point) == nil { self.highlightedBackgroundNode.alpha = 1.0 if self.highlightedBackgroundNode.supernode == nil { var anchorNode: ASDisplayNode? @@ -238,4 +304,84 @@ class ItemListMultilineTextItemNode: ListViewItemNode { override func animateRemoved(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) } + + @objc func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { + switch recognizer.state { + case .ended: + if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation { + switch gesture { + case .tap, .longTap: + if let item = self.item, let linkItem = self.linkItemAtPoint(location) { + item.linkItemAction?(gesture == .tap ? .tap : .longTap, linkItem) + } + default: + break + } + } + default: + break + } + } + + private func linkItemAtPoint(_ point: CGPoint) -> TextLinkItem? { + let textNodeFrame = self.textNode.frame + if let (_, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { + if let url = attributes[NSAttributedStringKey(rawValue: TextNode.UrlAttribute)] as? String { + return .url(url) + } else if let peerName = attributes[NSAttributedStringKey(rawValue: TextNode.TelegramPeerTextMentionAttribute)] as? String { + return .mention(peerName) + } else if let hashtag = attributes[NSAttributedStringKey(rawValue: TextNode.TelegramHashtagAttribute)] as? TelegramHashtag { + return .hashtag(hashtag.peerName, hashtag.hashtag) + } else { + return nil + } + } + return nil + } + + override func longTapped() { + self.item?.longTapAction?() + } + + private func updateTouchesAtPoint(_ point: CGPoint?) { + if let item = self.item { + var rects: [CGRect]? + if let point = point { + let textNodeFrame = self.textNode.frame + if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { + let possibleNames: [String] = [ + TextNode.UrlAttribute, + TextNode.TelegramPeerMentionAttribute, + TextNode.TelegramPeerTextMentionAttribute, + TextNode.TelegramBotCommandAttribute, + TextNode.TelegramHashtagAttribute + ] + for name in possibleNames { + if let _ = attributes[NSAttributedStringKey(rawValue: name)] { + rects = self.textNode.attributeRects(name: name, at: index) + break + } + } + } + } + + if let rects = rects { + let linkHighlightingNode: LinkHighlightingNode + if let current = self.linkHighlightingNode { + linkHighlightingNode = current + } else { + linkHighlightingNode = LinkHighlightingNode(color: item.theme.list.itemAccentColor.withAlphaComponent(0.5)) + self.linkHighlightingNode = linkHighlightingNode + self.insertSubnode(linkHighlightingNode, belowSubnode: self.textNode) + } + linkHighlightingNode.frame = self.textNode.frame + linkHighlightingNode.updateRects(rects) + } else if let linkHighlightingNode = self.linkHighlightingNode { + self.linkHighlightingNode = nil + linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in + linkHighlightingNode?.removeFromSupernode() + }) + } + } + } } diff --git a/TelegramUI/ItemListPeerActionItem.swift b/TelegramUI/ItemListPeerActionItem.swift index b30ccc1908..ad5cab5c55 100644 --- a/TelegramUI/ItemListPeerActionItem.swift +++ b/TelegramUI/ItemListPeerActionItem.swift @@ -20,10 +20,10 @@ class ItemListPeerActionItem: ListViewItem, ItemListItem { self.action = action } - func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { async { let node = ItemListPeerActionItemNode() - let (layout, apply) = node.asyncLayout()(self, width, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) node.contentSize = layout.contentSize node.insets = layout.insets @@ -34,7 +34,7 @@ class ItemListPeerActionItem: ListViewItem, ItemListItem { } } - func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { if let node = node as? ItemListPeerActionItemNode { Queue.mainQueue().async { let makeLayout = node.asyncLayout() @@ -45,7 +45,7 @@ class ItemListPeerActionItem: ListViewItem, ItemListItem { } async { - let (layout, apply) = makeLayout(self, width, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) Queue.mainQueue().async { completion(layout, { apply(animated) @@ -106,27 +106,27 @@ class ItemListPeerActionItemNode: ListViewItemNode { self.addSubnode(self.titleNode) } - func asyncLayout() -> (_ item: ItemListPeerActionItem, _ width: CGFloat, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) { + func asyncLayout() -> (_ item: ItemListPeerActionItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let currentItem = self.item - return { item, width, neighbors in + return { item, params, neighbors in var updatedTheme: PresentationTheme? if currentItem?.theme !== item.theme { updatedTheme = item.theme } - let leftInset: CGFloat = 65.0 + let leftInset: CGFloat = 65.0 + params.leftInset let editingOffset: CGFloat = (item.editing ? 38.0 : 0.0) - let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: item.title, font: titleFont, textColor: item.theme.list.itemAccentColor), nil, 1, .end, CGSize(width: width - leftInset - editingOffset, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.theme.list.itemAccentColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - editingOffset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let separatorHeight = UIScreenPixel let insets = itemListNeighborsGroupedInsets(neighbors) - let contentSize = CGSize(width: width, height: 44.0) + let contentSize = CGSize(width: params.width, height: 44.0) let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) let layoutSize = layout.size @@ -136,9 +136,9 @@ class ItemListPeerActionItemNode: ListViewItemNode { strongSelf.item = item if let _ = updatedTheme { - strongSelf.topStripeNode.backgroundColor = item.theme.list.itemSeparatorColor - strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemSeparatorColor - strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBackgroundColor + strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor } @@ -154,7 +154,7 @@ class ItemListPeerActionItemNode: ListViewItemNode { strongSelf.iconNode.image = item.icon if let image = item.icon { - transition.updateFrame(node: strongSelf.iconNode, frame: CGRect(origin: CGPoint(x: editingOffset + floor((leftInset - image.size.width) / 2.0), y: floor((contentSize.height - image.size.height) / 2.0)), size: image.size)) + transition.updateFrame(node: strongSelf.iconNode, frame: CGRect(origin: CGPoint(x: params.leftInset + editingOffset + floor((leftInset - params.leftInset - image.size.width) / 2.0), y: floor((contentSize.height - image.size.height) / 2.0)), size: image.size)) } if strongSelf.backgroundNode.supernode == nil { @@ -183,20 +183,20 @@ class ItemListPeerActionItemNode: ListViewItemNode { bottomStripeInset = 0.0 bottomStripeOffset = 0.0 } - strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))) transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + editingOffset, y: 11.0), size: titleLayout.size)) - strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: width, height: 44.0 + UIScreenPixel + UIScreenPixel)) + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: 44.0 + UIScreenPixel + UIScreenPixel)) } }) } } - override func setHighlighted(_ highlighted: Bool, animated: Bool) { - super.setHighlighted(highlighted, animated: animated) + override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { + super.setHighlighted(highlighted, at: point, animated: animated) if highlighted { self.highlightedBackgroundNode.alpha = 1.0 diff --git a/TelegramUI/ItemListPeerItem.swift b/TelegramUI/ItemListPeerItem.swift index e84100861d..ad0ac1a2f6 100644 --- a/TelegramUI/ItemListPeerItem.swift +++ b/TelegramUI/ItemListPeerItem.swift @@ -53,17 +53,9 @@ final class ItemListPeerItem: ListViewItem, ItemListItem { let removePeer: (PeerId) -> Void let toggleUpdated: ((Bool) -> Void)? - init(theme: PresentationTheme? = nil, strings: PresentationStrings? = nil, account: Account, peer: Peer, presence: PeerPresence?, text: ItemListPeerItemText, label: ItemListPeerItemLabel, editing: ItemListPeerItemEditing, switchValue: Bool?, enabled: Bool, sectionId: ItemListSectionId, action: (() -> Void)?, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, removePeer: @escaping (PeerId) -> Void, toggleUpdated: ((Bool) -> Void)? = nil) { - if let theme = theme { - self.theme = theme - } else { - self.theme = defaultPresentationTheme - } - if let strings = strings { - self.strings = strings - } else { - self.strings = defaultPresentationStrings - } + init(theme: PresentationTheme, strings: PresentationStrings, account: Account, peer: Peer, presence: PeerPresence?, text: ItemListPeerItemText, label: ItemListPeerItemLabel, editing: ItemListPeerItemEditing, switchValue: Bool?, enabled: Bool, sectionId: ItemListSectionId, action: (() -> Void)?, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, removePeer: @escaping (PeerId) -> Void, toggleUpdated: ((Bool) -> Void)? = nil) { + self.theme = theme + self.strings = strings self.account = account self.peer = peer self.presence = presence @@ -79,10 +71,10 @@ final class ItemListPeerItem: ListViewItem, ItemListItem { self.toggleUpdated = toggleUpdated } - func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { async { let node = ItemListPeerItemNode() - let (layout, apply) = node.asyncLayout()(self, width, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) node.contentSize = layout.contentSize node.insets = layout.insets @@ -93,7 +85,7 @@ final class ItemListPeerItem: ListViewItem, ItemListItem { } } - func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { if let node = node as? ItemListPeerItemNode { Queue.mainQueue().async { let makeLayout = node.asyncLayout() @@ -104,7 +96,7 @@ final class ItemListPeerItem: ListViewItem, ItemListItem { } async { - let (layout, apply) = makeLayout(self, width, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) Queue.mainQueue().async { completion(layout, { apply(animated) @@ -145,7 +137,7 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode { private var switchNode: SwitchNode? private var peerPresenceManager: PeerPresenceStatusManager? - private var layoutParams: (ItemListPeerItem, CGFloat, ItemListNeighbors)? + private var layoutParams: (ItemListPeerItem, ListViewItemLayoutParams, ItemListNeighbors)? private var editableControlNode: ItemListEditableControlNode? @@ -206,7 +198,7 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode { }) } - func asyncLayout() -> (_ item: ItemListPeerItem, _ width: CGFloat, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) { + func asyncLayout() -> (_ item: ItemListPeerItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeStatusLayout = TextNode.asyncLayout(self.statusNode) let makeLabelLayout = TextNode.asyncLayout(self.labelNode) @@ -216,11 +208,11 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode { var currentSwitchNode = self.switchNode - var currentLabelArrowNode = self.labelArrowNode + let currentLabelArrowNode = self.labelArrowNode let currentItem = self.layoutParams?.0 - return { item, width, neighbors in + return { item, params, neighbors in var updateArrowImage: UIImage? var updatedTheme: PresentationTheme? @@ -235,12 +227,12 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode { let peerRevealOptions: [ItemListRevealOption] if item.editing.editable && item.enabled { - peerRevealOptions = [ItemListRevealOption(key: 0, title: "Remove", icon: nil, color: UIColor(rgb: 0xff3824))] + peerRevealOptions = [ItemListRevealOption(key: 0, title: item.strings.Common_Delete, icon: nil, color: item.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.theme.list.itemDisclosureActions.destructive.foregroundColor)] } else { peerRevealOptions = [] } - var rightInset: CGFloat = 0.0 + var rightInset: CGFloat = params.rightInset let switchSize = CGSize(width: 51.0, height: 31.0) if let _ = item.switchValue { @@ -274,12 +266,14 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode { switch item.text { case .presence: - if let presence = item.presence as? TelegramUserPresence { + if let user = item.peer as? TelegramUser, user.botInfo != nil { + statusAttributedString = NSAttributedString(string: item.strings.Bot_GenericBotStatus, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor) + } else if let presence = item.presence as? TelegramUserPresence { let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 - let (string, activity) = stringAndActivityForUserPresence(strings: item.strings, presence: presence, relativeTo: Int32(timestamp)) + let (string, activity) = stringAndActivityForUserPresence(strings: item.strings, timeFormat: .regular, presence: presence, relativeTo: Int32(timestamp)) statusAttributedString = NSAttributedString(string: string, font: statusFont, textColor: activity ? item.theme.list.itemAccentColor : item.theme.list.itemSecondaryTextColor) } else { - statusAttributedString = NSAttributedString(string: "last seen recently", font: statusFont, textColor: item.theme.list.itemSecondaryTextColor) + statusAttributedString = NSAttributedString(string: item.strings.LastSeen_ALongTimeAgo, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor) } case let .text(text): statusAttributedString = NSAttributedString(string: text, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor) @@ -287,13 +281,13 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode { break } - let leftInset: CGFloat = 65.0 + let leftInset: CGFloat = 65.0 + params.leftInset var editableControlSizeAndApply: (CGSize, () -> ItemListEditableControlNode)? let editingOffset: CGFloat if item.editing.editing { - let sizeAndApply = editableControlLayout(48.0) + let sizeAndApply = editableControlLayout(48.0, item.theme, false) editableControlSizeAndApply = sizeAndApply editingOffset = sizeAndApply.0.width } else { @@ -323,13 +317,13 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode { labelAttributedString = NSAttributedString(string: text, font: labelDisclosureFont, textColor: item.theme.list.itemSecondaryTextColor) } - let (labelLayout, labelApply) = makeLabelLayout(labelAttributedString, nil, 1, .end, CGSize(width: width - leftInset - 16.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: labelAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 16.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let (titleLayout, titleApply) = makeTitleLayout(titleAttributedString, nil, 1, .end, CGSize(width: width - leftInset - 12.0 - labelLayout.size.width - editingOffset - rightInset - labelInset, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) - let (statusLayout, statusApply) = makeStatusLayout(statusAttributedString, nil, 1, .end, CGSize(width: width - leftInset - 8.0 - (labelLayout.size.width > 0.0 ? (labelLayout.size.width) + 15.0 : 0.0) - editingOffset - rightInset - labelInset, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 12.0 - labelLayout.size.width - editingOffset - rightInset - labelInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (statusLayout, statusApply) = makeStatusLayout(TextNodeLayoutArguments(attributedString: statusAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - (labelLayout.size.width > 0.0 ? (labelLayout.size.width) + 15.0 : 0.0) - editingOffset - rightInset - labelInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let insets = itemListNeighborsGroupedInsets(neighbors) - let contentSize = CGSize(width: width, height: 48.0) + let contentSize = CGSize(width: params.width, height: 48.0) let separatorHeight = UIScreenPixel let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) @@ -338,7 +332,7 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode { if !item.enabled { if currentDisabledOverlayNode == nil { currentDisabledOverlayNode = ASDisplayNode() - currentDisabledOverlayNode?.backgroundColor = item.theme.list.itemBackgroundColor.withAlphaComponent(0.5) + currentDisabledOverlayNode?.backgroundColor = item.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.5) } } else { currentDisabledOverlayNode = nil @@ -346,16 +340,16 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode { return (layout, { [weak self] animated in if let strongSelf = self { - strongSelf.layoutParams = (item, width, neighbors) + strongSelf.layoutParams = (item, params, neighbors) if let updateArrowImage = updateArrowImage { strongSelf.labelArrowNode?.image = updateArrowImage } if let _ = updatedTheme { - strongSelf.topStripeNode.backgroundColor = item.theme.list.itemSeparatorColor - strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemSeparatorColor - strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBackgroundColor + strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor } @@ -396,9 +390,9 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode { } strongSelf.editableControlNode = editableControlNode strongSelf.insertSubnode(editableControlNode, aboveSubnode: strongSelf.avatarNode) - let editableControlFrame = CGRect(origin: CGPoint(x: revealOffset, y: 0.0), size: editableControlSizeAndApply.0) + let editableControlFrame = CGRect(origin: CGPoint(x: params.leftInset + revealOffset, y: 0.0), size: editableControlSizeAndApply.0) editableControlNode.frame = editableControlFrame - transition.animatePosition(node: editableControlNode, from: CGPoint(x: editableControlFrame.midX - editableControlFrame.size.width, y: editableControlFrame.midY)) + transition.animatePosition(node: editableControlNode, from: CGPoint(x: -editableControlFrame.size.width / 2.0, y: editableControlFrame.midY)) editableControlNode.alpha = 0.0 transition.updateAlpha(node: editableControlNode, alpha: 1.0) } @@ -444,7 +438,7 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode { bottomStripeInset = 0.0 bottomStripeOffset = 0.0 } - strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) transition.updateFrame(node: strongSelf.topStripeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))) transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))) @@ -465,7 +459,7 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode { } } } - currentSwitchNode.frame = CGRect(origin: CGPoint(x: revealOffset + width - switchSize.width - 15.0, y: floor((contentSize.height - switchSize.height) / 2.0)), size: switchSize) + currentSwitchNode.frame = CGRect(origin: CGPoint(x: revealOffset + params.width - switchSize.width - 15.0, y: floor((contentSize.height - switchSize.height) / 2.0)), size: switchSize) if let switchValue = item.switchValue { currentSwitchNode.setOn(switchValue, animated: animated) } @@ -480,7 +474,7 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode { strongSelf.labelArrowNode = updatedLabelArrowNode strongSelf.addSubnode(updatedLabelArrowNode) if let image = updatedLabelArrowNode.image { - let labelArrowNodeFrame = CGRect(origin: CGPoint(x: width - rightLabelInset - image.size.width, y: floor((contentSize.height - image.size.height) / 2.0)), size: image.size) + let labelArrowNodeFrame = CGRect(origin: CGPoint(x: params.width - params.rightInset - rightLabelInset - image.size.width, y: floor((contentSize.height - image.size.height) / 2.0)), size: image.size) transition.updateFrame(node: updatedLabelArrowNode, frame: labelArrowNodeFrame) rightLabelInset += 19.0 } @@ -489,17 +483,19 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode { strongSelf.labelArrowNode = nil } - transition.updateFrame(node: strongSelf.labelNode, frame: CGRect(origin: CGPoint(x: revealOffset + width - labelLayout.size.width - rightLabelInset - rightInset, y: floor((contentSize.height - labelLayout.size.height) / 2.0)), size: labelLayout.size)) + transition.updateFrame(node: strongSelf.labelNode, frame: CGRect(origin: CGPoint(x: revealOffset + params.width - labelLayout.size.width - rightLabelInset - rightInset, y: floor((contentSize.height - labelLayout.size.height) / 2.0)), size: labelLayout.size)) - transition.updateFrame(node: strongSelf.avatarNode, frame: CGRect(origin: CGPoint(x: revealOffset + editingOffset + 12.0, y: 4.0), size: CGSize(width: 40.0, height: 40.0))) + transition.updateFrame(node: strongSelf.avatarNode, frame: CGRect(origin: CGPoint(x: params.leftInset + revealOffset + editingOffset + 12.0, y: 4.0), size: CGSize(width: 40.0, height: 40.0))) strongSelf.avatarNode.setPeer(account: item.account, peer: item.peer) - strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: width, height: 48.0 + UIScreenPixel + UIScreenPixel)) + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: 48.0 + UIScreenPixel + UIScreenPixel)) if let presence = item.presence as? TelegramUserPresence { strongSelf.peerPresenceManager?.reset(presence: presence) } + strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) + strongSelf.setRevealOptions(peerRevealOptions) strongSelf.setRevealOptionsOpened(item.editing.revealed, animated: animated) } @@ -507,8 +503,8 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode { } } - override func setHighlighted(_ highlighted: Bool, animated: Bool) { - super.setHighlighted(highlighted, animated: animated) + override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { + super.setHighlighted(highlighted, at: point, animated: animated) if highlighted { self.highlightedBackgroundNode.alpha = 1.0 @@ -556,35 +552,38 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode { override func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { super.updateRevealOffset(offset: offset, transition: transition) - let leftInset: CGFloat = 65.0 - let width = self.bounds.size.width + guard let params = self.layoutParams?.1 else { + return + } + + let leftInset: CGFloat = 65.0 + params.leftInset let editingOffset: CGFloat if let editableControlNode = self.editableControlNode { editingOffset = editableControlNode.bounds.size.width var editableControlFrame = editableControlNode.frame - editableControlFrame.origin.x = offset + editableControlFrame.origin.x = params.leftInset + offset transition.updateFrame(node: editableControlNode, frame: editableControlFrame) } else { editingOffset = 0.0 } transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: self.titleNode.frame.minY), size: self.titleNode.bounds.size)) - transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 25.0), size: self.statusNode.bounds.size)) + transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: self.statusNode.frame.minY), size: self.statusNode.bounds.size)) - var rightLabelInset: CGFloat = 15.0 + var rightLabelInset: CGFloat = 15.0 + params.rightInset if let labelArrowNode = self.labelArrowNode { if let image = labelArrowNode.image { - let labelArrowNodeFrame = CGRect(origin: CGPoint(x: revealOffset + width - rightLabelInset - image.size.width, y: labelArrowNode.frame.minY), size: image.size) + let labelArrowNodeFrame = CGRect(origin: CGPoint(x: revealOffset + params.width - rightLabelInset - image.size.width, y: labelArrowNode.frame.minY), size: image.size) transition.updateFrame(node: labelArrowNode, frame: labelArrowNodeFrame) rightLabelInset += 19.0 } } - transition.updateFrame(node: self.labelNode, frame: CGRect(origin: CGPoint(x: revealOffset + width - self.labelNode.bounds.size.width - rightLabelInset, y: floor((self.contentSize.height - self.labelNode.bounds.size.height) / 2.0 - self.labelNode.bounds.size.height / 10.0)), size: self.labelNode.bounds.size)) + transition.updateFrame(node: self.labelNode, frame: CGRect(origin: CGPoint(x: revealOffset + params.width - self.labelNode.bounds.size.width - rightLabelInset, y: self.labelNode.frame.minY), size: self.labelNode.bounds.size)) - transition.updateFrame(node: avatarNode, frame: CGRect(origin: CGPoint(x: revealOffset + editingOffset + 12.0, y: 4.0), size: CGSize(width: 40.0, height: 40.0))) + transition.updateFrame(node: self.avatarNode, frame: CGRect(origin: CGPoint(x: revealOffset + editingOffset + params.leftInset + 12.0, y: self.avatarNode.frame.minY), size: CGSize(width: 40.0, height: 40.0))) } override func revealOptionsInteractivelyOpened() { diff --git a/TelegramUI/ItemListRecentSessionItem.swift b/TelegramUI/ItemListRecentSessionItem.swift index 89240fb48d..db0bc1b478 100644 --- a/TelegramUI/ItemListRecentSessionItem.swift +++ b/TelegramUI/ItemListRecentSessionItem.swift @@ -55,10 +55,10 @@ final class ItemListRecentSessionItem: ListViewItem, ItemListItem { self.removeSession = removeSession } - func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { async { let node = ItemListRecentSessionItemNode() - let (layout, apply) = node.asyncLayout()(self, width, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) node.contentSize = layout.contentSize node.insets = layout.insets @@ -69,7 +69,7 @@ final class ItemListRecentSessionItem: ListViewItem, ItemListItem { } } - func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { if let node = node as? ItemListRecentSessionItemNode { Queue.mainQueue().async { let makeLayout = node.asyncLayout() @@ -80,7 +80,7 @@ final class ItemListRecentSessionItem: ListViewItem, ItemListItem { } async { - let (layout, apply) = makeLayout(self, width, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) Queue.mainQueue().async { completion(layout, { apply(animated) @@ -107,7 +107,7 @@ class ItemListRecentSessionItemNode: ItemListRevealOptionsItemNode { private let locationNode: TextNode private let labelNode: TextNode - private var layoutParams: (ItemListRecentSessionItem, CGFloat, ItemListNeighbors)? + private var layoutParams: (ItemListRecentSessionItem, ListViewItemLayoutParams, ItemListNeighbors)? private var editableControlNode: ItemListEditableControlNode? @@ -152,7 +152,7 @@ class ItemListRecentSessionItemNode: ItemListRevealOptionsItemNode { self.addSubnode(self.labelNode) } - func asyncLayout() -> (_ item: ItemListRecentSessionItem, _ width: CGFloat, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) { + func asyncLayout() -> (_ item: ItemListRecentSessionItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeAppLayout = TextNode.asyncLayout(self.appNode) let makeLocationLayout = TextNode.asyncLayout(self.locationNode) @@ -163,7 +163,7 @@ class ItemListRecentSessionItemNode: ItemListRevealOptionsItemNode { let currentItem = self.layoutParams?.0 - return { item, width, neighbors in + return { item, params, neighbors in var updatedTheme: PresentationTheme? if currentItem?.theme !== item.theme { @@ -177,12 +177,12 @@ class ItemListRecentSessionItemNode: ItemListRevealOptionsItemNode { let peerRevealOptions: [ItemListRevealOption] if item.editable && item.enabled { - peerRevealOptions = [ItemListRevealOption(key: 0, title: item.strings.AuthSessions_TerminateSession, icon: nil, color: UIColor(rgb: 0xff3824))] + peerRevealOptions = [ItemListRevealOption(key: 0, title: item.strings.AuthSessions_TerminateSession, icon: nil, color: item.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.theme.list.itemDisclosureActions.destructive.foregroundColor)] } else { peerRevealOptions = [] } - let rightInset: CGFloat = 0.0 + let rightInset: CGFloat = params.rightInset titleAttributedString = NSAttributedString(string: "\(item.session.appName) \(item.session.appVersion)", font: titleFont, textColor: item.theme.list.itemPrimaryTextColor) @@ -211,30 +211,30 @@ class ItemListRecentSessionItemNode: ItemListRevealOptionsItemNode { labelAttributedString = NSAttributedString(string: item.strings.Presence_online, font: textFont, textColor: item.theme.list.itemAccentColor) } else { let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) - let dateText = stringForRelativeTimestamp(strings: item.strings, relativeTimestamp: item.session.activityDate, relativeTo: timestamp) + let dateText = stringForRelativeTimestamp(strings: item.strings, relativeTimestamp: item.session.activityDate, relativeTo: timestamp, timeFormat: .regular) labelAttributedString = NSAttributedString(string: dateText, font: textFont, textColor: item.theme.list.itemSecondaryTextColor) } - let leftInset: CGFloat = 15.0 + let leftInset: CGFloat = 15.0 + params.leftInset var editableControlSizeAndApply: (CGSize, () -> ItemListEditableControlNode)? let editingOffset: CGFloat if item.editing { - let sizeAndApply = editableControlLayout(75.0) + let sizeAndApply = editableControlLayout(75.0, item.theme, false) editableControlSizeAndApply = sizeAndApply editingOffset = sizeAndApply.0.width } else { editingOffset = 0.0 } - let (labelLayout, labelApply) = makeLabelLayout(labelAttributedString, nil, 1, .end, CGSize(width: width - leftInset - 8.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) - let (titleLayout, titleApply) = makeTitleLayout(titleAttributedString, nil, 1, .end, CGSize(width: width - leftInset - 8.0 - editingOffset - rightInset - labelLayout.size.width - 5.0, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) - let (appLayout, appApply) = makeAppLayout(appAttributedString, nil, 1, .end, CGSize(width: width - leftInset - 8.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) - let (locationLayout, locationApply) = makeLocationLayout(locationAttributedString, nil, 1, .end, CGSize(width: width - leftInset - 8.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: labelAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - editingOffset - rightInset - labelLayout.size.width - 5.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (appLayout, appApply) = makeAppLayout(TextNodeLayoutArguments(attributedString: appAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (locationLayout, locationApply) = makeLocationLayout(TextNodeLayoutArguments(attributedString: locationAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let insets = itemListNeighborsGroupedInsets(neighbors) - let contentSize = CGSize(width: width, height: 75.0) + let contentSize = CGSize(width: params.width, height: 75.0) let separatorHeight = UIScreenPixel let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) @@ -251,12 +251,12 @@ class ItemListRecentSessionItemNode: ItemListRevealOptionsItemNode { return (layout, { [weak self] animated in if let strongSelf = self { - strongSelf.layoutParams = (item, width, neighbors) + strongSelf.layoutParams = (item, params, neighbors) if let _ = updatedTheme { - strongSelf.topStripeNode.backgroundColor = item.theme.list.itemSeparatorColor - strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemSeparatorColor - strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBackgroundColor + strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor } @@ -297,9 +297,9 @@ class ItemListRecentSessionItemNode: ItemListRevealOptionsItemNode { } strongSelf.editableControlNode = editableControlNode strongSelf.insertSubnode(editableControlNode, aboveSubnode: strongSelf.titleNode) - let editableControlFrame = CGRect(origin: CGPoint(x: revealOffset, y: 0.0), size: editableControlSizeAndApply.0) + let editableControlFrame = CGRect(origin: CGPoint(x: params.leftInset + revealOffset, y: 0.0), size: editableControlSizeAndApply.0) editableControlNode.frame = editableControlFrame - transition.animatePosition(node: editableControlNode, from: CGPoint(x: editableControlFrame.midX - editableControlFrame.size.width, y: editableControlFrame.midY)) + transition.animatePosition(node: editableControlNode, from: CGPoint(x: -editableControlFrame.size.width / 2.0, y: editableControlFrame.midY)) editableControlNode.alpha = 0.0 transition.updateAlpha(node: editableControlNode, alpha: 1.0) } @@ -344,16 +344,18 @@ class ItemListRecentSessionItemNode: ItemListRevealOptionsItemNode { bottomStripeInset = 0.0 bottomStripeOffset = 0.0 } - strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) transition.updateFrame(node: strongSelf.topStripeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))) transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))) - transition.updateFrame(node: strongSelf.labelNode, frame: CGRect(origin: CGPoint(x: revealOffset + width - labelLayout.size.width - 15.0 - rightInset, y: 10.0), size: labelLayout.size)) + transition.updateFrame(node: strongSelf.labelNode, frame: CGRect(origin: CGPoint(x: revealOffset + params.width - labelLayout.size.width - 15.0 - rightInset, y: 10.0), size: labelLayout.size)) transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 10.0), size: titleLayout.size)) transition.updateFrame(node: strongSelf.appNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 30.0), size: appLayout.size)) transition.updateFrame(node: strongSelf.locationNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 50.0), size: locationLayout.size)) - strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: width, height: 75.0 + UIScreenPixel + UIScreenPixel)) + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: 75.0 + UIScreenPixel + UIScreenPixel)) + + strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) strongSelf.setRevealOptions(peerRevealOptions) strongSelf.setRevealOptionsOpened(item.revealed, animated: animated) @@ -373,20 +375,23 @@ class ItemListRecentSessionItemNode: ItemListRevealOptionsItemNode { override func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { super.updateRevealOffset(offset: offset, transition: transition) - let leftInset: CGFloat = 15.0 - let width = self.bounds.size.width + guard let params = self.layoutParams?.1 else { + return + } + + let leftInset: CGFloat = 15.0 + params.leftInset let editingOffset: CGFloat if let editableControlNode = self.editableControlNode { editingOffset = editableControlNode.bounds.size.width var editableControlFrame = editableControlNode.frame - editableControlFrame.origin.x = offset + editableControlFrame.origin.x = params.leftInset + offset transition.updateFrame(node: editableControlNode, frame: editableControlFrame) } else { editingOffset = 0.0 } - transition.updateFrame(node: self.labelNode, frame: CGRect(origin: CGPoint(x: revealOffset + width - self.labelNode.bounds.size.width - 15.0, y: self.labelNode.frame.minY), size: self.labelNode.bounds.size)) + transition.updateFrame(node: self.labelNode, frame: CGRect(origin: CGPoint(x: revealOffset + params.width - params.rightInset - self.labelNode.bounds.size.width - 15.0, y: self.labelNode.frame.minY), size: self.labelNode.bounds.size)) transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: self.titleNode.frame.minY), size: self.titleNode.bounds.size)) transition.updateFrame(node: self.appNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: self.appNode.frame.minY), size: self.appNode.bounds.size)) transition.updateFrame(node: self.locationNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: self.locationNode.frame.minY), size: self.locationNode.bounds.size)) diff --git a/TelegramUI/ItemListRevealOptionsNode.swift b/TelegramUI/ItemListRevealOptionsNode.swift index 8b6d116ae6..8061f4da6d 100644 --- a/TelegramUI/ItemListRevealOptionsNode.swift +++ b/TelegramUI/ItemListRevealOptionsNode.swift @@ -1,12 +1,14 @@ import Foundation import AsyncDisplayKit import Display +import Lottie struct ItemListRevealOption: Equatable { let key: Int32 let title: String let icon: UIImage? let color: UIColor + let textColor: UIColor static func ==(lhs: ItemListRevealOption, rhs: ItemListRevealOption) -> Bool { if lhs.key != rhs.key { @@ -18,6 +20,9 @@ struct ItemListRevealOption: Equatable { if !lhs.color.isEqual(rhs.color) { return false } + if !lhs.textColor.isEqual(rhs.textColor) { + return false + } if lhs.icon !== rhs.icon { return false } @@ -32,13 +37,15 @@ final class ItemListRevealOptionNode: ASDisplayNode { private let titleNode: ASTextNode private let iconNode: ASImageNode? - init(title: String, icon: UIImage?, color: UIColor) { + private var animView: LOTView? + + init(title: String, icon: UIImage?, color: UIColor, textColor: UIColor) { self.titleNode = ASTextNode() - self.titleNode.attributedText = NSAttributedString(string: title, font: icon == nil ? titleFontWithoutIcon : titleFontWithIcon, textColor: .white) + self.titleNode.attributedText = NSAttributedString(string: title, font: icon == nil ? titleFontWithoutIcon : titleFontWithIcon, textColor: textColor) if let icon = icon { let iconNode = ASImageNode() - iconNode.image = icon + iconNode.image = generateTintedImage(image: icon, color: textColor) self.iconNode = iconNode } else { self.iconNode = nil @@ -53,6 +60,23 @@ final class ItemListRevealOptionNode: ASDisplayNode { self.backgroundColor = color } + override func didLoad() { + super.didLoad() + + if let url = frameworkBundle.url(forResource: "mute", withExtension: "json") { + let animView = LOTAnimationView(contentsOf: url) + animView.frame = CGRect(origin: CGPoint(), size: CGSize(width: 50.0, height: 50.0)) + self.animView = animView + self.view.addSubview(animView) + animView.loopAnimation = true + animView.logHierarchyKeypaths() + animView.setValue(UIColor.green, forKeypath: "Outlines.Group 1.Fill 1.Color", atFrame: 0) + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2.0, execute: { + animView.play() + }) + } + } + override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { let titleSize = self.titleNode.measure(constrainedSize) var maxWidth = titleSize.width @@ -84,6 +108,7 @@ final class ItemListRevealOptionsNode: ASDisplayNode { private var optionNodes: [ItemListRevealOptionNode] = [] private var revealOffset: CGFloat = 0.0 + private var rightInset: CGFloat = 0.0 init(optionSelected: @escaping (ItemListRevealOption) -> Void) { self.optionSelected = optionSelected @@ -104,7 +129,7 @@ final class ItemListRevealOptionsNode: ASDisplayNode { node.removeFromSupernode() } self.optionNodes = options.map { option in - return ItemListRevealOptionNode(title: option.title, icon: option.icon, color: option.color) + return ItemListRevealOptionNode(title: option.title, icon: option.icon, color: option.color, textColor: option.textColor) } for node in self.optionNodes { self.addSubnode(node) @@ -122,14 +147,9 @@ final class ItemListRevealOptionsNode: ASDisplayNode { return CGSize(width: maxWidth * CGFloat(self.optionNodes.count), height: constrainedSize.height) } - override func layout() { - super.layout() - - self.updateNodesLayout(transition: .immediate) - } - - func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { + func updateRevealOffset(offset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) { self.revealOffset = offset + self.rightInset = rightInset self.updateNodesLayout(transition: transition) } @@ -140,7 +160,7 @@ final class ItemListRevealOptionsNode: ASDisplayNode { } let basicNodeWidth = floorToScreenPixels(size.width / CGFloat(self.optionNodes.count)) let lastNodeWidth = size.width - basicNodeWidth * CGFloat(self.optionNodes.count - 1) - let revealFactor = self.revealOffset / size.width + let revealFactor = min(1.0, self.revealOffset / (size.width + self.rightInset)) var leftOffset: CGFloat = 0.0 for i in 0 ..< self.optionNodes.count { let node = self.optionNodes[i] diff --git a/TelegramUI/ItemListSectionHeaderItem.swift b/TelegramUI/ItemListSectionHeaderItem.swift index 5a2cee75ed..97e87d51af 100644 --- a/TelegramUI/ItemListSectionHeaderItem.swift +++ b/TelegramUI/ItemListSectionHeaderItem.swift @@ -10,20 +10,16 @@ class ItemListSectionHeaderItem: ListViewItem, ItemListItem { let isAlwaysPlain: Bool = true - init(theme: PresentationTheme? = nil, text: String, sectionId: ItemListSectionId) { - if let theme = theme { - self.theme = theme - } else { - self.theme = defaultPresentationTheme - } + init(theme: PresentationTheme, text: String, sectionId: ItemListSectionId) { + self.theme = theme self.text = text self.sectionId = sectionId } - func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { async { let node = ItemListSectionHeaderItemNode() - let (layout, apply) = node.asyncLayout()(self, width, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) node.contentSize = layout.contentSize node.insets = layout.insets @@ -34,7 +30,7 @@ class ItemListSectionHeaderItem: ListViewItem, ItemListItem { } } - func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { guard let node = node as? ItemListSectionHeaderItemNode else { assertionFailure() return @@ -44,7 +40,7 @@ class ItemListSectionHeaderItem: ListViewItem, ItemListItem { let makeLayout = node.asyncLayout() async { - let (layout, apply) = makeLayout(self, width, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) Queue.mainQueue().async { completion(layout, { apply() @@ -71,18 +67,18 @@ class ItemListSectionHeaderItemNode: ListViewItemNode { self.addSubnode(self.titleNode) } - func asyncLayout() -> (_ item: ItemListSectionHeaderItem, _ width: CGFloat, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + func asyncLayout() -> (_ item: ItemListSectionHeaderItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) - return { item, width, neighbors in - let leftInset: CGFloat = 15.0 + return { item, params, neighbors in + let leftInset: CGFloat = 15.0 + params.leftInset - let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: item.text, font: titleFont, textColor: item.theme.list.sectionHeaderTextColor), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.text, font: titleFont, textColor: item.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let contentSize: CGSize var insets = UIEdgeInsets() - contentSize = CGSize(width: width, height: 30.0) + contentSize = CGSize(width: params.width, height: 30.0) switch neighbors.top { case .none: insets.top += 24.0 diff --git a/TelegramUI/ItemListSelectableControlNode.swift b/TelegramUI/ItemListSelectableControlNode.swift new file mode 100644 index 0000000000..2dc4faf423 --- /dev/null +++ b/TelegramUI/ItemListSelectableControlNode.swift @@ -0,0 +1,34 @@ +import Foundation +import AsyncDisplayKit +import Display + +final class ItemListSelectableControlNode: ASDisplayNode { + private let checkNode: CheckNode + + init(strokeColor: UIColor, fillColor: UIColor, foregroundColor: UIColor) { + self.checkNode = CheckNode(strokeColor: strokeColor, fillColor: fillColor, foregroundColor: foregroundColor, style: .plain) + + super.init() + + self.addSubnode(self.checkNode) + } + + static func asyncLayout(_ node: ItemListSelectableControlNode?) -> (_ strokeColor: UIColor, _ fillColor: UIColor, _ foregroundColor: UIColor, _ selected: Bool) -> (CGFloat, (CGSize, Bool) -> ItemListSelectableControlNode) { + return { strokeColor, fillColor, foregroundColor, selected in + let resultNode: ItemListSelectableControlNode + if let node = node { + resultNode = node + } else { + resultNode = ItemListSelectableControlNode(strokeColor: strokeColor, fillColor: fillColor, foregroundColor: foregroundColor) + } + + return (45.0, { size, animated in + + let checkSize = CGSize(width: 32.0, height: 32.0) + resultNode.checkNode.frame = CGRect(origin: CGPoint(x: 12.0, y: floor((size.height - checkSize.height) / 2.0)), size: checkSize) + resultNode.checkNode.setIsChecked(selected, animated: animated) + return resultNode + }) + } + } +} diff --git a/TelegramUI/ItemListSingleLineInputItem.swift b/TelegramUI/ItemListSingleLineInputItem.swift index 2d2622546b..f680065a2b 100644 --- a/TelegramUI/ItemListSingleLineInputItem.swift +++ b/TelegramUI/ItemListSingleLineInputItem.swift @@ -35,10 +35,10 @@ class ItemListSingleLineInputItem: ListViewItem, ItemListItem { self.action = action } - func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { async { let node = ItemListSingleLineInputItemNode() - let (layout, apply) = node.asyncLayout()(self, width, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) node.contentSize = layout.contentSize node.insets = layout.insets @@ -49,13 +49,13 @@ class ItemListSingleLineInputItem: ListViewItem, ItemListItem { } } - func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { if let node = node as? ItemListSingleLineInputItemNode { Queue.mainQueue().async { let makeLayout = node.asyncLayout() async { - let (layout, apply) = makeLayout(self, width, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) Queue.mainQueue().async { completion(layout, { apply() @@ -117,29 +117,29 @@ class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDelegate, It self.textNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0) } - func asyncLayout() -> (_ item: ItemListSingleLineInputItem, _ width: CGFloat, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + func asyncLayout() -> (_ item: ItemListSingleLineInputItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let currentItem = self.item - return { item, width, neighbors in + return { item, params, neighbors in var updatedTheme: PresentationTheme? if currentItem?.theme !== item.theme { updatedTheme = item.theme } - let leftInset: CGFloat = 16.0 + let leftInset: CGFloat = 16.0 + params.leftInset let titleString = NSMutableAttributedString(attributedString: item.title) titleString.removeAttribute(NSAttributedStringKey.font, range: NSMakeRange(0, titleString.length)) titleString.addAttributes([NSAttributedStringKey.font: Font.regular(17.0)], range: NSMakeRange(0, titleString.length)) - let (titleLayout, titleApply) = makeTitleLayout(titleString, nil, 0, .end, CGSize(width: width - 32 - leftInset, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - 32.0 - leftInset - params.rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let separatorHeight = UIScreenPixel - let contentSize = CGSize(width: width, height: 44.0) + let contentSize = CGSize(width: params.width, height: 44.0) let insets = itemListNeighborsGroupedInsets(neighbors) let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) @@ -152,9 +152,9 @@ class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDelegate, It strongSelf.item = item if let _ = updatedTheme { - strongSelf.topStripeNode.backgroundColor = item.theme.list.itemSeparatorColor - strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemSeparatorColor - strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBackgroundColor + strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor strongSelf.textNode.textField.textColor = item.theme.list.itemPrimaryTextColor strongSelf.textNode.textField.keyboardAppearance = item.theme.chatList.searchBarKeyboardColor.keyboardAppearance @@ -204,7 +204,7 @@ class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDelegate, It strongSelf.textNode.textField.text = item.text } - strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset + titleLayout.size.width + item.spacing, y: floor((layout.contentSize.height - 40.0) / 2.0)), size: CGSize(width: max(1.0, width - (leftInset + titleLayout.size.width + item.spacing)), height: 40.0)) + strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset + titleLayout.size.width + item.spacing, y: floor((layout.contentSize.height - 40.0) / 2.0)), size: CGSize(width: max(1.0, params.width - (leftInset + titleLayout.size.width + item.spacing)), height: 40.0)) if strongSelf.backgroundNode.supernode == nil { strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) @@ -228,7 +228,7 @@ class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDelegate, It default: bottomStripeInset = 0.0 } - strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) diff --git a/TelegramUI/ItemListStickerPackItem.swift b/TelegramUI/ItemListStickerPackItem.swift index 3f3c50943e..3f63e3e701 100644 --- a/TelegramUI/ItemListStickerPackItem.swift +++ b/TelegramUI/ItemListStickerPackItem.swift @@ -48,6 +48,7 @@ enum ItemListStickerPackItemControl: Equatable { final class ItemListStickerPackItem: ListViewItem, ItemListItem { let theme: PresentationTheme + let strings: PresentationStrings let account: Account let packInfo: StickerPackCollectionInfo let itemCount: String @@ -62,8 +63,9 @@ final class ItemListStickerPackItem: ListViewItem, ItemListItem { let addPack: () -> Void let removePack: () -> Void - init(theme: PresentationTheme, account: Account, packInfo: StickerPackCollectionInfo, itemCount: String, topItem: StickerPackItem?, unread: Bool, control: ItemListStickerPackItemControl, editing: ItemListStickerPackItemEditing, enabled: Bool, sectionId: ItemListSectionId, action: (() -> Void)?, setPackIdWithRevealedOptions: @escaping (ItemCollectionId?, ItemCollectionId?) -> Void, addPack: @escaping () -> Void, removePack: @escaping () -> Void) { + init(theme: PresentationTheme, strings: PresentationStrings, account: Account, packInfo: StickerPackCollectionInfo, itemCount: String, topItem: StickerPackItem?, unread: Bool, control: ItemListStickerPackItemControl, editing: ItemListStickerPackItemEditing, enabled: Bool, sectionId: ItemListSectionId, action: (() -> Void)?, setPackIdWithRevealedOptions: @escaping (ItemCollectionId?, ItemCollectionId?) -> Void, addPack: @escaping () -> Void, removePack: @escaping () -> Void) { self.theme = theme + self.strings = strings self.account = account self.packInfo = packInfo self.itemCount = itemCount @@ -79,10 +81,10 @@ final class ItemListStickerPackItem: ListViewItem, ItemListItem { self.removePack = removePack } - func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { async { let node = ItemListStickerPackItemNode() - let (layout, apply) = node.asyncLayout()(self, width, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) node.contentSize = layout.contentSize node.insets = layout.insets @@ -93,7 +95,7 @@ final class ItemListStickerPackItem: ListViewItem, ItemListItem { } } - func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { if let node = node as? ItemListStickerPackItemNode { Queue.mainQueue().async { let makeLayout = node.asyncLayout() @@ -104,7 +106,7 @@ final class ItemListStickerPackItem: ListViewItem, ItemListItem { } async { - let (layout, apply) = makeLayout(self, width, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) Queue.mainQueue().async { completion(layout, { apply(animated) @@ -140,7 +142,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { private let installationActionImageNode: ASImageNode private let installationActionNode: HighlightableButtonNode - private var layoutParams: (ItemListStickerPackItem, CGFloat, ItemListNeighbors)? + private var layoutParams: (ItemListStickerPackItem, ListViewItemLayoutParams, ItemListNeighbors)? private var editableControlNode: ItemListEditableControlNode? @@ -210,7 +212,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { self.fetchDisposable.dispose() } - func asyncLayout() -> (_ item: ItemListStickerPackItem, _ width: CGFloat, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) { + func asyncLayout() -> (_ item: ItemListStickerPackItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) { let makeImageLayout = self.imageNode.asyncLayout() let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeStatusLayout = TextNode.asyncLayout(self.statusNode) @@ -221,7 +223,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { let currentItem = self.layoutParams?.0 - return { item, width, neighbors in + return { item, params, neighbors in var titleAttributedString: NSAttributedString? var statusAttributedString: NSAttributedString? @@ -233,12 +235,12 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { let packRevealOptions: [ItemListRevealOption] if item.editing.editable && item.enabled { - packRevealOptions = [ItemListRevealOption(key: 0, title: "Remove", icon: nil, color: UIColor(rgb: 0xff3824))] + packRevealOptions = [ItemListRevealOption(key: 0, title: item.strings.Common_Delete, icon: nil, color: item.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.theme.list.itemDisclosureActions.destructive.foregroundColor)] } else { packRevealOptions = [] } - var rightInset: CGFloat = 0.0 + var rightInset: CGFloat = params.rightInset var installationActionImage: UIImage? switch item.control { @@ -261,24 +263,24 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { titleAttributedString = NSAttributedString(string: item.packInfo.title, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor) statusAttributedString = NSAttributedString(string: item.itemCount, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor) - let leftInset: CGFloat = 65.0 + let leftInset: CGFloat = 65.0 + params.leftInset var editableControlSizeAndApply: (CGSize, () -> ItemListEditableControlNode)? let editingOffset: CGFloat if item.editing.editing { - let sizeAndApply = editableControlLayout(59.0) + let sizeAndApply = editableControlLayout(59.0, item.theme, false) editableControlSizeAndApply = sizeAndApply editingOffset = sizeAndApply.0.width } else { editingOffset = 0.0 } - let (titleLayout, titleApply) = makeTitleLayout(titleAttributedString, nil, 1, .end, CGSize(width: width - leftInset - 8.0 - editingOffset - rightInset - 10.0, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) - let (statusLayout, statusApply) = makeStatusLayout(statusAttributedString, nil, 1, .end, CGSize(width: width - leftInset - 8.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - editingOffset - rightInset - 10.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (statusLayout, statusApply) = makeStatusLayout(TextNodeLayoutArguments(attributedString: statusAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let insets = itemListNeighborsGroupedInsets(neighbors) - let contentSize = CGSize(width: width, height: 59.0) + let contentSize = CGSize(width: params.width, height: 59.0) let separatorHeight = UIScreenPixel let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) @@ -287,7 +289,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { if !item.enabled { if currentDisabledOverlayNode == nil { currentDisabledOverlayNode = ASDisplayNode() - currentDisabledOverlayNode?.backgroundColor = item.theme.list.itemBackgroundColor.withAlphaComponent(0.5) + currentDisabledOverlayNode?.backgroundColor = item.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.5) } } else { currentDisabledOverlayNode = nil @@ -322,12 +324,12 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { return (layout, { [weak self] animated in if let strongSelf = self { - strongSelf.layoutParams = (item, width, neighbors) + strongSelf.layoutParams = (item, params, neighbors) if let _ = updatedTheme { - strongSelf.topStripeNode.backgroundColor = item.theme.list.itemSeparatorColor - strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemSeparatorColor - strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBackgroundColor + strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor } @@ -368,9 +370,9 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { } strongSelf.editableControlNode = editableControlNode strongSelf.insertSubnode(editableControlNode, aboveSubnode: strongSelf.imageNode) - let editableControlFrame = CGRect(origin: CGPoint(x: revealOffset, y: 0.0), size: editableControlSizeAndApply.0) + let editableControlFrame = CGRect(origin: CGPoint(x: params.leftInset + revealOffset, y: 0.0), size: editableControlSizeAndApply.0) editableControlNode.frame = editableControlFrame - transition.animatePosition(node: editableControlNode, from: CGPoint(x: editableControlFrame.midX - editableControlFrame.size.width, y: editableControlFrame.midY)) + transition.animatePosition(node: editableControlNode, from: CGPoint(x: -editableControlFrame.size.width / 2.0, y: editableControlFrame.midY)) editableControlNode.alpha = 0.0 transition.updateAlpha(node: editableControlNode, alpha: 1.0) } @@ -390,7 +392,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { let _ = titleApply() let _ = statusApply() - let installationActionFrame = CGRect(origin: CGPoint(x: width - 50.0, y: 0.0), size: CGSize(width: 50.0, height: layout.contentSize.height)) + let installationActionFrame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 50.0, y: 0.0), size: CGSize(width: 50.0, height: layout.contentSize.height)) strongSelf.installationActionNode.frame = installationActionFrame switch item.control { @@ -433,7 +435,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { bottomStripeInset = 0.0 bottomStripeOffset = 0.0 } - strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) transition.updateFrame(node: strongSelf.topStripeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))) transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))) @@ -448,13 +450,15 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: (strongSelf.unreadNode.isHidden ? 0.0 : 10.0) + leftInset + revealOffset + editingOffset, y: 11.0), size: titleLayout.size)) transition.updateFrame(node: strongSelf.statusNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 32.0), size: statusLayout.size)) - transition.updateFrame(node: strongSelf.imageNode, frame: CGRect(origin: CGPoint(x: revealOffset + editingOffset + 15.0, y: 11.0), size: CGSize(width: 34.0, height: 34.0))) + transition.updateFrame(node: strongSelf.imageNode, frame: CGRect(origin: CGPoint(x: params.leftInset + revealOffset + editingOffset + 15.0, y: 11.0), size: CGSize(width: 34.0, height: 34.0))) if let updatedImageSignal = updatedImageSignal { - strongSelf.imageNode.setSignal(account: item.account, signal: updatedImageSignal) + strongSelf.imageNode.setSignal(updatedImageSignal) } - strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: width, height: 59.0 + UIScreenPixel + UIScreenPixel)) + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: 59.0 + UIScreenPixel + UIScreenPixel)) + + strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) strongSelf.setRevealOptions(packRevealOptions) strongSelf.setRevealOptionsOpened(item.editing.revealed, animated: animated) @@ -467,8 +471,8 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { } } - override func setHighlighted(_ highlighted: Bool, animated: Bool) { - super.setHighlighted(highlighted, animated: animated) + override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { + super.setHighlighted(highlighted, at: point, animated: animated) if highlighted { self.highlightedBackgroundNode.alpha = 1.0 @@ -516,13 +520,17 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { override func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { super.updateRevealOffset(offset: offset, transition: transition) - let leftInset: CGFloat = 65.0 + guard let params = self.layoutParams?.1 else { + return + } + + let leftInset: CGFloat = 65.0 + params.leftInset let editingOffset: CGFloat if let editableControlNode = self.editableControlNode { editingOffset = editableControlNode.bounds.size.width var editableControlFrame = editableControlNode.frame - editableControlFrame.origin.x = offset + editableControlFrame.origin.x = params.leftInset + offset transition.updateFrame(node: editableControlNode, frame: editableControlFrame) } else { editingOffset = 0.0 @@ -531,7 +539,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: self.titleNode.frame.minY), size: self.titleNode.bounds.size)) transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: self.statusNode.frame.minY), size: self.statusNode.bounds.size)) - transition.updateFrame(node: self.imageNode, frame: CGRect(origin: CGPoint(x: revealOffset + editingOffset + 15.0, y: self.imageNode.frame.minY), size: CGSize(width: 34.0, height: 34.0))) + transition.updateFrame(node: self.imageNode, frame: CGRect(origin: CGPoint(x: params.leftInset + revealOffset + editingOffset + 15.0, y: self.imageNode.frame.minY), size: CGSize(width: 34.0, height: 34.0))) } override func revealOptionsInteractivelyOpened() { diff --git a/TelegramUI/ItemListSwitchItem.swift b/TelegramUI/ItemListSwitchItem.swift index f969a62748..7818985c94 100644 --- a/TelegramUI/ItemListSwitchItem.swift +++ b/TelegramUI/ItemListSwitchItem.swift @@ -12,12 +12,8 @@ class ItemListSwitchItem: ListViewItem, ItemListItem { let style: ItemListStyle let updated: (Bool) -> Void - init(theme: PresentationTheme? = nil, title: String, value: Bool, enabled: Bool = true, sectionId: ItemListSectionId, style: ItemListStyle, updated: @escaping (Bool) -> Void) { - if let theme = theme { - self.theme = theme - } else { - self.theme = defaultPresentationTheme - } + init(theme: PresentationTheme, title: String, value: Bool, enabled: Bool = true, sectionId: ItemListSectionId, style: ItemListStyle, updated: @escaping (Bool) -> Void) { + self.theme = theme self.title = title self.value = value self.enabled = enabled @@ -26,10 +22,10 @@ class ItemListSwitchItem: ListViewItem, ItemListItem { self.updated = updated } - func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { async { let node = ItemListSwitchItemNode() - let (layout, apply) = node.asyncLayout()(self, width, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) node.contentSize = layout.contentSize node.insets = layout.insets @@ -40,13 +36,13 @@ class ItemListSwitchItem: ListViewItem, ItemListItem { } } - func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { if let node = node as? ItemListSwitchItemNode { Queue.mainQueue().async { let makeLayout = node.asyncLayout() async { - let (layout, apply) = makeLayout(self, width, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) Queue.mainQueue().async { completion(layout, { var animated = true @@ -102,16 +98,18 @@ class ItemListSwitchItemNode: ListViewItemNode { (self.switchNode.view as? UISwitch)?.addTarget(self, action: #selector(self.switchValueChanged(_:)), for: .valueChanged) } - func asyncLayout() -> (_ item: ItemListSwitchItem, _ width: CGFloat, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) { + func asyncLayout() -> (_ item: ItemListSwitchItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let currentItem = self.item var currentDisabledOverlayNode = self.disabledOverlayNode - return { item, width, neighbors in + return { item, params, neighbors in let contentSize: CGSize let insets: UIEdgeInsets let separatorHeight = UIScreenPixel + let itemBackgroundColor: UIColor + let itemSeparatorColor: UIColor var updatedTheme: PresentationTheme? @@ -121,19 +119,23 @@ class ItemListSwitchItemNode: ListViewItemNode { switch item.style { case .plain: - contentSize = CGSize(width: width, height: 44.0) + itemBackgroundColor = item.theme.list.plainBackgroundColor + itemSeparatorColor = item.theme.list.itemPlainSeparatorColor + contentSize = CGSize(width: params.width, height: 44.0) insets = itemListNeighborsPlainInsets(neighbors) case .blocks: - contentSize = CGSize(width: width, height: 44.0) + itemBackgroundColor = item.theme.list.itemBlocksBackgroundColor + itemSeparatorColor = item.theme.list.itemBlocksSeparatorColor + contentSize = CGSize(width: params.width, height: 44.0) insets = itemListNeighborsGroupedInsets(neighbors) } - let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: item.title, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor), nil, 1, .end, CGSize(width: width - 80, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 80.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) if !item.enabled { if currentDisabledOverlayNode == nil { currentDisabledOverlayNode = ASDisplayNode() - currentDisabledOverlayNode?.backgroundColor = item.theme.list.itemBackgroundColor.withAlphaComponent(0.5) + currentDisabledOverlayNode?.backgroundColor = itemBackgroundColor.withAlphaComponent(0.5) } } else { currentDisabledOverlayNode = nil @@ -171,9 +173,9 @@ class ItemListSwitchItemNode: ListViewItemNode { } if let _ = updatedTheme { - strongSelf.topStripeNode.backgroundColor = item.theme.list.itemSeparatorColor - strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemSeparatorColor - strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBackgroundColor + strongSelf.topStripeNode.backgroundColor = itemSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor + strongSelf.backgroundNode.backgroundColor = itemBackgroundColor strongSelf.switchNode.frameColor = item.theme.list.itemSwitchColors.frameColor strongSelf.switchNode.contentColor = item.theme.list.itemSwitchColors.contentColor @@ -186,7 +188,7 @@ class ItemListSwitchItemNode: ListViewItemNode { switch item.style { case .plain: - leftInset = 35.0 + leftInset = 35.0 + params.leftInset if strongSelf.backgroundNode.supernode != nil { strongSelf.backgroundNode.removeFromSupernode() @@ -198,9 +200,9 @@ class ItemListSwitchItemNode: ListViewItemNode { strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0) } - strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: width - leftInset, height: separatorHeight)) + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight)) case .blocks: - leftInset = 16.0 + leftInset = 16.0 + params.leftInset if strongSelf.backgroundNode.supernode == nil { strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) @@ -220,12 +222,12 @@ class ItemListSwitchItemNode: ListViewItemNode { let bottomStripeInset: CGFloat switch neighbors.bottom { case .sameSection(false): - bottomStripeInset = 16.0 + bottomStripeInset = 16.0 + params.leftInset default: bottomStripeInset = 0.0 } - strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) } @@ -237,7 +239,7 @@ class ItemListSwitchItemNode: ListViewItemNode { } let switchSize = switchView.bounds.size - strongSelf.switchNode.frame = CGRect(origin: CGPoint(x: width - switchSize.width - 15.0, y:6.0), size: switchSize) + strongSelf.switchNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - switchSize.width - 15.0, y:6.0), size: switchSize) if switchView.isOn != item.value { switchView.setOn(item.value, animated: animated) } diff --git a/TelegramUI/ItemListTextItem.swift b/TelegramUI/ItemListTextItem.swift index e8f5c2e3da..fbb32cf8c9 100644 --- a/TelegramUI/ItemListTextItem.swift +++ b/TelegramUI/ItemListTextItem.swift @@ -20,21 +20,17 @@ class ItemListTextItem: ListViewItem, ItemListItem { let isAlwaysPlain: Bool = true - init(theme: PresentationTheme? = nil, text: ItemListTextItemText, sectionId: ItemListSectionId, linkAction: ((ItemListTextItemLinkAction) -> Void)? = nil) { - if let theme = theme { - self.theme = theme - } else { - self.theme = defaultPresentationTheme - } + init(theme: PresentationTheme, text: ItemListTextItemText, sectionId: ItemListSectionId, linkAction: ((ItemListTextItemLinkAction) -> Void)? = nil) { + self.theme = theme self.text = text self.sectionId = sectionId self.linkAction = linkAction } - func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { async { let node = ItemListTextItemNode() - let (layout, apply) = node.asyncLayout()(self, width, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) node.contentSize = layout.contentSize node.insets = layout.insets @@ -45,7 +41,7 @@ class ItemListTextItem: ListViewItem, ItemListItem { } } - func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { guard let node = node as? ItemListTextItemNode else { assertionFailure() return @@ -55,7 +51,7 @@ class ItemListTextItem: ListViewItem, ItemListItem { let makeLayout = node.asyncLayout() async { - let (layout, apply) = makeLayout(self, width, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) Queue.mainQueue().async { completion(layout, { apply() @@ -95,11 +91,11 @@ class ItemListTextItemNode: ListViewItemNode { self.view.addGestureRecognizer(recognizer) } - func asyncLayout() -> (_ item: ItemListTextItem, _ width: CGFloat, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + func asyncLayout() -> (_ item: ItemListTextItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) - return { item, width, neighbors in - let leftInset: CGFloat = 15.0 + return { item, params, neighbors in + let leftInset: CGFloat = 15.0 + params.leftInset let verticalInset: CGFloat = 7.0 let attributedText: NSAttributedString @@ -111,11 +107,11 @@ class ItemListTextItemNode: ListViewItemNode { return (TextNode.UrlAttribute, contents) })) } - let (titleLayout, titleApply) = makeTitleLayout(attributedText, nil, 0, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let contentSize: CGSize - contentSize = CGSize(width: width, height: titleLayout.size.height + verticalInset + verticalInset) + contentSize = CGSize(width: params.width, height: titleLayout.size.height + verticalInset + verticalInset) let insets = itemListNeighborsGroupedInsets(neighbors) let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) diff --git a/TelegramUI/ItemListTextWithLabelItem.swift b/TelegramUI/ItemListTextWithLabelItem.swift index 58f36b1f79..d1e04f16ab 100644 --- a/TelegramUI/ItemListTextWithLabelItem.swift +++ b/TelegramUI/ItemListTextWithLabelItem.swift @@ -7,25 +7,32 @@ final class ItemListTextWithLabelItem: ListViewItem, ItemListItem { let theme: PresentationTheme let label: String let text: String + let enabledEntitiyTypes: EnabledEntityTypes let multiline: Bool let sectionId: ItemListSectionId let action: (() -> Void)? + let longTapAction: (() -> Void)? + let linkItemAction: ((TextLinkItemActionType, TextLinkItem) -> Void)? + let tag: Any? - init(theme: PresentationTheme, label: String, text: String, multiline: Bool, sectionId: ItemListSectionId, action: (() -> Void)?, tag: Any? = nil) { + init(theme: PresentationTheme, label: String, text: String, enabledEntitiyTypes: EnabledEntityTypes, multiline: Bool, sectionId: ItemListSectionId, action: (() -> Void)?, longTapAction: (() -> Void)? = nil, linkItemAction: ((TextLinkItemActionType, TextLinkItem) -> Void)? = nil, tag: Any? = nil) { self.theme = theme self.label = label self.text = text + self.enabledEntitiyTypes = enabledEntitiyTypes self.multiline = multiline self.sectionId = sectionId self.action = action + self.longTapAction = longTapAction + self.linkItemAction = linkItemAction self.tag = tag } - func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { async { let node = ItemListTextWithLabelItemNode() - let (layout, apply) = node.asyncLayout()(self, width, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) node.contentSize = layout.contentSize node.insets = layout.insets @@ -36,13 +43,13 @@ final class ItemListTextWithLabelItem: ListViewItem, ItemListItem { } } - func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { if let node = node as? ItemListTextWithLabelItemNode { Queue.mainQueue().async { let makeLayout = node.asyncLayout() async { - let (layout, apply) = makeLayout(self, width, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) Queue.mainQueue().async { completion(layout, { apply() @@ -57,7 +64,7 @@ final class ItemListTextWithLabelItem: ListViewItem, ItemListItem { return self.action != nil } - func selected(listView: ListView){ + func selected(listView: ListView) { listView.clearHighlightAnimated(true) self.action?() } @@ -65,6 +72,8 @@ final class ItemListTextWithLabelItem: ListViewItem, ItemListItem { private let labelFont = Font.regular(14.0) private let textFont = Font.regular(17.0) +private let textBoldFont = Font.medium(17.0) +private let textFixedFont = Font.regular(17.0) class ItemListTextWithLabelItemNode: ListViewItemNode { let labelNode: TextNode @@ -74,9 +83,14 @@ class ItemListTextWithLabelItemNode: ListViewItemNode { private let topStripeNode: ASDisplayNode private let bottomStripeNode: ASDisplayNode private let highlightedBackgroundNode: ASDisplayNode + private var linkHighlightingNode: LinkHighlightingNode? var item: ItemListTextWithLabelItem? + override var canBeLongTapped: Bool { + return true + } + init() { self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true @@ -106,35 +120,57 @@ class ItemListTextWithLabelItemNode: ListViewItemNode { self.addSubnode(self.textNode) } - func asyncLayout() -> (_ item: ItemListTextWithLabelItem, _ width: CGFloat, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + override func didLoad() { + super.didLoad() + + let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:))) + recognizer.tapActionAtPoint = { [weak self] point in + if let strongSelf = self, strongSelf.linkItemAtPoint(point) != nil { + return .waitForSingleTap + } + return .fail + } + recognizer.highlight = { [weak self] point in + if let strongSelf = self { + strongSelf.updateTouchesAtPoint(point) + } + } + self.view.addGestureRecognizer(recognizer) + } + + func asyncLayout() -> (_ item: ItemListTextWithLabelItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { let makeLabelLayout = TextNode.asyncLayout(self.labelNode) let makeTextLayout = TextNode.asyncLayout(self.textNode) let currentItem = self.item - return { item, width, neighbors in + return { item, params, neighbors in var updatedTheme: PresentationTheme? if currentItem?.theme !== item.theme { updatedTheme = item.theme } let insets = itemListNeighborsPlainInsets(neighbors) - let leftInset: CGFloat = 35.0 + let leftInset: CGFloat = 35.0 + params.leftInset + let rightInset: CGFloat = 8.0 + params.rightInset let separatorHeight = UIScreenPixel - let (labelLayout, labelApply) = makeLabelLayout(NSAttributedString(string: item.label, font: labelFont, textColor: item.theme.list.itemAccentColor), nil, 1, .end, CGSize(width: width - leftInset - 8.0, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) - let (textLayout, textApply) = makeTextLayout(NSAttributedString(string: item.text, font: textFont, textColor: item.theme.list.itemPrimaryTextColor), nil, item.multiline ? 0 : 1, .end, CGSize(width: width - leftInset - 8.0, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) - let contentSize = CGSize(width: width, height: textLayout.size.height + 39.0) + let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.label, font: labelFont, textColor: item.theme.list.itemAccentColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let entities = generateTextEntities(item.text, enabledTypes: item.enabledEntitiyTypes) + let string = stringWithAppliedEntities(item.text, entities: entities, baseColor: item.theme.list.itemPrimaryTextColor, linkColor: item.theme.list.itemAccentColor, baseFont: textFont, boldFont: textBoldFont, fixedFont: textFixedFont) + + let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: string, backgroundColor: nil, maximumNumberOfLines: item.multiline ? 0 : 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let contentSize = CGSize(width: params.width, height: textLayout.size.height + 39.0) let nodeLayout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) - let layoutSize = nodeLayout.size return (nodeLayout, { [weak self] in if let strongSelf = self { strongSelf.item = item if let _ = updatedTheme { - strongSelf.topStripeNode.backgroundColor = item.theme.list.itemSeparatorColor - strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemSeparatorColor - strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBackgroundColor + strongSelf.topStripeNode.backgroundColor = item.theme.list.itemPlainSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemPlainSeparatorColor + strongSelf.backgroundNode.backgroundColor = item.theme.list.plainBackgroundColor strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor } @@ -148,7 +184,7 @@ class ItemListTextWithLabelItemNode: ListViewItemNode { let style = ItemListStyle.plain switch style { case .plain: - leftInset = 35.0 + leftInset = 35.0 + params.leftInset if strongSelf.backgroundNode.supernode != nil { strongSelf.backgroundNode.removeFromSupernode() @@ -160,9 +196,9 @@ class ItemListTextWithLabelItemNode: ListViewItemNode { strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0) } - strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: width - leftInset, height: separatorHeight)) + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight)) case .blocks: - leftInset = 16.0 + leftInset = 16.0 + params.leftInset if strongSelf.backgroundNode.supernode == nil { strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) @@ -183,27 +219,27 @@ class ItemListTextWithLabelItemNode: ListViewItemNode { let bottomStripeOffset: CGFloat switch neighbors.bottom { case .sameSection(false): - bottomStripeInset = 16.0 + bottomStripeInset = 16.0 + params.leftInset bottomStripeOffset = -separatorHeight default: bottomStripeInset = 0.0 bottomStripeOffset = 0.0 } - strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) - strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) - strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight)) + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight)) } - strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: width, height: contentSize.height + UIScreenPixel + UIScreenPixel)) + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: contentSize.height + UIScreenPixel + UIScreenPixel)) } }) } } - override func setHighlighted(_ highlighted: Bool, animated: Bool) { - super.setHighlighted(highlighted, animated: animated) + override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { + super.setHighlighted(highlighted, at: point, animated: animated) - if highlighted { + if highlighted && self.linkItemAtPoint(point) == nil { self.highlightedBackgroundNode.alpha = 1.0 if self.highlightedBackgroundNode.supernode == nil { var anchorNode: ASDisplayNode? @@ -238,6 +274,40 @@ class ItemListTextWithLabelItemNode: ListViewItemNode { } } + @objc func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { + switch recognizer.state { + case .ended: + if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation { + switch gesture { + case .tap, .longTap: + if let item = self.item, let linkItem = self.linkItemAtPoint(location) { + item.linkItemAction?(gesture == .tap ? .tap : .longTap, linkItem) + } + default: + break + } + } + default: + break + } + } + + private func linkItemAtPoint(_ point: CGPoint) -> TextLinkItem? { + let textNodeFrame = self.textNode.frame + if let (_, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { + if let url = attributes[NSAttributedStringKey(rawValue: TextNode.UrlAttribute)] as? String { + return .url(url) + } else if let peerName = attributes[NSAttributedStringKey(rawValue: TextNode.TelegramPeerTextMentionAttribute)] as? String { + return .mention(peerName) + } else if let hashtag = attributes[NSAttributedStringKey(rawValue: TextNode.TelegramHashtagAttribute)] as? TelegramHashtag { + return .hashtag(hashtag.peerName, hashtag.hashtag) + } else { + return nil + } + } + return nil + } + override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } @@ -246,6 +316,52 @@ class ItemListTextWithLabelItemNode: ListViewItemNode { self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) } + override func longTapped() { + self.item?.longTapAction?() + } + + private func updateTouchesAtPoint(_ point: CGPoint?) { + if let item = self.item { + var rects: [CGRect]? + if let point = point { + let textNodeFrame = self.textNode.frame + if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { + let possibleNames: [String] = [ + TextNode.UrlAttribute, + TextNode.TelegramPeerMentionAttribute, + TextNode.TelegramPeerTextMentionAttribute, + TextNode.TelegramBotCommandAttribute, + TextNode.TelegramHashtagAttribute + ] + for name in possibleNames { + if let _ = attributes[NSAttributedStringKey(rawValue: name)] { + rects = self.textNode.attributeRects(name: name, at: index) + break + } + } + } + } + + if let rects = rects { + let linkHighlightingNode: LinkHighlightingNode + if let current = self.linkHighlightingNode { + linkHighlightingNode = current + } else { + linkHighlightingNode = LinkHighlightingNode(color: item.theme.list.itemAccentColor.withAlphaComponent(0.5)) + self.linkHighlightingNode = linkHighlightingNode + self.insertSubnode(linkHighlightingNode, belowSubnode: self.textNode) + } + linkHighlightingNode.frame = self.textNode.frame + linkHighlightingNode.updateRects(rects) + } else if let linkHighlightingNode = self.linkHighlightingNode { + self.linkHighlightingNode = nil + linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in + linkHighlightingNode?.removeFromSupernode() + }) + } + } + } + var tag: Any? { return self.item?.tag } diff --git a/TelegramUI/JoinLinkPreviewController.swift b/TelegramUI/JoinLinkPreviewController.swift new file mode 100644 index 0000000000..d25e425aa3 --- /dev/null +++ b/TelegramUI/JoinLinkPreviewController.swift @@ -0,0 +1,107 @@ +import Foundation +import Display +import AsyncDisplayKit +import Postbox +import TelegramCore +import SwiftSignalKit + +public final class JoinLinkPreviewController: ViewController { + private var controllerNode: JoinLinkPreviewControllerNode { + return self.displayNode as! JoinLinkPreviewControllerNode + } + + private var animatedIn = false + + private let account: Account + private let link: String + private let navigateToPeer: (PeerId) -> Void + private var presentationData: PresentationData + + private let disposable = MetaDisposable() + + public init(account: Account, link: String, navigateToPeer: @escaping (PeerId) -> Void) { + self.account = account + self.link = link + self.navigateToPeer = navigateToPeer + + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + + super.init(navigationBarTheme: nil) + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.disposable.dispose() + } + + override public func loadDisplayNode() { + self.displayNode = JoinLinkPreviewControllerNode(account: self.account, requestLayout: { [weak self] transition in + self?.requestLayout(transition: transition) + }) + self.controllerNode.dismiss = { [weak self] in + self?.presentingViewController?.dismiss(animated: false, completion: nil) + } + self.controllerNode.cancel = { [weak self] in + self?.dismiss() + } + self.controllerNode.join = { [weak self] in + self?.join() + } + self.displayNodeDidLoad() + self.disposable.set((joinLinkInformation(self.link, account: self.account) |> deliverOnMainQueue).start(next: { [weak self] result in + if let strongSelf = self { + switch result { + case let .invite(title, photoRepresentation, participantsCount, participants): + let data = JoinLinkPreviewData(isGroup: participants != nil, isJoined: false) + strongSelf.controllerNode.setPeer(image: photoRepresentation, title: title, memberCount: participantsCount, members: participants ?? [], data: data) + case let .alreadyJoined(peerId): + strongSelf.navigateToPeer(peerId) + strongSelf.dismiss() + default: + break + } + } + })) + self.ready.set(self.controllerNode.ready.get()) + } + + override public func loadView() { + super.loadView() + + self.statusBar.removeFromSupernode() + } + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if !self.animatedIn { + self.animatedIn = true + self.controllerNode.animateIn() + } + } + + override public func dismiss(completion: (() -> Void)? = nil) { + self.controllerNode.animateOut(completion: completion) + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) + } + + private func join() { + self.disposable.set((joinChatInteractively(with: self.link, account: self.account) |> deliverOnMainQueue).start(next: { [weak self] peerId in + if let strongSelf = self { + if let peerId = peerId { + strongSelf.navigateToPeer(peerId) + strongSelf.dismiss() + } + } + })) + } +} + diff --git a/TelegramUI/JoinLinkPreviewControllerNode.swift b/TelegramUI/JoinLinkPreviewControllerNode.swift new file mode 100644 index 0000000000..671f75571e --- /dev/null +++ b/TelegramUI/JoinLinkPreviewControllerNode.swift @@ -0,0 +1,451 @@ +import Foundation +import Display +import AsyncDisplayKit +import SwiftSignalKit +import Postbox +import TelegramCore + +struct JoinLinkPreviewData { + let isGroup: Bool + let isJoined: Bool +} + +final class JoinLinkPreviewControllerNode: ViewControllerTracingNode, UIScrollViewDelegate { + private let account: Account + private var presentationData: PresentationData + + private let requestLayout: (ContainedViewLayoutTransition) -> Void + + private var containerLayout: (ContainerViewLayout, CGFloat, CGFloat)? + + private let dimNode: ASDisplayNode + + private let wrappingScrollNode: ASScrollNode + private let cancelButtonNode: ASButtonNode + + private let contentContainerNode: ASDisplayNode + private let contentBackgroundNode: ASImageNode + + private var contentNode: (ASDisplayNode & ShareContentContainerNode)? + private var previousContentNode: (ASDisplayNode & ShareContentContainerNode)? + private var animateContentNodeOffsetFromBackgroundOffset: CGFloat? + + private let actionsBackgroundNode: ASImageNode + private let actionButtonNode: ShareActionButtonNode + private let actionSeparatorNode: ASDisplayNode + + var dismiss: (() -> Void)? + var cancel: (() -> Void)? + var join: (() -> Void)? + + let ready = Promise() + private var didSetReady = false + + private var scheduledLayoutTransitionRequestId: Int = 0 + private var scheduledLayoutTransitionRequest: (Int, ContainedViewLayoutTransition)? + + private let disposable = MetaDisposable() + + init(account: Account, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void) { + self.account = account + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + + self.requestLayout = requestLayout + + let roundedBackground = generateStretchableFilledCircleImage(radius: 16.0, color: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor) + let highlightedRoundedBackground = generateStretchableFilledCircleImage(radius: 16.0, color: self.presentationData.theme.actionSheet.opaqueItemHighlightedBackgroundColor) + + let theme = self.presentationData.theme + let halfRoundedBackground = generateImage(CGSize(width: 32.0, height: 32.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(theme.actionSheet.opaqueItemBackgroundColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height))) + context.fill(CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height / 2.0))) + })?.stretchableImage(withLeftCapWidth: 16, topCapHeight: 1) + + let highlightedHalfRoundedBackground = generateImage(CGSize(width: 32.0, height: 32.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(theme.actionSheet.opaqueItemHighlightedBackgroundColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height))) + context.fill(CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height / 2.0))) + })?.stretchableImage(withLeftCapWidth: 16, topCapHeight: 1) + + self.wrappingScrollNode = ASScrollNode() + self.wrappingScrollNode.view.alwaysBounceVertical = true + self.wrappingScrollNode.view.delaysContentTouches = false + self.wrappingScrollNode.view.canCancelContentTouches = true + + self.dimNode = ASDisplayNode() + self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) + + self.cancelButtonNode = ASButtonNode() + self.cancelButtonNode.displaysAsynchronously = false + self.cancelButtonNode.setBackgroundImage(roundedBackground, for: .normal) + self.cancelButtonNode.setBackgroundImage(highlightedRoundedBackground, for: .highlighted) + + self.contentContainerNode = ASDisplayNode() + self.contentContainerNode.isOpaque = false + self.contentContainerNode.clipsToBounds = true + + self.contentBackgroundNode = ASImageNode() + self.contentBackgroundNode.displaysAsynchronously = false + self.contentBackgroundNode.displayWithoutProcessing = true + self.contentBackgroundNode.image = roundedBackground + + self.actionsBackgroundNode = ASImageNode() + self.actionsBackgroundNode.isLayerBacked = true + self.actionsBackgroundNode.displayWithoutProcessing = true + self.actionsBackgroundNode.displaysAsynchronously = false + self.actionsBackgroundNode.image = halfRoundedBackground + + self.actionButtonNode = ShareActionButtonNode(badgeBackgroundColor: self.presentationData.theme.actionSheet.controlAccentColor, badgeTextColor: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor) + self.actionButtonNode.displaysAsynchronously = false + self.actionButtonNode.titleNode.displaysAsynchronously = false + self.actionButtonNode.setBackgroundImage(highlightedHalfRoundedBackground, for: .highlighted) + + self.actionSeparatorNode = ASDisplayNode() + self.actionSeparatorNode.isLayerBacked = true + self.actionSeparatorNode.displaysAsynchronously = false + self.actionSeparatorNode.backgroundColor = self.presentationData.theme.actionSheet.opaqueItemSeparatorColor + + super.init() + + self.backgroundColor = nil + self.isOpaque = false + + self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) + self.addSubnode(self.dimNode) + + self.wrappingScrollNode.view.delegate = self + self.addSubnode(self.wrappingScrollNode) + + self.cancelButtonNode.setTitle(self.presentationData.strings.Common_Cancel, with: Font.medium(20.0), with: self.presentationData.theme.actionSheet.standardActionTextColor, for: .normal) + + self.wrappingScrollNode.addSubnode(self.cancelButtonNode) + self.cancelButtonNode.addTarget(self, action: #selector(self.cancelButtonPressed), forControlEvents: .touchUpInside) + + self.actionButtonNode.addTarget(self, action: #selector(self.installActionButtonPressed), forControlEvents: .touchUpInside) + + self.wrappingScrollNode.addSubnode(self.contentBackgroundNode) + + self.wrappingScrollNode.addSubnode(self.contentContainerNode) + self.contentContainerNode.addSubnode(self.actionSeparatorNode) + self.contentContainerNode.addSubnode(self.actionsBackgroundNode) + self.contentContainerNode.addSubnode(self.actionButtonNode) + + self.transitionToContentNode(ShareLoadingContainerNode(theme: theme)) + + self.actionButtonNode.alpha = 0.0 + self.actionSeparatorNode.alpha = 0.0 + self.actionsBackgroundNode.alpha = 0.0 + + self.ready.set(.single(true)) + self.didSetReady = true + } + + deinit { + self.disposable.dispose() + } + + override func didLoad() { + super.didLoad() + + if #available(iOSApplicationExtension 11.0, *) { + self.wrappingScrollNode.view.contentInsetAdjustmentBehavior = .never + } + } + + func transitionToContentNode(_ contentNode: (ASDisplayNode & ShareContentContainerNode)?, fastOut: Bool = false) { + if self.contentNode !== contentNode { + let transition: ContainedViewLayoutTransition + + let previous = self.contentNode + if let previous = previous { + previous.setContentOffsetUpdated(nil) + transition = .animated(duration: 0.4, curve: .spring) + + self.previousContentNode = previous + previous.alpha = 0.0 + previous.layer.animateAlpha(from: 1.0, to: 0.0, duration: fastOut ? 0.1 : 0.2, removeOnCompletion: true, completion: { [weak self, weak previous] _ in + if let strongSelf = self, let previous = previous { + if strongSelf.previousContentNode === previous { + strongSelf.previousContentNode = nil + } + previous.removeFromSupernode() + } + }) + } else { + transition = .immediate + } + self.contentNode = contentNode + + if let (layout, navigationBarHeight, bottomGridInset) = self.containerLayout { + if let contentNode = contentNode, let previous = previous { + contentNode.frame = previous.frame + contentNode.updateLayout(size: previous.bounds.size, bottomInset: bottomGridInset, transition: .immediate) + + contentNode.setContentOffsetUpdated({ [weak self] contentOffset, transition in + self?.contentNodeOffsetUpdated(contentOffset, transition: transition) + }) + self.contentContainerNode.insertSubnode(contentNode, at: 0) + + contentNode.alpha = 1.0 + let animation = contentNode.layer.makeAnimation(from: 0.0 as NSNumber, to: 1.0 as NSNumber, keyPath: "opacity", timingFunction: kCAMediaTimingFunctionEaseInEaseOut, duration: 0.35) + animation.fillMode = kCAFillModeBoth + if !fastOut { + animation.beginTime = CACurrentMediaTime() + 0.1 + } + contentNode.layer.add(animation, forKey: "opacity") + + self.animateContentNodeOffsetFromBackgroundOffset = self.contentBackgroundNode.frame.minY + self.scheduleInteractiveTransition(transition) + + contentNode.activate() + previous.deactivate() + } else { + if let contentNode = self.contentNode { + contentNode.setContentOffsetUpdated({ [weak self] contentOffset, transition in + self?.contentNodeOffsetUpdated(contentOffset, transition: transition) + }) + self.contentContainerNode.insertSubnode(contentNode, at: 0) + } + + self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) + } + } else if let contentNode = contentNode { + contentNode.setContentOffsetUpdated({ [weak self] contentOffset, transition in + self?.contentNodeOffsetUpdated(contentOffset, transition: transition) + }) + self.contentContainerNode.insertSubnode(contentNode, at: 0) + } + } + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + var insets = layout.insets(options: [.statusBar, .input]) + let cleanInsets = layout.insets(options: [.statusBar]) + insets.top = max(10.0, insets.top) + + let bottomInset: CGFloat = 10.0 + cleanInsets.bottom + let buttonHeight: CGFloat = 57.0 + let sectionSpacing: CGFloat = 8.0 + let titleAreaHeight: CGFloat = 64.0 + + let maximumContentHeight = layout.size.height - insets.top - max(bottomInset + buttonHeight, insets.bottom) - sectionSpacing + + let width = min(layout.size.width, layout.size.height) - 20.0 + let sideInset = floor((layout.size.width - width) / 2.0) + + let contentContainerFrame = CGRect(origin: CGPoint(x: sideInset, y: insets.top), size: CGSize(width: width, height: maximumContentHeight)) + let contentFrame = contentContainerFrame.insetBy(dx: 0.0, dy: 0.0) + + var bottomGridInset = buttonHeight + + self.containerLayout = (layout, navigationBarHeight, bottomGridInset) + self.scheduledLayoutTransitionRequest = nil + + transition.updateFrame(node: self.wrappingScrollNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + + transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + + transition.updateFrame(node: self.cancelButtonNode, frame: CGRect(origin: CGPoint(x: sideInset, y: layout.size.height - bottomInset - buttonHeight), size: CGSize(width: width, height: buttonHeight))) + + transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame) + + transition.updateFrame(node: self.actionsBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentContainerFrame.size.height - bottomGridInset), size: CGSize(width: contentContainerFrame.size.width, height: bottomGridInset))) + + transition.updateFrame(node: self.actionButtonNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentContainerFrame.size.height - buttonHeight), size: CGSize(width: contentContainerFrame.size.width, height: buttonHeight))) + + transition.updateFrame(node: self.actionSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentContainerFrame.size.height - bottomGridInset - UIScreenPixel), size: CGSize(width: contentContainerFrame.size.width, height: UIScreenPixel))) + + let gridSize = CGSize(width: contentFrame.size.width, height: max(32.0, contentFrame.size.height - titleAreaHeight)) + + if let contentNode = self.contentNode { + transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(x: floor((contentContainerFrame.size.width - contentFrame.size.width) / 2.0), y: titleAreaHeight), size: gridSize)) + contentNode.updateLayout(size: gridSize, bottomInset: bottomGridInset, transition: transition) + } + } + + private func contentNodeOffsetUpdated(_ contentOffset: CGFloat, transition: ContainedViewLayoutTransition) { + if let (layout, _, _) = self.containerLayout { + var insets = layout.insets(options: [.statusBar, .input]) + insets.top = max(10.0, insets.top) + let cleanInsets = layout.insets(options: [.statusBar]) + + let bottomInset: CGFloat = 10.0 + cleanInsets.bottom + let buttonHeight: CGFloat = 57.0 + let sectionSpacing: CGFloat = 8.0 + + let width = min(layout.size.width, layout.size.height) - 20.0 + + let sideInset = floor((layout.size.width - width) / 2.0) + + let maximumContentHeight = layout.size.height - insets.top - max(bottomInset + buttonHeight, insets.bottom) - sectionSpacing + let contentFrame = CGRect(origin: CGPoint(x: sideInset, y: insets.top), size: CGSize(width: width, height: maximumContentHeight)) + + var backgroundFrame = CGRect(origin: CGPoint(x: contentFrame.minX, y: contentFrame.minY - contentOffset), size: contentFrame.size) + if backgroundFrame.minY < contentFrame.minY { + backgroundFrame.origin.y = contentFrame.minY + } + if backgroundFrame.maxY > contentFrame.maxY { + backgroundFrame.size.height += contentFrame.maxY - backgroundFrame.maxY + } + if backgroundFrame.size.height < buttonHeight + 32.0 { + backgroundFrame.origin.y -= buttonHeight + 32.0 - backgroundFrame.size.height + backgroundFrame.size.height = buttonHeight + 32.0 + } + transition.updateFrame(node: self.contentBackgroundNode, frame: backgroundFrame) + + if let animateContentNodeOffsetFromBackgroundOffset = self.animateContentNodeOffsetFromBackgroundOffset { + self.animateContentNodeOffsetFromBackgroundOffset = nil + let offset = backgroundFrame.minY - animateContentNodeOffsetFromBackgroundOffset + if let contentNode = self.contentNode { + transition.animatePositionAdditive(node: contentNode, offset: -offset) + } + if let previousContentNode = self.previousContentNode { + transition.updatePosition(node: previousContentNode, position: previousContentNode.position.offsetBy(dx: 0.0, dy: offset)) + } + } + } + } + + @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.cancelButtonPressed() + } + } + + @objc func cancelButtonPressed() { + self.cancel?() + } + + @objc func installActionButtonPressed() { + self.join?() + } + + func animateIn() { + if self.contentNode != nil { + self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + + let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY + + let dimPosition = self.dimNode.layer.position + self.dimNode.layer.animatePosition(from: CGPoint(x: dimPosition.x, y: dimPosition.y - offset), to: dimPosition, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + self.layer.animateBoundsOriginYAdditive(from: -offset, to: 0.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + } + } + + func animateOut(completion: (() -> Void)? = nil) { + if self.contentNode != nil { + var dimCompleted = false + var offsetCompleted = false + + let internalCompletion: () -> Void = { [weak self] in + if let strongSelf = self, dimCompleted && offsetCompleted { + strongSelf.dismiss?() + } + completion?() + } + + self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in + dimCompleted = true + internalCompletion() + }) + + let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY + let dimPosition = self.dimNode.layer.position + self.dimNode.layer.animatePosition(from: dimPosition, to: CGPoint(x: dimPosition.x, y: dimPosition.y - offset), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + self.layer.animateBoundsOriginYAdditive(from: 0.0, to: -offset, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in + offsetCompleted = true + internalCompletion() + }) + } else { + self.dismiss?() + completion?() + } + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if let result = self.actionButtonNode.hitTest(self.actionButtonNode.convert(point, from: self), with: event) { + return result + } + if self.bounds.contains(point) { + if !self.contentBackgroundNode.bounds.contains(self.convert(point, to: self.contentBackgroundNode)) && !self.cancelButtonNode.bounds.contains(self.convert(point, to: self.cancelButtonNode)) { + return self.dimNode.view + } + } + return super.hitTest(point, with: event) + } + + func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { + let contentOffset = scrollView.contentOffset + let additionalTopHeight = max(0.0, -contentOffset.y) + + if additionalTopHeight >= 30.0 { + self.cancelButtonPressed() + } + } + + private func scheduleInteractiveTransition(_ transition: ContainedViewLayoutTransition) { + if let scheduledLayoutTransitionRequest = self.scheduledLayoutTransitionRequest { + switch scheduledLayoutTransitionRequest.1 { + case .immediate: + self.scheduleLayoutTransitionRequest(transition) + default: + break + } + } else { + self.scheduleLayoutTransitionRequest(transition) + } + } + + private func scheduleLayoutTransitionRequest(_ transition: ContainedViewLayoutTransition) { + let requestId = self.scheduledLayoutTransitionRequestId + self.scheduledLayoutTransitionRequestId += 1 + self.scheduledLayoutTransitionRequest = (requestId, transition) + (self.view as? UITracingLayerView)?.schedule(layout: { [weak self] in + if let strongSelf = self { + if let (currentRequestId, currentRequestTransition) = strongSelf.scheduledLayoutTransitionRequest, currentRequestId == requestId { + strongSelf.scheduledLayoutTransitionRequest = nil + strongSelf.requestLayout(currentRequestTransition) + } + } + }) + self.setNeedsLayout() + } + + func transitionToProgress(signal: Signal) { + let transition = ContainedViewLayoutTransition.animated(duration: 0.12, curve: .easeInOut) + transition.updateAlpha(node: self.actionButtonNode, alpha: 0.0) + transition.updateAlpha(node: self.actionSeparatorNode, alpha: 0.0) + transition.updateAlpha(node: self.actionsBackgroundNode, alpha: 0.0) + + self.transitionToContentNode(ShareLoadingContainerNode(theme: self.presentationData.theme), fastOut: true) + let timestamp = CACurrentMediaTime() + self.disposable.set(signal.start(completed: { [weak self] in + let minDelay = 0.6 + let delay = max(0.0, (timestamp + minDelay) - CACurrentMediaTime()) + Queue.mainQueue().after(delay, { + if let strongSelf = self { + strongSelf.cancel?() + } + }) + })) + } + + func setPeer(image: TelegramMediaImageRepresentation?, title: String, memberCount: Int32, members: [Peer], data: JoinLinkPreviewData) { + let transition = ContainedViewLayoutTransition.animated(duration: 0.22, curve: .easeInOut) + transition.updateAlpha(node: self.actionButtonNode, alpha: 1.0) + transition.updateAlpha(node: self.actionSeparatorNode, alpha: 1.0) + transition.updateAlpha(node: self.actionsBackgroundNode, alpha: 1.0) + + self.actionButtonNode.isEnabled = true + if data.isJoined { + self.actionButtonNode.setTitle(self.presentationData.strings.Conversation_LinkDialogOpen, with: Font.medium(20.0), with: self.presentationData.theme.actionSheet.standardActionTextColor, for: .normal) + } else { + self.actionButtonNode.setTitle(data.isGroup ? self.presentationData.strings.Invitation_JoinGroup : self.presentationData.strings.Channel_JoinChannel, with: Font.medium(20.0), with: self.presentationData.theme.actionSheet.standardActionTextColor, for: .normal) + } + + self.transitionToContentNode(JoinLinkPreviewPeerContentNode(account: self.account, image: image, title: title, memberCount: memberCount, members: members, theme: self.presentationData.theme, strings: self.presentationData.strings)) + } +} diff --git a/TelegramUI/JoinLinkPreviewPeerContentNode.swift b/TelegramUI/JoinLinkPreviewPeerContentNode.swift new file mode 100644 index 0000000000..cf11b2b65f --- /dev/null +++ b/TelegramUI/JoinLinkPreviewPeerContentNode.swift @@ -0,0 +1,106 @@ +import Foundation +import AsyncDisplayKit +import Display +import Postbox +import TelegramCore + +private let avatarFont: UIFont = UIFont(name: "ArialRoundedMTBold", size: 26.0)! + +final class JoinLinkPreviewPeerContentNode: ASDisplayNode, ShareContentContainerNode { + private var contentOffsetUpdated: ((CGFloat, ContainedViewLayoutTransition) -> Void)? + + private let avatarNode: AvatarNode + private let titleNode: ASTextNode + private let countNode: ASTextNode + private let peersScrollNode: ASScrollNode + + private let peerNodes: [SelectablePeerNode] + + init(account: Account, image: TelegramMediaImageRepresentation?, title: String, memberCount: Int32, members: [Peer], theme: PresentationTheme, strings: PresentationStrings) { + self.avatarNode = AvatarNode(font: avatarFont) + self.titleNode = ASTextNode() + self.countNode = ASTextNode() + self.peersScrollNode = ASScrollNode() + + let itemTheme = SelectablePeerNodeTheme(textColor: theme.actionSheet.primaryTextColor, secretTextColor: .green, selectedTextColor: theme.actionSheet.controlAccentColor, checkBackgroundColor: theme.actionSheet.opaqueItemBackgroundColor, checkFillColor: theme.actionSheet.controlAccentColor, checkColor: theme.actionSheet.opaqueItemBackgroundColor) + + self.peerNodes = members.map { peer in + let node = SelectablePeerNode() + node.setup(account: account, strings: strings, peer: peer, chatPeer: nil) + node.theme = itemTheme + return node + } + + super.init() + + let peer = TelegramGroup(id: PeerId(namespace: 0, id: 0), title: title, photo: image.flatMap { [$0] } ?? [], participantCount: Int(memberCount), role: .member, membership: .Left, flags: [], migrationReference: nil, creationDate: 0, version: 0) + + self.addSubnode(self.avatarNode) + self.avatarNode.setPeer(account: account, peer: peer) + + self.addSubnode(self.titleNode) + self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(16.0), textColor: theme.actionSheet.primaryTextColor) + + self.addSubnode(self.countNode) + let membersString: String + if !members.isEmpty { + membersString = strings.Invitation_Members(memberCount) + } else { + membersString = strings.Conversation_StatusMembers(memberCount) + } + self.countNode.attributedText = NSAttributedString(string: membersString, font: Font.regular(16.0), textColor: theme.actionSheet.secondaryTextColor) + + if !self.peerNodes.isEmpty { + for peerNode in peerNodes { + self.peersScrollNode.addSubnode(peerNode) + } + self.addSubnode(self.peersScrollNode) + } + } + + func activate() { + } + + func deactivate() { + } + + func setEnsurePeerVisibleOnLayout(_ peerId: PeerId?) { + } + + func setContentOffsetUpdated(_ f: ((CGFloat, ContainedViewLayoutTransition) -> Void)?) { + self.contentOffsetUpdated = f + } + + func updateLayout(size: CGSize, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) { + let nodeHeight: CGFloat = self.peerNodes.isEmpty ? 224.0 : 324.0 + + let verticalOrigin = size.height - nodeHeight + + let avatarSize: CGFloat = 75.0 + + transition.updateFrame(node: self.avatarNode, frame: CGRect(origin: CGPoint(x: floor((size.width - avatarSize) / 2.0), y: verticalOrigin + 22.0), size: CGSize(width: avatarSize, height: avatarSize))) + + let titleSize = self.titleNode.measure(size) + transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: verticalOrigin + 22.0 + avatarSize + 15.0), size: titleSize)) + + let countSize = self.countNode.measure(size) + transition.updateFrame(node: self.countNode, frame: CGRect(origin: CGPoint(x: floor((size.width - countSize.width) / 2.0), y: verticalOrigin + 22.0 + avatarSize + 15.0 + titleSize.height + 1.0), size: countSize)) + + let peerSize = CGSize(width: 85.0, height: 95.0) + let peerInset: CGFloat = 10.0 + + var peerOffset = peerInset + for node in self.peerNodes { + node.frame = CGRect(origin: CGPoint(x: peerOffset, y: 0.0), size: peerSize) + peerOffset += peerSize.width + } + + self.peersScrollNode.view.contentSize = CGSize(width: CGFloat(self.peerNodes.count) * peerSize.width + peerInset * 2.0, height: peerSize.height) + transition.updateFrame(node: self.peersScrollNode, frame: CGRect(origin: CGPoint(x: 0.0, y: verticalOrigin + 168.0), size: CGSize(width: size.width, height: peerSize.height))) + + self.contentOffsetUpdated?(-size.height + nodeHeight - 64.0, transition) + } + + func updateSelectedPeers() { + } +} diff --git a/TelegramUI/LanguageSelectionController.swift b/TelegramUI/LanguageSelectionController.swift index 9dd969e98b..bd1a7fceed 100644 --- a/TelegramUI/LanguageSelectionController.swift +++ b/TelegramUI/LanguageSelectionController.swift @@ -157,7 +157,7 @@ private final class InnerLanguageSelectionController: UIViewController, UITableV super.init(nibName: nil, bundle: nil) self.title = self.presentationData.strings.Settings_AppLanguage - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .plain, target: self, action: #selector(self.cancelPressed)) self.definesPresentationContext = true @@ -175,7 +175,7 @@ private final class InnerLanguageSelectionController: UIViewController, UITableV } }) - self.languagesDisposable = (availableLocalizations(network: account.network) + self.languagesDisposable = (availableLocalizations(postbox: account.postbox, network: account.network, allowCached: true) |> deliverOnMainQueue).start(next: { [weak self] languages in if let strongSelf = self { strongSelf.languages = languages @@ -198,7 +198,7 @@ private final class InnerLanguageSelectionController: UIViewController, UITableV private func updateThemeAndStrings() { self.title = self.presentationData.strings.Settings_AppLanguage - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .plain, target: self, action: #selector(self.cancelPressed)) if self.isViewLoaded { self.searchController.searchBar.placeholder = self.presentationData.strings.Common_Search @@ -240,7 +240,7 @@ private final class InnerLanguageSelectionController: UIViewController, UITableV self.searchController.searchBar.barTintColor = self.presentationData.theme.chatList.backgroundColor self.searchController.searchBar.tintColor = self.presentationData.theme.rootController.navigationBar.accentTextColor self.searchController.searchBar.backgroundColor = self.presentationData.theme.chatList.backgroundColor - self.searchController.searchBar.setTextColor(self.presentationData.theme.chatList.titleColor) + self.searchController.searchBar.setTextColor(self.presentationData.theme.chatList.titleColor) let searchImage = generateImage(CGSize(width: 8.0, height: 28.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) diff --git a/TelegramUI/LegacyAttachmentMenu.swift b/TelegramUI/LegacyAttachmentMenu.swift index 72638dfabf..b03efd9a8c 100644 --- a/TelegramUI/LegacyAttachmentMenu.swift +++ b/TelegramUI/LegacyAttachmentMenu.swift @@ -6,14 +6,15 @@ import SwiftSignalKit import Postbox import TelegramCore -func legacyAttachmentMenu(account: Account, peer: Peer, theme: PresentationTheme, strings: PresentationStrings, parentController: LegacyController, recentlyUsedInlineBots: [Peer], openGallery: @escaping () -> Void, openCamera: @escaping (TGAttachmentCameraView?, TGMenuSheetController?) -> Void, openFileGallery: @escaping () -> Void, openMap: @escaping () -> Void, openContacts: @escaping () -> Void, sendMessagesWithSignals: @escaping ([Any]?) -> Void, selectRecentlyUsedInlineBot: @escaping (Peer) -> Void) -> TGMenuSheetController { +func legacyAttachmentMenu(account: Account, peer: Peer, saveEditedPhotos: Bool, allowGrouping: Bool, theme: PresentationTheme, strings: PresentationStrings, parentController: LegacyController, recentlyUsedInlineBots: [Peer], openGallery: @escaping () -> Void, openCamera: @escaping (TGAttachmentCameraView?, TGMenuSheetController?) -> Void, openFileGallery: @escaping () -> Void, openMap: @escaping () -> Void, openContacts: @escaping () -> Void, sendMessagesWithSignals: @escaping ([Any]?) -> Void, selectRecentlyUsedInlineBot: @escaping (Peer) -> Void) -> TGMenuSheetController { let controller = TGMenuSheetController(context: parentController.context, dark: false)! controller.dismissesByOutsideTap = true controller.hasSwipeGesture = true - controller.maxHeight = 445.0 - TGMenuSheetButtonItemViewHeight + controller.maxHeight = 445.0// - TGMenuSheetButtonItemViewHeight var itemViews: [Any] = [] - let carouselItem = TGAttachmentCarouselItemView(context: parentController.context, camera: PGCamera.cameraAvailable(), selfPortrait: false, forProfilePhoto: false, assetType: TGMediaAssetAnyType, saveEditedPhotos: false)! + let carouselItem = TGAttachmentCarouselItemView(context: parentController.context, camera: PGCamera.cameraAvailable(), selfPortrait: false, forProfilePhoto: false, assetType: TGMediaAssetAnyType, saveEditedPhotos: saveEditedPhotos, allowGrouping: allowGrouping)! + //carouselItem.defaultStatusBarStyle = theme.rootController.statusBar.style.style.systemStyle carouselItem.suggestionContext = legacySuggestionContext(account: account, peerId: peer.id) carouselItem.recipientName = peer.displayTitle carouselItem.cameraPressed = { [weak controller] cameraView in @@ -28,7 +29,7 @@ func legacyAttachmentMenu(account: Account, peer: Peer, theme: PresentationTheme if let controller = controller, let carouselItem = carouselItem { controller.dismiss(animated: true) let intent: TGMediaAssetsControllerIntent = asFiles ? TGMediaAssetsControllerSendFileIntent : TGMediaAssetsControllerSendMediaIntent - let signals = TGMediaAssetsController.resultSignals(for: carouselItem.selectionContext, editingContext: carouselItem.editingContext, intent: intent, currentItem: currentItem, storeAssets: true, useMediaCache: false, descriptionGenerator: legacyAssetPickerItemGenerator(), saveEditedPhotos: true) + let signals = TGMediaAssetsController.resultSignals(for: carouselItem.selectionContext, editingContext: carouselItem.editingContext, intent: intent, currentItem: currentItem, storeAssets: true, useMediaCache: false, descriptionGenerator: legacyAssetPickerItemGenerator(), saveEditedPhotos: saveEditedPhotos) sendMessagesWithSignals(signals) } }; @@ -86,3 +87,30 @@ func legacyAttachmentMenu(account: Account, peer: Peer, theme: PresentationTheme return controller } + +func legacyPasteMenu(account: Account, peer: Peer, saveEditedPhotos: Bool, allowGrouping: Bool, theme: PresentationTheme, strings: PresentationStrings, images: [UIImage], sendMessagesWithSignals: @escaping ([Any]?) -> Void) -> ViewController { + + let legacyController = LegacyController(presentation: .custom, theme: theme) + legacyController.statusBar.statusBarStyle = .Hide + let baseController = TGViewController(context: legacyController.context)! + legacyController.bind(controller: baseController) + var hasTimer = false + if peer is TelegramUser || peer is TelegramSecretChat { + hasTimer = true + } + let recipientName = peer.displayTitle + + legacyController.presentationCompleted = { [weak legacyController, weak baseController] in + if let strongLegacyController = legacyController, let baseController = baseController { + TGClipboardMenu.present(inParentController: baseController, context: strongLegacyController.context, images: images, hasCaption: true, hasTimer: hasTimer, recipientName: recipientName, completed: { selectionContext, editingContext, currentItem in + let signals = TGClipboardMenu.resultSignals(for: selectionContext, editingContext: editingContext, currentItem: currentItem, descriptionGenerator: legacyAssetPickerItemGenerator()) + sendMessagesWithSignals(signals) + }, dismissed: { + if let strongLegacyController = legacyController { + strongLegacyController.dismiss() + } + }, sourceView: baseController.view, sourceRect: nil) + } + } + return legacyController +} diff --git a/TelegramUI/LegacyCamera.swift b/TelegramUI/LegacyCamera.swift index 1880630946..e18bb50863 100644 --- a/TelegramUI/LegacyCamera.swift +++ b/TelegramUI/LegacyCamera.swift @@ -6,7 +6,8 @@ import TelegramCore import Postbox func presentedLegacyCamera(account: Account, peer: Peer, cameraView: TGAttachmentCameraView?, menuController: TGMenuSheetController?, parentController: ViewController, sendMessagesWithSignals: @escaping ([Any]?) -> Void) { - let legacyController = LegacyController(presentation: .custom) + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme) legacyController.supportedOrientations = .portrait legacyController.statusBar.statusBarStyle = .Hide diff --git a/TelegramUI/LegacyController.swift b/TelegramUI/LegacyController.swift index 7fdb3e0b7a..8b6fa730d2 100644 --- a/TelegramUI/LegacyController.swift +++ b/TelegramUI/LegacyController.swift @@ -26,9 +26,9 @@ private final class LegacyComponentsOverlayWindowManagerImpl: NSObject, LegacyCo private var controller: LegacyController? private var boundController = false - init(parentController: ViewController?) { + init(parentController: ViewController?, theme: PresentationTheme?) { self.parentController = parentController - self.controller = LegacyController(presentation: .custom) + self.controller = LegacyController(presentation: .custom, theme: theme) super.init() @@ -46,6 +46,18 @@ private final class LegacyComponentsOverlayWindowManagerImpl: NSObject, LegacyCo func bindController(_ controller: UIViewController!) { self.contentController = controller + controller.state_setNeedsStatusBarAppearanceUpdate({ [weak self, weak controller] in + if let parentController = self?.parentController, let controller = controller { + if parentController.statusBar.statusBarStyle != .Hide { + self?.controller?.statusBar.statusBarStyle = StatusBarStyle(systemStyle: controller.preferredStatusBarStyle) + } + } + }) + if let parentController = self.parentController { + if parentController.statusBar.statusBarStyle != .Hide { + self.controller?.statusBar.statusBarStyle = StatusBarStyle(systemStyle: controller.preferredStatusBarStyle) + } + } } func context() -> LegacyComponentsContext! { @@ -68,9 +80,11 @@ private final class LegacyComponentsOverlayWindowManagerImpl: NSObject, LegacyCo final class LegacyControllerContext: NSObject, LegacyComponentsContext { private weak var controller: ViewController? + private let theme: PresentationTheme? - init(controller: ViewController?) { + init(controller: ViewController?, theme: PresentationTheme?) { self.controller = controller + self.theme = theme super.init() } @@ -177,11 +191,16 @@ final class LegacyControllerContext: NSObject, LegacyComponentsContext { return nil } - public func presentActionSheet(_ actions: [LegacyComponentsActionSheetAction]!, view: UIView!, completion: ((LegacyComponentsActionSheetAction?) -> Swift.Void)!) { + public func presentActionSheet(_ actions: [LegacyComponentsActionSheetAction]!, view: UIView!, completion: ((LegacyComponentsActionSheetAction?) -> Void)!) { + + } + + public func presentActionSheet(_ actions: [LegacyComponentsActionSheetAction]!, view: UIView!, sourceRect: (() -> CGRect)!, completion: ((LegacyComponentsActionSheetAction?) -> Void)!) { + } func makeOverlayWindowManager() -> LegacyComponentsOverlayWindowManager! { - return LegacyComponentsOverlayWindowManagerImpl(parentController: self.controller) + return LegacyComponentsOverlayWindowManagerImpl(parentController: self.controller, theme: self.theme) } func applicationStatusBarAlpha() -> CGFloat { @@ -207,10 +226,32 @@ final class LegacyControllerContext: NSObject, LegacyComponentsContext { func animateApplicationStatusBarStyleTransition(withDuration duration: TimeInterval) { } + + func safeAreaInset() -> UIEdgeInsets { + if let controller = self.controller as? LegacyController, let validLayout = controller.validLayout { + return validLayout.safeInsets + } + return UIEdgeInsets() + } + + func prefersLightStatusBar() -> Bool { + if let controller = self.controller { + switch controller.statusBar.statusBarStyle { + case .Black: + return false + case .White: + return true + default: + return false + } + } else { + return false + } + } } public class LegacyController: ViewController { - private var legacyController: UIViewController! + public private(set) var legacyController: UIViewController! private let presentation: LegacyControllerPresentation private var controllerNode: LegacyControllerNode { @@ -222,15 +263,21 @@ public class LegacyController: ViewController { return self.contextImpl! } + fileprivate var validLayout: ContainerViewLayout? + var controllerLoaded: (() -> Void)? public var presentationCompleted: (() -> Void)? - public init(presentation: LegacyControllerPresentation) { + public init(presentation: LegacyControllerPresentation, theme: PresentationTheme?) { self.presentation = presentation super.init(navigationBarTheme: nil) - let contextImpl = LegacyControllerContext(controller: self) + if let theme = theme { + self.statusBar.statusBarStyle = theme.rootController.statusBar.style.style + } + + let contextImpl = LegacyControllerContext(controller: self, theme: theme) self.contextImpl = contextImpl } @@ -255,9 +302,19 @@ public class LegacyController: ViewController { override public func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) + if self.ignoreAppearanceMethodInvocations() { + return + } + if self.controllerNode.controllerView == nil { self.controllerNode.controllerView = self.legacyController.view + if let legacyController = self.legacyController as? TGViewController { + legacyController.ignoreAppearEvents = true + } self.controllerNode.view.insertSubview(self.legacyController.view, at: 0) + if let legacyController = self.legacyController as? TGViewController { + legacyController.ignoreAppearEvents = false + } if let controllerLoaded = self.controllerLoaded { controllerLoaded() @@ -270,12 +327,20 @@ public class LegacyController: ViewController { override public func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) + if self.ignoreAppearanceMethodInvocations() { + return + } + self.legacyController.viewWillDisappear(animated && passControllerAppearanceAnimated(in: false, presentation: self.presentation)) } override public func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) + if self.ignoreAppearanceMethodInvocations() { + return + } + switch self.presentation { case let .modal(animateIn): if animateIn { @@ -295,10 +360,16 @@ public class LegacyController: ViewController { override public func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) + if self.ignoreAppearanceMethodInvocations() { + return + } + self.legacyController.viewDidDisappear(animated && passControllerAppearanceAnimated(in: false, presentation: self.presentation)) } override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + self.validLayout = layout + super.containerLayoutUpdated(layout, transition: transition) self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) diff --git a/TelegramUI/LegacyHTTPOperationImpl.swift b/TelegramUI/LegacyHTTPOperationImpl.swift new file mode 100644 index 0000000000..0348786085 --- /dev/null +++ b/TelegramUI/LegacyHTTPOperationImpl.swift @@ -0,0 +1,7 @@ +import Foundation +import MtProtoKitDynamic + +import LegacyComponents + +@objc class LegacyHTTPOperationImpl: AFHTTPRequestOperation, LegacyHTTPRequestOperation { +} diff --git a/TelegramUI/LegacyInstantVideoController.swift b/TelegramUI/LegacyInstantVideoController.swift index 2aa72bbf0a..271da23426 100644 --- a/TelegramUI/LegacyInstantVideoController.swift +++ b/TelegramUI/LegacyInstantVideoController.swift @@ -18,16 +18,17 @@ final class InstantVideoController: LegacyController { private var captureController: TGVideoMessageCaptureController? var onDismiss: (() -> Void)? + var onStop: (() -> Void)? private let micLevelValue = ValuePromise(0.0) let audioStatus: InstantVideoControllerRecordingStatus private var dismissedVideo = false - override init(presentation: LegacyControllerPresentation) { + override init(presentation: LegacyControllerPresentation, theme: PresentationTheme?) { self.audioStatus = InstantVideoControllerRecordingStatus(micLevel: self.micLevelValue.get()) - super.init(presentation: presentation) + super.init(presentation: presentation, theme: theme) } required public init(coder aDecoder: NSCoder) { @@ -41,9 +42,10 @@ final class InstantVideoController: LegacyController { self?.micLevelValue.set(Float(level)) } captureController.onDismiss = { [weak self] _ in - if let strongSelf = self { - strongSelf.onDismiss?() - } + self?.onDismiss?() + } + captureController.onStop = { [weak self] in + self?.onStop?() } } } @@ -82,28 +84,20 @@ final class InstantVideoController: LegacyController { } } -func legacyInstantVideoController(theme: PresentationTheme, panelFrame: CGRect, account: Account, peerId: PeerId) -> InstantVideoController { - let legacyController = InstantVideoController(presentation: .custom) +func legacyInstantVideoController(theme: PresentationTheme, panelFrame: CGRect, account: Account, peerId: PeerId, send: @escaping (EnqueueMessage) -> Void) -> InstantVideoController { + let legacyController = InstantVideoController(presentation: .custom, theme: theme) legacyController.statusBar.statusBarStyle = .Hide let baseController = TGViewController(context: legacyController.context)! legacyController.bind(controller: baseController) legacyController.presentationCompleted = { [weak legacyController, weak baseController] in if let legacyController = legacyController, let baseController = baseController { - let controllerTheme = TGVideoMessageCaptureControllerTheme(darkBackground: theme.rootController.statusBar.style.style == .White, panelSeparatorColor: theme.chat.inputPanel.panelStrokeColor, panelTime: theme.chat.inputPanel.primaryTextColor, panelDotColor: theme.chat.inputPanel.mediaRecordingDotColor, panelAccentColor: theme.chat.inputPanel.panelControlAccentColor) - let controller = TGVideoMessageCaptureController(context: legacyController.context, assets: TGVideoMessageCaptureControllerAssets(send: PresentationResourcesChat.chatInputPanelSendButtonImage(theme)!, slideToCancel:PresentationResourcesChat.chatInputPanelMediaRecordingCancelArrowImage(theme)!, actionDelete: generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Acessory Panels/MessageSelectionThrash"), color: theme.chat.inputPanel.panelControlAccentColor), theme: controllerTheme)!, transitionInView: { + //let controllerTheme = TGVideoMessageCaptureControllerTheme(darkBackground: theme.rootController.statusBar.style.style == .White, panelSeparatorColor: theme.chat.inputPanel.panelStrokeColor, panelBackgroundColor: theme.chat.inputPanel.panelBackgroundColor, panelTime: theme.chat.inputPanel.primaryTextColor, panelDotColor: theme.chat.inputPanel.mediaRecordingDotColor, panelAccentColor: theme.chat.inputPanel.panelControlAccentColor) + let inputPanelTheme = theme.chat.inputPanel + let controller = TGVideoMessageCaptureController(context: legacyController.context, assets: TGVideoMessageCaptureControllerAssets(send: PresentationResourcesChat.chatInputPanelSendButtonImage(theme)!, slideToCancel:PresentationResourcesChat.chatInputPanelMediaRecordingCancelArrowImage(theme)!, actionDelete: generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Acessory Panels/MessageSelectionThrash"), color: theme.chat.inputPanel.panelControlAccentColor))!, transitionInView: { return nil }, parentController: baseController, controlsFrame: panelFrame, isAlreadyLocked: { return false - }, liveUploadInterface: nil)! - /*controller.finishedWithVideo = ^(NSURL *videoURL, UIImage *previewImage, __unused NSUInteger fileSize, NSTimeInterval duration, CGSize dimensions, TGLiveUploadActorData *liveUploadData, TGVideoEditAdjustments *adjustments) - { - __strong TGModernConversationController *strongSelf = weakSelf; - if (strongSelf != nil) - { - NSDictionary *desc = [strongSelf->_companion videoDescriptionFromVideoURL:videoURL previewImage:previewImage dimensions:dimensions duration:duration adjustments:adjustments stickers:nil caption:nil roundMessage:true liveUploadData:liveUploadData timer:0]; - [strongSelf->_companion controllerWantsToSendImagesWithDescriptions:@[ desc ] asReplyToMessageId:[strongSelf currentReplyMessageId] botReplyMarkup:nil]; - } - }*/ + }, liveUploadInterface: nil, pallete: TGModernConversationInputMicPallete(dark: theme.overallDarkAppearance, buttonColor: inputPanelTheme.actionControlFillColor, iconColor: inputPanelTheme.actionControlForegroundColor, backgroundColor: inputPanelTheme.panelBackgroundColor, borderColor: inputPanelTheme.panelStrokeColor, lock: inputPanelTheme.panelControlAccentColor, textColor: inputPanelTheme.primaryTextColor, secondaryTextColor: inputPanelTheme.secondaryTextColor, recording: inputPanelTheme.mediaRecordingDotColor))! controller.finishedWithVideo = { videoUrl, previewImage, _, duration, dimensions, liveUploadData, adjustments in guard let videoUrl = videoUrl else { return @@ -136,14 +130,15 @@ func legacyInstantVideoController(theme: PresentationTheme, panelFrame: CGRect, resourceAdjustments = VideoMediaResourceAdjustments(data: adjustmentsData, digest: digest) } + if finalDuration.isZero || finalDuration.isNaN { + return + } + let resource = LocalFileVideoMediaResource(randomId: arc4random64(), path: videoUrl.path, adjustments: resourceAdjustments) let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: arc4random64()), resource: resource, previewRepresentations: previewRepresentations, mimeType: "video/mp4", size: nil, attributes: [.FileName(fileName: "video.mp4"), .Video(duration: Int(finalDuration), size: finalDimensions, flags: [.instantRoundVideo])]) - var attributes: [MessageAttribute] = [] - /*if let timer = item.timer, timer > 0 && timer <= 60 { - attributes.append(AutoremoveTimeoutMessageAttribute(timeout: Int32(timer), countdownBeginTime: nil)) - }*/ - let _ = enqueueMessages(account: account, peerId: peerId, messages: [.message(text: "", attributes: attributes, media: media, replyToMessageId: nil)]).start() + let attributes: [MessageAttribute] = [] + send(.message(text: "", attributes: attributes, media: media, replyToMessageId: nil, localGroupingKey: nil)) } controller.didDismiss = { [weak legacyController] in if let legacyController = legacyController { diff --git a/TelegramUI/LegacyLocationController.swift b/TelegramUI/LegacyLocationController.swift index 33edc0cd4c..457a86dd77 100644 --- a/TelegramUI/LegacyLocationController.swift +++ b/TelegramUI/LegacyLocationController.swift @@ -4,20 +4,109 @@ import LegacyComponents import TelegramCore import Postbox -func legacyLocationController(message: Message, mapMedia: TelegramMediaMap, account: Account, openPeer: @escaping (Peer) -> Void) -> ViewController { - var legacyPeer: AnyObject? - if let user = message.author as? TelegramUser { +private func generateClearIcon(color: UIColor) -> UIImage? { + return generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: color) +} + +private func makeLegacyPeer(_ peer: Peer) -> AnyObject? { + if let user = peer as? TelegramUser { let legacyUser = TGUser() legacyUser.uid = user.id.id legacyUser.firstName = user.firstName legacyUser.lastName = user.lastName - legacyPeer = legacyUser - } else if let channel = message.author as? TelegramChannel { + if let representation = smallestImageRepresentation(user.photo) { + legacyUser.photoUrlSmall = legacyImageLocationUri(resource: representation.resource) + } + return legacyUser + } else if let channel = peer as? TelegramChannel { let legacyConversation = TGConversation() legacyConversation.conversationId = Int64(channel.id.id) legacyConversation.chatTitle = channel.title - legacyPeer = legacyConversation + if let representation = smallestImageRepresentation(channel.photo) { + legacyConversation.chatPhotoSmall = legacyImageLocationUri(resource: representation.resource) + } + return legacyConversation + } else { + return nil } +} + +private func makeLegacyMessage(_ message: Message) -> TGMessage { + let result = TGMessage() + result.mid = message.id.id + result.date = Double(message.timestamp) + if message.flags.contains(.Failed) { + result.deliveryState = TGMessageDeliveryStateFailed + } else if message.flags.contains(.Sending) { + result.deliveryState = TGMessageDeliveryStatePending + } else { + result.deliveryState = TGMessageDeliveryStateDelivered + } + + for attribute in message.attributes { + if let attribute = attribute as? EditedMessageAttribute { + result.editDate = Double(attribute.date) + } + } + + var media: [Any] = [] + for m in message.media { + if let mapMedia = m as? TelegramMediaMap { + let legacyLocation = TGLocationMediaAttachment() + legacyLocation.latitude = mapMedia.latitude + legacyLocation.longitude = mapMedia.longitude + if let venue = mapMedia.venue { + legacyLocation.venue = TGVenueAttachment(title: venue.title, address: venue.address, provider: venue.provider, venueId: venue.id, type: venue.type) + } + if let liveBroadcastingTimeout = mapMedia.liveBroadcastingTimeout { + legacyLocation.period = liveBroadcastingTimeout + } + + media.append(legacyLocation) + } + } + if !media.isEmpty { + result.mediaAttachments = media + } + + return result +} + +private func legacyRemainingTime(message: TGMessage) -> SSignal { + var liveBroadcastingTimeoutValue: Int32? + if let mediaAttachments = message.mediaAttachments { + for media in mediaAttachments { + if let m = media as? TGLocationMediaAttachment, m.period != 0 { + liveBroadcastingTimeoutValue = m.period + } + } + } + guard let liveBroadcastingTimeout = liveBroadcastingTimeoutValue else { + return SSignal.fail(nil) + } + + if message.deliveryState != TGMessageDeliveryStateDelivered { + return SSignal.single(liveBroadcastingTimeout as NSNumber) + } + + let remainingTime = SSignal.`defer`({ + let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + let remainingTime = max(0, Int32(message.date) + liveBroadcastingTimeout - currentTime) + var signal = SSignal.single(remainingTime as NSNumber) + if remainingTime == 0 { + signal = signal?.then(SSignal.fail(nil)) + } + return signal + })! + + return (remainingTime.then(SSignal.complete().delay(5.0, on: SQueue.main()))).restart().`catch`({ _ in + return SSignal.complete() + }) +} + +func legacyLocationController(message: Message, mapMedia: TelegramMediaMap, account: Account, openPeer: @escaping (Peer) -> Void) -> ViewController { + let legacyAuthor: AnyObject? = message.author.flatMap(makeLegacyPeer) + let legacyLocation = TGLocationMediaAttachment() legacyLocation.latitude = mapMedia.latitude legacyLocation.longitude = mapMedia.longitude @@ -25,8 +114,70 @@ func legacyLocationController(message: Message, mapMedia: TelegramMediaMap, acco legacyLocation.venue = TGVenueAttachment(title: venue.title, address: venue.address, provider: venue.provider, venueId: venue.id, type: venue.type) } - let legacyController = LegacyController(presentation: .modal(animateIn: true)) - let controller = TGLocationViewController(context: legacyController.context, locationAttachment: legacyLocation, peer: legacyPeer)! + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + + let legacyController = LegacyController(presentation: .modal(animateIn: true), theme: presentationData.theme) + + let legacyMessage = makeLegacyMessage(message) + + let controller: TGLocationViewController + if let liveBroadcastingTimeout = mapMedia.liveBroadcastingTimeout { + let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + let remainingTime = max(0, message.timestamp + liveBroadcastingTimeout - currentTime) + + let messageLiveLocation = TGLiveLocation(message: legacyMessage, peer: legacyAuthor, hasOwnSession: false, isOwnLocation: false, isExpired: remainingTime == 0)! + + controller = TGLocationViewController(context: legacyController.context, liveLocation: messageLiveLocation) + controller.remainingTimeForMessage = { message in + return legacyRemainingTime(message: message!) + } + if remainingTime == 0 { + let freezeLocations: [Any] = [messageLiveLocation] + controller.setLiveLocationsSignal(.single(freezeLocations)) + } else { + let updatedLocations = SSignal(generator: { subscriber in + let disposable = topPeerActiveLiveLocationMessages(account: account, peerId: message.id.peerId).start(next: { messages in + var result: [Any] = [] + let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + loop: for message in messages { + let legacyMessage = makeLegacyMessage(message) + guard let legacyAuthor = message.author.flatMap(makeLegacyPeer) else { + continue loop + } + let remainingTime = max(0, message.timestamp + liveBroadcastingTimeout - currentTime) + if legacyMessage.locationAttachment?.period != 0 { + let liveLocation = TGLiveLocation(message: legacyMessage, peer: legacyAuthor, hasOwnSession: false, isOwnLocation: false, isExpired: remainingTime == 0)! + result.append(liveLocation) + } + } + subscriber?.putNext(result) + }) + + return SBlockDisposable(block: { + disposable.dispose() + }) + })! + controller.setLiveLocationsSignal(updatedLocations) + } + } else { + controller = TGLocationViewController(context: legacyController.context, message: legacyMessage, peer: legacyAuthor)! + controller.receivingPeer = message.peers[message.id.peerId].flatMap(makeLegacyPeer) + } + let namespacesWithEnabledLiveLocation: Set = Set([ + Namespaces.Peer.CloudChannel, + Namespaces.Peer.CloudGroup, + Namespaces.Peer.CloudUser + ]) + if namespacesWithEnabledLiveLocation.contains(message.id.peerId.namespace) { + //controller.allowLiveLocationSharing = true + } + + let theme = (account.telegramApplicationContext.currentPresentationData.with { $0 }).theme + + let listTheme = theme.list + let searchTheme = theme.rootController.activeNavigationSearchBar + controller.pallete = TGLocationPallete(backgroundColor: listTheme.plainBackgroundColor, selectionColor: listTheme.itemHighlightedBackgroundColor, separatorColor: listTheme.itemPlainSeparatorColor, textColor: listTheme.itemPrimaryTextColor, secondaryTextColor: listTheme.itemSecondaryTextColor, accentColor: listTheme.itemAccentColor, destructiveColor: listTheme.itemDestructiveColor, locationColor: UIColor(rgb: 0x008df2), liveLocationColor: UIColor(rgb: 0xff6464), iconColor: listTheme.controlSecondaryColor, sectionHeaderBackgroundColor: theme.chatList.sectionHeaderFillColor, sectionHeaderTextColor: theme.chatList.sectionHeaderTextColor, searchBarPallete: TGSearchBarPallete(dark: theme.overallDarkAppearance, backgroundColor: searchTheme.backgroundColor, highContrastBackgroundColor: searchTheme.backgroundColor, textColor: searchTheme.inputTextColor, placeholderColor: searchTheme.inputPlaceholderTextColor, clearIcon: generateClearIcon(color: theme.rootController.activeNavigationSearchBar.inputClearButtonColor), barBackgroundColor: searchTheme.backgroundColor, barSeparatorColor: searchTheme.separatorColor, plainBackgroundColor: searchTheme.backgroundColor, accentColor: searchTheme.accentColor, accentContrastColor: searchTheme.accentColor, menuBackgroundColor: searchTheme.backgroundColor, segmentedControlBackgroundImage: nil, segmentedControlSelectedImage: nil, segmentedControlHighlightedImage: nil, segmentedControlDividerImage: nil), avatarPlaceholder: nil) + controller.modalMode = true let navigationController = TGNavigationController(controllers: [controller])! legacyController.bind(controller: navigationController) @@ -44,17 +195,17 @@ func legacyLocationController(message: Message, mapMedia: TelegramMediaMap, acco shareController?.dismiss() for peerId in peerIds { - let _ = enqueueMessages(account: account, peerId: peerId, messages: [.forward(source: message.id)]).start() + let _ = enqueueMessages(account: account, peerId: peerId, messages: , grouping: .auto)]).start() } } } }*/ - controller.calloutPressed = { [weak legacyController] in + /*controller.calloutPressed = { [weak legacyController] in legacyController?.dismiss() if let author = message.author { openPeer(author) } - } + }*/ return legacyController } diff --git a/TelegramUI/LegacyLocationPicker.swift b/TelegramUI/LegacyLocationPicker.swift index 9170c76979..6d8f60b368 100644 --- a/TelegramUI/LegacyLocationPicker.swift +++ b/TelegramUI/LegacyLocationPicker.swift @@ -3,9 +3,16 @@ import Display import LegacyComponents import TelegramCore -func legacyLocationPickerController(sendLocation: @escaping (CLLocationCoordinate2D, MapVenue?) -> Void) -> ViewController { - let legacyController = LegacyController(presentation: .modal(animateIn: true)) +private func generateClearIcon(color: UIColor) -> UIImage? { + return generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: color) +} + +func legacyLocationPickerController(sendLocation: @escaping (CLLocationCoordinate2D, MapVenue?) -> Void, theme: PresentationTheme) -> ViewController { + let legacyController = LegacyController(presentation: .modal(animateIn: true), theme: theme) let controller = TGLocationPickerController(context: legacyController.context, intent: TGLocationPickerControllerDefaultIntent)! + let listTheme = theme.list + let searchTheme = theme.rootController.activeNavigationSearchBar + controller.pallete = TGLocationPallete(backgroundColor: listTheme.plainBackgroundColor, selectionColor: listTheme.itemHighlightedBackgroundColor, separatorColor: listTheme.itemPlainSeparatorColor, textColor: listTheme.itemPrimaryTextColor, secondaryTextColor: listTheme.itemSecondaryTextColor, accentColor: listTheme.itemAccentColor, destructiveColor: listTheme.itemDestructiveColor, locationColor: UIColor(rgb: 0x008df2), liveLocationColor: UIColor(rgb: 0xff6464), iconColor: listTheme.controlSecondaryColor, sectionHeaderBackgroundColor: theme.chatList.sectionHeaderFillColor, sectionHeaderTextColor: theme.chatList.sectionHeaderTextColor, searchBarPallete: TGSearchBarPallete(dark: theme.overallDarkAppearance, backgroundColor: searchTheme.backgroundColor, highContrastBackgroundColor: searchTheme.backgroundColor, textColor: searchTheme.inputTextColor, placeholderColor: searchTheme.inputPlaceholderTextColor, clearIcon: generateClearIcon(color: theme.rootController.activeNavigationSearchBar.inputClearButtonColor), barBackgroundColor: searchTheme.backgroundColor, barSeparatorColor: searchTheme.separatorColor, plainBackgroundColor: searchTheme.backgroundColor, accentColor: searchTheme.accentColor, accentContrastColor: searchTheme.accentColor, menuBackgroundColor: searchTheme.backgroundColor, segmentedControlBackgroundImage: nil, segmentedControlSelectedImage: nil, segmentedControlHighlightedImage: nil, segmentedControlDividerImage: nil), avatarPlaceholder: nil) let navigationController = TGNavigationController(controllers: [controller])! controller.navigation_setDismiss({ [weak legacyController] in legacyController?.dismiss() diff --git a/TelegramUI/LegacyMediaPickers.swift b/TelegramUI/LegacyMediaPickers.swift index 0257d8c688..5cada9e4b4 100644 --- a/TelegramUI/LegacyMediaPickers.swift +++ b/TelegramUI/LegacyMediaPickers.swift @@ -22,7 +22,7 @@ func configureLegacyAssetPicker(_ controller: TGMediaAssetsController, account: controller.shouldShowFileTipIfNeeded = showFileTooltip } -func legacyAssetPicker(fileMode: Bool, peer: Peer) -> Signal<(LegacyComponentsContext) -> TGMediaAssetsController, NoError> { +func legacyAssetPicker(theme: PresentationTheme, fileMode: Bool, peer: Peer, saveEditedPhotos: Bool, allowGrouping: Bool) -> Signal<(LegacyComponentsContext) -> TGMediaAssetsController, NoError> { return Signal { subscriber in let intent = fileMode ? TGMediaAssetsControllerSendFileIntent : TGMediaAssetsControllerSendMediaIntent @@ -33,7 +33,7 @@ func legacyAssetPicker(fileMode: Bool, peer: Peer) -> Signal<(LegacyComponentsCo } else { Queue.mainQueue().async { subscriber.putNext({ context in - let controller = TGMediaAssetsController(context: context, assetGroup: group, intent: intent, recipientName: peer.displayTitle, saveEditedPhotos: true) + let controller = TGMediaAssetsController(context: context, assetGroup: group, intent: intent, recipientName: peer.displayTitle, saveEditedPhotos: saveEditedPhotos, allowGrouping: allowGrouping) return controller! }) subscriber.putCompletion() @@ -42,7 +42,7 @@ func legacyAssetPicker(fileMode: Bool, peer: Peer) -> Signal<(LegacyComponentsCo }) } else { subscriber.putNext({ context in - let controller = TGMediaAssetsController(context: context, assetGroup: nil, intent: intent, recipientName: peer.displayTitle, saveEditedPhotos: true) + let controller = TGMediaAssetsController(context: context, assetGroup: nil, intent: intent, recipientName: peer.displayTitle, saveEditedPhotos: saveEditedPhotos, allowGrouping: allowGrouping) return controller! }) subscriber.putCompletion() @@ -74,10 +74,12 @@ private enum LegacyAssetItem { private final class LegacyAssetItemWrapper: NSObject { let item: LegacyAssetItem let timer: Int? + let groupedId: Int64? - init(item: LegacyAssetItem, timer: Int?) { + init(item: LegacyAssetItem, timer: Int?, groupedId: Int64?) { self.item = item self.timer = timer + self.groupedId = groupedId super.init() } @@ -89,7 +91,7 @@ func legacyAssetPickerItemGenerator() -> ((Any?, String?, String?) -> [AnyHashab if (dict["type"] as! NSString) == "editedPhoto" || (dict["type"] as! NSString) == "capturedPhoto" { let image = dict["image"] as! UIImage var result: [AnyHashable : Any] = [:] - result["item" as NSString] = LegacyAssetItemWrapper(item: .image(data: .image(image), caption: caption), timer: (dict["timer"] as? NSNumber)?.intValue) + result["item" as NSString] = LegacyAssetItemWrapper(item: .image(data: .image(image), caption: caption), timer: (dict["timer"] as? NSNumber)?.intValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value) return result } else if (dict["type"] as! NSString) == "cloudPhoto" { let asset = dict["asset"] as! TGMediaAsset @@ -102,7 +104,7 @@ func legacyAssetPickerItemGenerator() -> ((Any?, String?, String?) -> [AnyHashab //result["item" as NSString] = LegacyAssetItemWrapper(item: .file(.asset(asset.backingAsset))) return nil } else { - result["item" as NSString] = LegacyAssetItemWrapper(item: .image(data: .asset(asset.backingAsset), caption: caption), timer: (dict["timer"] as? NSNumber)?.intValue) + result["item" as NSString] = LegacyAssetItemWrapper(item: .image(data: .asset(asset.backingAsset), caption: caption), timer: (dict["timer"] as? NSNumber)?.intValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value) } return result } else if (dict["type"] as! NSString) == "file" { @@ -117,19 +119,19 @@ func legacyAssetPickerItemGenerator() -> ((Any?, String?, String?) -> [AnyHashab } var result: [AnyHashable: Any] = [:] - result["item" as NSString] = LegacyAssetItemWrapper(item: .file(data: .tempFile(tempFileUrl.path), mimeType: mimeType, name: name, caption: caption), timer: (dict["timer"] as? NSNumber)?.intValue) + result["item" as NSString] = LegacyAssetItemWrapper(item: .file(data: .tempFile(tempFileUrl.path), mimeType: mimeType, name: name, caption: caption), timer: (dict["timer"] as? NSNumber)?.intValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value) return result } } else if (dict["type"] as! NSString) == "video" { if let asset = dict["asset"] as? TGMediaAsset { var result: [AnyHashable: Any] = [:] - result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .asset(asset), previewImage: dict["previewImage"] as? UIImage, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption), timer: (dict["timer"] as? NSNumber)?.intValue) + result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .asset(asset), previewImage: dict["previewImage"] as? UIImage, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption), timer: (dict["timer"] as? NSNumber)?.intValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value) return result } else if let url = dict["url"] as? String { let dimensions = (dict["dimensions"]! as AnyObject).cgSizeValue! let duration = (dict["duration"]! as AnyObject).doubleValue! var result: [AnyHashable: Any] = [:] - result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .tempFile(path: url, dimensions: dimensions, duration: duration), previewImage: dict["previewImage"] as? UIImage, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption), timer: (dict["timer"] as? NSNumber)?.intValue) + result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .tempFile(path: url, dimensions: dimensions, duration: duration), previewImage: dict["previewImage"] as? UIImage, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption), timer: (dict["timer"] as? NSNumber)?.intValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value) return result } } @@ -158,12 +160,12 @@ func legacyAssetPickerEnqueueMessages(account: Account, peerId: PeerId, signals: if let scaledImageData = UIImageJPEGRepresentation(scaledImage, 0.52) { let _ = try? scaledImageData.write(to: URL(fileURLWithPath: tempFilePath)) let resource = LocalFileReferenceMediaResource(localFilePath: tempFilePath, randomId: randomId) - let media = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: randomId), representations: [TelegramMediaImageRepresentation(dimensions: scaledSize, resource: resource)]) + let media = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: randomId), representations: [TelegramMediaImageRepresentation(dimensions: scaledSize, resource: resource)], reference: nil) var attributes: [MessageAttribute] = [] if let timer = item.timer, timer > 0 && timer <= 60 { attributes.append(AutoremoveTimeoutMessageAttribute(timeout: Int32(timer), countdownBeginTime: nil)) } - messages.append(.message(text: caption ?? "", attributes: attributes, media: media, replyToMessageId: nil)) + messages.append(.message(text: caption ?? "", attributes: attributes, media: media, replyToMessageId: nil, localGroupingKey: item.groupedId)) } } case let .asset(asset): @@ -173,12 +175,12 @@ func legacyAssetPickerEnqueueMessages(account: Account, peerId: PeerId, signals: let scaledSize = size.aspectFitted(CGSize(width: 1280.0, height: 1280.0)) let resource = PhotoLibraryMediaResource(localIdentifier: asset.localIdentifier) - let media = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: randomId), representations: [TelegramMediaImageRepresentation(dimensions: scaledSize, resource: resource)]) + let media = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: randomId), representations: [TelegramMediaImageRepresentation(dimensions: scaledSize, resource: resource)], reference: nil) var attributes: [MessageAttribute] = [] if let timer = item.timer, timer > 0 && timer <= 60 { attributes.append(AutoremoveTimeoutMessageAttribute(timeout: Int32(timer), countdownBeginTime: nil)) } - messages.append(.message(text: caption ?? "", attributes: attributes, media: media, replyToMessageId: nil)) + messages.append(.message(text: caption ?? "", attributes: attributes, media: media, replyToMessageId: nil, localGroupingKey: item.groupedId)) case .tempFile: break } @@ -189,7 +191,7 @@ func legacyAssetPickerEnqueueMessages(account: Account, peerId: PeerId, signals: arc4random_buf(&randomId, 8) let resource = LocalFileReferenceMediaResource(localFilePath: path, randomId: randomId) let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), resource: resource, previewRepresentations: [], mimeType: mimeType, size: nil, attributes: [.FileName(fileName: name)]) - messages.append(.message(text: caption ?? "", attributes: [], media: media, replyToMessageId: nil)) + messages.append(.message(text: caption ?? "", attributes: [], media: media, replyToMessageId: nil, localGroupingKey: item.groupedId)) default: break } @@ -244,7 +246,7 @@ func legacyAssetPickerEnqueueMessages(account: Account, peerId: PeerId, signals: if let timer = item.timer, timer > 0 && timer <= 60 { attributes.append(AutoremoveTimeoutMessageAttribute(timeout: Int32(timer), countdownBeginTime: nil)) } - messages.append(.message(text: caption ?? "", attributes: attributes, media: media, replyToMessageId: nil)) + messages.append(.message(text: caption ?? "", attributes: attributes, media: media, replyToMessageId: nil, localGroupingKey: item.groupedId)) } } } diff --git a/TelegramUI/LegacySuggestionContext.swift b/TelegramUI/LegacySuggestionContext.swift index 19e0f2deca..f485c33bea 100644 --- a/TelegramUI/LegacySuggestionContext.swift +++ b/TelegramUI/LegacySuggestionContext.swift @@ -10,7 +10,7 @@ func legacySuggestionContext(account: Account, peerId: PeerId) -> TGSuggestionCo return SSignal { subscriber in if let mention = mention { let normalizedQuery = mention.lowercased() - let disposable = peerParticipants(account: account, id: peerId).start(next: { peers in + let disposable = peerParticipants(postbox: account.postbox, id: peerId).start(next: { peers in let filteredPeers = peers.filter { peer in if peer.indexName.matchesByTokens(normalizedQuery) { return true diff --git a/TelegramUI/ListMessageDateHeader.swift b/TelegramUI/ListMessageDateHeader.swift new file mode 100644 index 0000000000..9742888480 --- /dev/null +++ b/TelegramUI/ListMessageDateHeader.swift @@ -0,0 +1,84 @@ +import Foundation +import Display +import AsyncDisplayKit + +private let timezoneOffset: Int32 = { + let nowTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) + var now: time_t = time_t(nowTimestamp) + var timeinfoNow: tm = tm() + localtime_r(&now, &timeinfoNow) + return Int32(timeinfoNow.tm_gmtoff) +}() + +final class ListMessageDateHeader: ListViewItemHeader { + private let timestamp: Int32 + private let roundedTimestamp: Int32 + private let month: Int32 + private let year: Int32 + + let id: Int64 + let theme: PresentationTheme + let strings: PresentationStrings + + init(timestamp: Int32, theme: PresentationTheme, strings: PresentationStrings) { + self.timestamp = timestamp + self.theme = theme + self.strings = strings + + var time: time_t = time_t(timestamp + timezoneOffset) + var timeinfo: tm = tm() + localtime_r(&time, &timeinfo) + + self.roundedTimestamp = timeinfo.tm_year * 100 + timeinfo.tm_mon + self.month = timeinfo.tm_mon + self.year = timeinfo.tm_year + + self.id = Int64(self.roundedTimestamp) + } + + let stickDirection: ListViewItemHeaderStickDirection = .top + + let height: CGFloat = 36.0 + + func node() -> ListViewItemHeaderNode { + return ListMessageDateHeaderNode(theme: self.theme, strings: self.strings, roundedTimestamp: self.roundedTimestamp, month: self.month, year: self.year) + } +} + +private let sectionTitleFont = Font.regular(14.0) + +final class ListMessageDateHeaderNode: ListViewItemHeaderNode { + var theme: PresentationTheme + var strings: PresentationStrings + let titleNode: ASTextNode + let backgroundNode: ASDisplayNode + + init(theme: PresentationTheme, strings: PresentationStrings, roundedTimestamp: Int32, month: Int32, year: Int32) { + self.theme = theme + self.strings = strings + + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.backgroundColor = theme.list.plainBackgroundColor.withAlphaComponent(0.9) + + self.titleNode = ASTextNode() + self.titleNode.isLayerBacked = true + + super.init() + + let dateText = stringForMonth(strings: strings, month: month, ofYear: year) + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.titleNode) + self.titleNode.attributedText = NSAttributedString(string: dateText, font: sectionTitleFont, textColor: theme.list.itemPrimaryTextColor) + self.titleNode.maximumNumberOfLines = 1 + self.titleNode.truncationMode = .byTruncatingTail + } + + override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) { + let titleSize = self.titleNode.measure(CGSize(width: size.width - leftInset - rightInset - 24.0, height: CGFloat.greatestFiniteMagnitude)) + self.titleNode.frame = CGRect(origin: CGPoint(x: leftInset + 12.0, y: 8.0), size: titleSize) + self.backgroundNode.frame = CGRect(origin: CGPoint(), size: size) + } +} + diff --git a/TelegramUI/ListMessageFileItemNode.swift b/TelegramUI/ListMessageFileItemNode.swift index 5b0717f35b..42952cc7c1 100644 --- a/TelegramUI/ListMessageFileItemNode.swift +++ b/TelegramUI/ListMessageFileItemNode.swift @@ -105,6 +105,7 @@ private func extensionImage(fileExtension: String?) -> UIImage? { } private let titleFont = Font.medium(16.0) +private let audioTitleFont = Font.regular(16.0) private let descriptionFont = Font.regular(13.0) private let extensionFont = Font.medium(13.0) @@ -113,9 +114,34 @@ private struct FetchControls { let cancel: () -> Void } +private enum FileIconImage: Equatable { + case imageRepresentation(TelegramMediaImageRepresentation) + case albumArt(SharedMediaPlaybackAlbumArt) + + static func ==(lhs: FileIconImage, rhs: FileIconImage) -> Bool { + switch lhs { + case let .imageRepresentation(value): + if case .imageRepresentation(value) = rhs { + return true + } else { + return false + } + case let .albumArt(value): + if case .albumArt(value) = rhs { + return true + } else { + return false + } + } + } +} + final class ListMessageFileItemNode: ListMessageNode { private let highlightedBackgroundNode: ASDisplayNode private let separatorNode: ASDisplayNode + + private var selectionNode: ItemListSelectableControlNode? + private let titleNode: TextNode private let descriptionNode: TextNode @@ -123,7 +149,7 @@ final class ListMessageFileItemNode: ListMessageNode { private let extensionIconText: TextNode private let iconImageNode: TransformImageNode - private var currentIconImageRepresentation: TelegramMediaImageRepresentation? + private var currentIconImage: FileIconImage? private var currentMedia: Media? private let statusDisposable = MetaDisposable() @@ -135,11 +161,18 @@ final class ListMessageFileItemNode: ListMessageNode { private var linearProgressNode: ASDisplayNode private let progressNode: RadialProgressNode + private var playbackOverlayNode: ListMessagePlaybackOverlayNode? private var account: Account? private (set) var message: Message? private var appliedItem: ListMessageItem? + private var layoutParams: ListViewItemLayoutParams? + private var currentLeftOffet: CGFloat = 0.0 + + override var canBeLongTapped: Bool { + return true + } public required init() { self.separatorNode = ASDisplayNode() @@ -165,6 +198,7 @@ final class ListMessageFileItemNode: ListMessageNode { self.iconImageNode = TransformImageNode() self.iconImageNode.displaysAsynchronously = false + self.iconImageNode.contentAnimations = .subsequentUpdates self.downloadStatusIconNode = ASImageNode() self.downloadStatusIconNode.isLayerBacked = true @@ -199,11 +233,11 @@ final class ListMessageFileItemNode: ListMessageNode { self.item = item } - override public func layoutForWidth(_ width: CGFloat, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { + override public func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { if let item = item as? ListMessageItem { let doLayout = self.asyncLayout() - let merged = (top: false, bottom: false, dateAtBottom: false)//item.mergedWithItems(top: previousItem, bottom: nextItem) - let (layout, apply) = doLayout(item, width, merged.top, merged.bottom, merged.dateAtBottom) + let merged = (top: false, bottom: false, dateAtBottom: item.getDateAtBottom(top: previousItem, bottom: nextItem)) + let (layout, apply) = doLayout(item, params, merged.top, merged.bottom, merged.dateAtBottom) self.contentSize = layout.contentSize self.insets = layout.insets apply(.None) @@ -218,7 +252,7 @@ final class ListMessageFileItemNode: ListMessageNode { //self.layer.animateBoundsOriginYAdditive(from: -self.bounds.size.height * 1.4, to: 0.0, duration: duration) } - override func asyncLayout() -> (_ item: ListMessageItem, _ width: CGFloat, _ mergedTop: Bool, _ mergedBottom: Bool, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { + override func asyncLayout() -> (_ item: ListMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { let titleNodeMakeLayout = TextNode.asyncLayout(self.titleNode) let descriptionNodeMakeLayout = TextNode.asyncLayout(self.descriptionNode) let extensionIconTextMakeLayout = TextNode.asyncLayout(self.extensionIconText) @@ -226,25 +260,36 @@ final class ListMessageFileItemNode: ListMessageNode { let currentMedia = self.currentMedia let currentMessage = self.message - let currentIconImageRepresentation = self.currentIconImageRepresentation + let currentIconImage = self.currentIconImage let currentItem = self.appliedItem - return { [weak self] item, width, mergedTop, _, _ in + let selectionNodeLayout = ItemListSelectableControlNode.asyncLayout(self.selectionNode) + + return { [weak self] item, params, _, _, dateHeaderAtBottom in var updatedTheme: PresentationTheme? if currentItem?.theme !== item.theme { updatedTheme = item.theme } - let leftInset: CGFloat = 65.0 + var leftInset: CGFloat = 65.0 + params.leftInset + let rightInset: CGFloat = 8.0 + params.rightInset + + var leftOffset: CGFloat = 0.0 + var selectionNodeWidthAndApply: (CGFloat, (CGSize, Bool) -> ItemListSelectableControlNode)? + if case let .selectable(selected) = item.selection { + let (selectionWidth, selectionApply) = selectionNodeLayout(item.theme.list.itemCheckColors.strokeColor, item.theme.list.itemCheckColors.fillColor, item.theme.list.itemCheckColors.foregroundColor, selected) + selectionNodeWidthAndApply = (selectionWidth, selectionApply) + leftOffset += selectionWidth + } var extensionIconImage: UIImage? var titleText: NSAttributedString? var descriptionText: NSAttributedString? var extensionText: NSAttributedString? - var iconImageRepresentation: TelegramMediaImageRepresentation? + var iconImage: FileIconImage? var updateIconImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? var updatedStatusSignal: Signal? var updatedFetchControls: FetchControls? @@ -259,10 +304,10 @@ final class ListMessageFileItemNode: ListMessageNode { selectedMedia = file for attribute in file.attributes { - if case let .Audio(voice, duration, title, performer, waveform) = attribute { + if case let .Audio(voice, _, title, performer, _) = attribute { isAudio = true - titleText = NSAttributedString(string: title ?? "Unknown Track", font: titleFont, textColor: item.theme.list.itemPrimaryTextColor) + titleText = NSAttributedString(string: title ?? "Unknown Track", font: audioTitleFont, textColor: item.theme.list.itemPrimaryTextColor) let descriptionString: String if let performer = performer { @@ -274,6 +319,10 @@ final class ListMessageFileItemNode: ListMessageNode { } descriptionText = NSAttributedString(string: descriptionString, font: descriptionFont, textColor: item.theme.list.itemSecondaryTextColor) + + if !voice { + iconImage = .albumArt(SharedMediaPlaybackAlbumArt(thumbnailResource: ExternalMusicAlbumArtResource(title: title ?? "", performer: performer ?? "", isThumbnail: true), fullSizeResource: ExternalMusicAlbumArtResource(title: title ?? "", performer: performer ?? "", isThumbnail: false))) + } } } @@ -283,19 +332,18 @@ final class ListMessageFileItemNode: ListMessageNode { var fileExtension: String? if let range = fileName.range(of: ".", options: [.backwards]) { - fileExtension = fileName.substring(from: range.upperBound).lowercased() + fileExtension = fileName[range.upperBound...].lowercased() } extensionIconImage = extensionImage(fileExtension: fileExtension) if let fileExtension = fileExtension { extensionText = NSAttributedString(string: fileExtension, font: extensionFont, textColor: UIColor.white) } - iconImageRepresentation = smallestImageRepresentation(file.previewRepresentations) + if let representation = smallestImageRepresentation(file.previewRepresentations) { + iconImage = .imageRepresentation(representation) + } - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "MMM d, yyyy 'at' h a" - - let dateString = dateFormatter.string(from: Date(timeIntervalSince1970: Double(item.message.timestamp))) + let dateString = stringForFullDate(timestamp: item.message.timestamp, strings: item.strings, timeFormat: .regular) let descriptionString: String if let size = file.size { @@ -311,6 +359,10 @@ final class ListMessageFileItemNode: ListMessageNode { } } + if isAudio { + leftInset += 14.0 + } + var mediaUpdated = false if let currentMedia = currentMedia { if let selectedMedia = selectedMedia { @@ -347,10 +399,10 @@ final class ListMessageFileItemNode: ListMessageNode { if let currentUpdatedStatusSignal = updatedStatusSignal { updatedStatusSignal = currentUpdatedStatusSignal |> map { status in switch status { - case .fetchStatus: - return .fetchStatus(.Local) - case .playbackStatus: - return status + case .fetchStatus: + return .fetchStatus(.Local) + case .playbackStatus: + return status } } } @@ -358,60 +410,98 @@ final class ListMessageFileItemNode: ListMessageNode { } } - let (titleNodeLayout, titleNodeApply) = titleNodeMakeLayout(titleText, nil, 1, .middle, CGSize(width: width - leftInset - 8.0, height: CGFloat.infinity), .natural, nil, UIEdgeInsets()) + let (titleNodeLayout, titleNodeApply) = titleNodeMakeLayout(TextNodeLayoutArguments(attributedString: titleText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let (descriptionNodeLayout, descriptionNodeApply) = descriptionNodeMakeLayout(descriptionText, nil, 1, .end, CGSize(width: width - leftInset - 8.0 - 12.0, height: CGFloat.infinity), .natural, nil, UIEdgeInsets()) + let (descriptionNodeLayout, descriptionNodeApply) = descriptionNodeMakeLayout(TextNodeLayoutArguments(attributedString: descriptionText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 12.0, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let (extensionTextLayout, extensionTextApply) = extensionIconTextMakeLayout(extensionText, nil, 1, .end, CGSize(width: 38.0, height: CGFloat.infinity), .natural, nil, UIEdgeInsets()) + let (extensionTextLayout, extensionTextApply) = extensionIconTextMakeLayout(TextNodeLayoutArguments(attributedString: extensionText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 38.0, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) var iconImageApply: (() -> Void)? - if let iconImageRepresentation = iconImageRepresentation { - let iconSize = CGSize(width: 42.0, height: 42.0) - let imageCorners = ImageCorners(topLeft: .Corner(4.0), topRight: .Corner(4.0), bottomLeft: .Corner(4.0), bottomRight: .Corner(4.0)) - let arguments = TransformImageArguments(corners: imageCorners, imageSize: iconImageRepresentation.dimensions.aspectFilled(iconSize), boundingSize: iconSize, intrinsicInsets: UIEdgeInsets()) - iconImageApply = iconImageLayout(arguments) + if let iconImage = iconImage { + switch iconImage { + case let .imageRepresentation(representation): + let iconSize = CGSize(width: 42.0, height: 42.0) + let imageCorners = ImageCorners(topLeft: .Corner(4.0), topRight: .Corner(4.0), bottomLeft: .Corner(4.0), bottomRight: .Corner(4.0)) + let arguments = TransformImageArguments(corners: imageCorners, imageSize: representation.dimensions.aspectFilled(iconSize), boundingSize: iconSize, intrinsicInsets: UIEdgeInsets()) + iconImageApply = iconImageLayout(arguments) + case .albumArt: + let iconSize = CGSize(width: 46.0, height: 46.0) + let imageCorners = ImageCorners(topLeft: .Corner(4.0), topRight: .Corner(4.0), bottomLeft: .Corner(4.0), bottomRight: .Corner(4.0)) + let arguments = TransformImageArguments(corners: imageCorners, imageSize: iconSize, boundingSize: iconSize, intrinsicInsets: UIEdgeInsets()) + iconImageApply = iconImageLayout(arguments) + } } - if currentIconImageRepresentation != iconImageRepresentation { - if let iconImageRepresentation = iconImageRepresentation { - let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [iconImageRepresentation]) - updateIconImageSignal = chatWebpageSnippetPhoto(account: item.account, photo: tmpImage) + if currentIconImage != iconImage { + if let iconImage = iconImage { + switch iconImage { + case let .imageRepresentation(representation): + let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [representation], reference: nil) + updateIconImageSignal = chatWebpageSnippetPhoto(account: item.account, photo: tmpImage) + case let .albumArt(albumArt): + updateIconImageSignal = playerAlbumArt(postbox: item.account.postbox, albumArt: albumArt, thumbnail: true) + + } } else { updateIconImageSignal = .complete() } } - let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: width, height: isAudio ? 54.0 : 52.0), insets: UIEdgeInsets(top: mergedTop ? 0.0 : 2.0, left: 0.0, bottom: 0.0, right: 0.0)) + var insets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0) + if dateHeaderAtBottom, let header = item.header { + insets.top += header.height + } - return (nodeLayout, { _ in + let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: isAudio ? 56.0 : 52.0), insets: insets) + + return (nodeLayout, { animation in if let strongSelf = self { + let transition: ContainedViewLayoutTransition + if animation.isAnimated { + transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) + } else { + transition = .immediate + } + strongSelf.currentMedia = selectedMedia strongSelf.message = message strongSelf.account = item.account strongSelf.appliedItem = item + strongSelf.layoutParams = params + strongSelf.currentLeftOffet = leftOffset if let _ = updatedTheme { - strongSelf.separatorNode.backgroundColor = item.theme.list.itemSeparatorColor + strongSelf.separatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor - strongSelf.progressNode.updateTheme(RadialProgressTheme(backgroundColor: item.theme.list.itemAccentColor, foregroundColor: item.theme.list.itemBackgroundColor, icon: nil)) + strongSelf.progressNode.updateTheme(RadialProgressTheme(backgroundColor: item.theme.list.itemAccentColor, foregroundColor: item.theme.list.plainBackgroundColor, icon: nil)) strongSelf.linearProgressNode.backgroundColor = item.theme.list.itemAccentColor } - strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: nodeLayout.contentSize.height - UIScreenPixel), size: CGSize(width: width - leftInset, height: UIScreenPixel)) - strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel - nodeLayout.insets.top), size: CGSize(width: width, height: nodeLayout.size.height + UIScreenPixel)) - - if isAudio { - if strongSelf.progressNode.supernode == nil { - strongSelf.addSubnode(strongSelf.progressNode) - strongSelf.progressNode.state = .Play + if let (selectionWidth, selectionApply) = selectionNodeWidthAndApply { + let selectionFrame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: selectionWidth, height: nodeLayout.contentSize.height)) + let selectionNode = selectionApply(selectionFrame.size, transition.isAnimated) + if selectionNode !== strongSelf.selectionNode { + strongSelf.selectionNode?.removeFromSupernode() + strongSelf.selectionNode = selectionNode + strongSelf.addSubnode(selectionNode) + selectionNode.frame = selectionFrame + transition.animatePosition(node: selectionNode, from: CGPoint(x: -selectionFrame.size.width / 2.0, y: selectionFrame.midY)) + } else { + transition.updateFrame(node: selectionNode, frame: selectionFrame) } - strongSelf.progressNode.frame = CGRect(origin: CGPoint(x: 10.0, y: 6.0), size: CGSize(width: 42.0, height: 42.0)) - } else if strongSelf.progressNode.supernode != nil { - strongSelf.progressNode.removeFromSupernode() + } else if let selectionNode = strongSelf.selectionNode { + strongSelf.selectionNode = nil + let selectionFrame = selectionNode.frame + transition.updatePosition(node: selectionNode, position: CGPoint(x: -selectionFrame.size.width / 2.0, y: selectionFrame.midY), completion: { [weak selectionNode] _ in + selectionNode?.removeFromSupernode() + }) } - strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 9.0), size: titleNodeLayout.size) + transition.updateFrame(node: strongSelf.separatorNode, frame: CGRect(origin: CGPoint(x: leftInset + leftOffset, y: nodeLayout.contentSize.height - UIScreenPixel), size: CGSize(width: params.width - leftInset - leftOffset, height: UIScreenPixel))) + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel - nodeLayout.insets.top), size: CGSize(width: params.width, height: nodeLayout.size.height + UIScreenPixel)) + + transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset, y: 8.0), size: titleNodeLayout.size)) let _ = titleNodeApply() var descriptionOffset: CGFloat = 0.0 @@ -429,24 +519,31 @@ final class ListMessageFileItemNode: ListMessageNode { } } - strongSelf.descriptionNode.frame = CGRect(origin: CGPoint(x: leftInset + descriptionOffset, y: 29.0), size: descriptionNodeLayout.size) + transition.updateFrame(node: strongSelf.descriptionNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset + descriptionOffset, y: isAudio ? 32.0 : 29.0), size: descriptionNodeLayout.size)) let _ = descriptionNodeApply() - let iconFrame = CGRect(origin: CGPoint(x: 9.0, y: 5.0), size: CGSize(width: 42.0, height: 42.0)) - strongSelf.extensionIconNode.frame = iconFrame + let iconFrame: CGRect + if isAudio { + let iconSize = CGSize(width: 48.0, height: 48.0) + iconFrame = CGRect(origin: CGPoint(x: params.leftInset + leftOffset + 20.0, y: 5.0), size: iconSize) + } else { + let iconSize = CGSize(width: 42.0, height: 42.0) + iconFrame = CGRect(origin: CGPoint(x: params.leftInset + leftOffset + 9.0, y: 5.0), size: iconSize) + } + transition.updateFrame(node: strongSelf.extensionIconNode, frame: iconFrame) strongSelf.extensionIconNode.image = extensionIconImage - strongSelf.extensionIconText.frame = CGRect(origin: CGPoint(x: 9.0 + floor((42.0 - extensionTextLayout.size.width) / 2.0), y: 5.0 + floor((42.0 - extensionTextLayout.size.height) / 2.0)), size: extensionTextLayout.size) + transition.updateFrame(node: strongSelf.extensionIconText, frame: CGRect(origin: CGPoint(x: leftOffset + 9.0 + floor((42.0 - extensionTextLayout.size.width) / 2.0), y: 5.0 + floor((42.0 - extensionTextLayout.size.height) / 2.0)), size: extensionTextLayout.size)) let _ = extensionTextApply() - strongSelf.currentIconImageRepresentation = iconImageRepresentation + strongSelf.currentIconImage = iconImage if let iconImageApply = iconImageApply { if let updateImageSignal = updateIconImageSignal { - strongSelf.iconImageNode.setSignal(account: item.account, signal: updateImageSignal) + strongSelf.iconImageNode.setSignal(updateImageSignal) } - strongSelf.iconImageNode.frame = iconFrame + transition.updateFrame(node: strongSelf.iconImageNode, frame: iconFrame) if strongSelf.iconImageNode.supernode == nil { strongSelf.addSubnode(strongSelf.iconImageNode) } @@ -470,14 +567,21 @@ final class ListMessageFileItemNode: ListMessageNode { } } + if let playbackOverlayNode = strongSelf.playbackOverlayNode { + transition.updateFrame(node: playbackOverlayNode, frame: iconFrame) + } + if let updatedStatusSignal = updatedStatusSignal { strongSelf.statusDisposable.set((updatedStatusSignal |> deliverOnMainQueue).start(next: { [weak strongSelf] status in displayLinkDispatcher.dispatch { if let strongSelf = strongSelf { strongSelf.resourceStatus = status + var musicIsPlaying: Bool? if !isAudio { - strongSelf.updateProgressFrame(size: strongSelf.bounds.size) + if let layoutParams = strongSelf.layoutParams { + strongSelf.updateProgressFrame(size: nodeLayout.contentSize, leftInset: layoutParams.leftInset, rightInset: layoutParams.rightInset, transition: .immediate) + } } else { switch status { case let .fetchStatus(fetchStatus): @@ -504,19 +608,33 @@ final class ListMessageFileItemNode: ListMessageNode { case let .playbackStatus(playbackStatus): switch playbackStatus { case .playing: + musicIsPlaying = true strongSelf.progressNode.state = .Pause case .paused: + musicIsPlaying = false strongSelf.progressNode.state = .Play } } } + if let musicIsPlaying = musicIsPlaying { + if strongSelf.playbackOverlayNode == nil { + let playbackOverlayNode = ListMessagePlaybackOverlayNode() + playbackOverlayNode.frame = strongSelf.iconImageNode.frame + strongSelf.playbackOverlayNode = playbackOverlayNode + strongSelf.addSubnode(playbackOverlayNode) + } + strongSelf.playbackOverlayNode?.isPlaying = musicIsPlaying + } else if let playbackOverlayNode = strongSelf.playbackOverlayNode { + strongSelf.playbackOverlayNode = nil + playbackOverlayNode.removeFromSupernode() + } } } })) } - strongSelf.updateProgressFrame(size: CGSize(width: width, height: 52.0)) - strongSelf.downloadStatusIconNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 32.0), size: CGSize(width: 11.0, height: 11.0)) + strongSelf.updateProgressFrame(size: CGSize(width: params.width, height: 52.0), leftInset: params.leftInset, rightInset: params.rightInset, transition: transition) + transition.updateFrame(node: strongSelf.downloadStatusIconNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset, y: 31.0), size: CGSize(width: 11.0, height: 11.0))) if let updatedFetchControls = updatedFetchControls { let _ = strongSelf.fetchControls.swap(updatedFetchControls) @@ -526,10 +644,10 @@ final class ListMessageFileItemNode: ListMessageNode { } } - override func setHighlighted(_ highlighted: Bool, animated: Bool) { - super.setHighlighted(highlighted, animated: animated) + override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { + super.setHighlighted(highlighted, at: point, animated: animated) - if highlighted { + if highlighted, let item = self.item, case .none = item.selection { self.highlightedBackgroundNode.alpha = 1.0 if self.highlightedBackgroundNode.supernode == nil { self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: self.separatorNode) @@ -570,7 +688,7 @@ final class ListMessageFileItemNode: ListMessageNode { override func updateSelectionState(animated: Bool) { } - private func updateProgressFrame(size: CGSize) { + private func updateProgressFrame(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) { var descriptionOffset: CGFloat = 0.0 if let resourceStatus = self.resourceStatus, let item = self.appliedItem { @@ -590,13 +708,11 @@ final class ListMessageFileItemNode: ListMessageNode { switch maybeFetchStatus { case let .Fetching(_, progress): - let progressFrame = CGRect(x: 65.0, y: size.height - 2.0, width: floor((size.width - 65.0) * CGFloat(progress)), height: 2.0) + let progressFrame = CGRect(x: self.currentLeftOffet + leftInset + 65.0, y: size.height - 2.0, width: floor((size.width - 65.0 - leftInset - rightInset) * CGFloat(progress)), height: 2.0) if self.linearProgressNode.supernode == nil { self.addSubnode(self.linearProgressNode) } - if !self.linearProgressNode.frame.equalTo(progressFrame) { - self.linearProgressNode.frame = progressFrame - } + transition.updateFrame(node: self.linearProgressNode, frame: progressFrame) if self.downloadStatusIconNode.supernode == nil { self.addSubnode(self.downloadStatusIconNode) } @@ -628,9 +744,10 @@ final class ListMessageFileItemNode: ListMessageNode { } var descriptionFrame = self.descriptionNode.frame - if !descriptionFrame.origin.x.isEqual(to: 65.0 + descriptionOffset) { - descriptionFrame.origin.x = 65.0 + descriptionOffset - self.descriptionNode.frame = descriptionFrame + let originX = self.titleNode.frame.minX + descriptionOffset + if !descriptionFrame.origin.x.isEqual(to: originX) { + descriptionFrame.origin.x = originX + transition.updateFrame(node: self.descriptionNode, frame: descriptionFrame) } } @@ -653,14 +770,33 @@ final class ListMessageFileItemNode: ListMessageNode { } case .Local: if let item = self.item, let controllerInteraction = self.controllerInteraction { - controllerInteraction.openMessage(item.message.id) + let _ = controllerInteraction.openMessage(item.message.id) } } case .playbackStatus: if let account = self.account, let applicationContext = account.applicationContext as? TelegramApplicationContext { - applicationContext.mediaManager.playlistPlayerControl(.playback(.togglePlayPause)) + applicationContext.mediaManager.playlistControl(.playback(.togglePlayPause)) } } } } + + override func header() -> ListViewItemHeader? { + return self.item?.header + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if let item = self.item, case .selectable = item.selection { + if self.bounds.contains(point) { + return self.view + } + } + return super.hitTest(point, with: event) + } + + override func longTapped() { + if let item = self.item { + item.controllerInteraction.openMessageContextMenu(item.message.id, self, self.bounds) + } + } } diff --git a/TelegramUI/ListMessageHoleItem.swift b/TelegramUI/ListMessageHoleItem.swift index 9787e19d2d..f8da923016 100644 --- a/TelegramUI/ListMessageHoleItem.swift +++ b/TelegramUI/ListMessageHoleItem.swift @@ -9,13 +9,13 @@ final class ListMessageHoleItem: ListViewItem { public init() { } - public func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { let configure = { () -> Void in let node = ListMessageHoleItemNode() let nodeLayout = node.asyncLayout() let (top, bottom, dateAtBottom) = (false, false, false) - let (layout, apply) = nodeLayout(self, width, top, bottom, dateAtBottom) + let (layout, apply) = nodeLayout(self, params, top, bottom, dateAtBottom) node.updateSelectionState(animated: false) @@ -35,7 +35,7 @@ final class ListMessageHoleItem: ListViewItem { } } - public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { if let node = node as? ListMessageHoleItemNode { Queue.mainQueue().async { node.updateSelectionState(animated: false) @@ -45,7 +45,7 @@ final class ListMessageHoleItem: ListViewItem { async { let (top, bottom, dateAtBottom) = (false, false, false) //self.mergedWithItems(top: previousItem, bottom: nextItem) - let (layout, apply) = nodeLayout(self, width, top, bottom, dateAtBottom) + let (layout, apply) = nodeLayout(self, params, top, bottom, dateAtBottom) Queue.mainQueue().async { completion(layout, { apply(animation) @@ -77,22 +77,22 @@ final class ListMessageHoleItemNode: ListViewItemNode { activityIndicator.startAnimating() } - override public func layoutForWidth(_ width: CGFloat, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { + override public func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { if let item = item as? ListMessageHoleItem { let doLayout = self.asyncLayout() - let merged = (top: false, bottom: false, dateAtBottom: false)//item.mergedWithItems(top: previousItem, bottom: nextItem) - let (layout, apply) = doLayout(item, width, merged.top, merged.bottom, merged.dateAtBottom) + let merged = (top: false, bottom: false, dateAtBottom: false) + let (layout, apply) = doLayout(item, params, merged.top, merged.bottom, merged.dateAtBottom) self.contentSize = layout.contentSize self.insets = layout.insets apply(.None) } } - func asyncLayout() -> (_ item: ListMessageHoleItem, _ width: CGFloat, _ mergedTop: Bool, _ mergedBottom: Bool, _ dateAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { - return { [weak self] _, width, _, _, _ in - return (ListViewItemNodeLayout(contentSize: CGSize(width: width, height: 50.0), insets: UIEdgeInsets()), { _ in + func asyncLayout() -> (_ item: ListMessageHoleItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool, _ dateAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { + return { [weak self] _, params, _, _, _ in + return (ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 50.0), insets: UIEdgeInsets()), { _ in if let strongSelf = self, let activityIndicator = strongSelf.activityIndicator { - let boundsSize = CGSize(width: width, height: 50.0) + let boundsSize = CGSize(width: params.width, height: 50.0) let size = activityIndicator.bounds.size activityIndicator.frame = CGRect(origin: CGPoint(x: floor((boundsSize.width - size.width) / 2.0), y: floor((boundsSize.height - size.height) / 2.0)), size: size) } diff --git a/TelegramUI/ListMessageItem.swift b/TelegramUI/ListMessageItem.swift index 2696f8f3fe..755f931574 100644 --- a/TelegramUI/ListMessageItem.swift +++ b/TelegramUI/ListMessageItem.swift @@ -7,27 +7,38 @@ import Postbox final class ListMessageItem: ListViewItem { let theme: PresentationTheme + let strings: PresentationStrings let account: Account - let peerId: PeerId + let chatLocation: ChatLocation let controllerInteraction: ChatControllerInteraction let message: Message + let selection: ChatHistoryMessageSelection + + let header: ListMessageDateHeader? let selectable: Bool = true - public init(theme: PresentationTheme, account: Account, peerId: PeerId, controllerInteraction: ChatControllerInteraction, message: Message) { + public init(theme: PresentationTheme, strings: PresentationStrings, account: Account, chatLocation: ChatLocation, controllerInteraction: ChatControllerInteraction, message: Message, selection: ChatHistoryMessageSelection, displayHeader: Bool) { self.theme = theme + self.strings = strings self.account = account - self.peerId = peerId + self.chatLocation = chatLocation self.controllerInteraction = controllerInteraction self.message = message + if displayHeader { + self.header = ListMessageDateHeader(timestamp: message.timestamp, theme: theme, strings: strings) + } else { + self.header = nil + } + self.selection = selection } - public func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { - var viewClassName: AnyClass = ListMessageFileItemNode.self + public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + var viewClassName: AnyClass = ListMessageSnippetItemNode.self for media in message.media { - if let _ = media as? TelegramMediaWebpage { - viewClassName = ListMessageSnippetItemNode.self + if let _ = media as? TelegramMediaFile { + viewClassName = ListMessageFileItemNode.self break } } @@ -38,8 +49,8 @@ final class ListMessageItem: ListViewItem { node.setupItem(self) let nodeLayout = node.asyncLayout() - let (top, bottom, dateAtBottom) = (previousItem != nil, nextItem != nil, false) //self.mergedWithItems(top: previousItem, bottom: nextItem) - let (layout, apply) = nodeLayout(self, width, top, bottom, dateAtBottom) + let (top, bottom, dateAtBottom) = (previousItem != nil, nextItem != nil, self.getDateAtBottom(top: previousItem, bottom: nextItem)) + let (layout, apply) = nodeLayout(self, params, top, bottom, dateAtBottom) node.updateSelectionState(animated: false) @@ -59,7 +70,7 @@ final class ListMessageItem: ListViewItem { } } - public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { if let node = node as? ListMessageNode { Queue.mainQueue().async { node.setupItem(self) @@ -69,9 +80,9 @@ final class ListMessageItem: ListViewItem { let nodeLayout = node.asyncLayout() async { - let (top, bottom, dateAtBottom) = (previousItem != nil, nextItem != nil, false) //self.mergedWithItems(top: previousItem, bottom: nextItem) + let (top, bottom, dateAtBottom) = (previousItem != nil, nextItem != nil, self.getDateAtBottom(top: previousItem, bottom: nextItem)) - let (layout, apply) = nodeLayout(self, width, top, bottom, dateAtBottom) + let (layout, apply) = nodeLayout(self, params, top, bottom, dateAtBottom) Queue.mainQueue().async { completion(layout, { apply(animation) @@ -87,15 +98,36 @@ final class ListMessageItem: ListViewItem { func selected(listView: ListView) { listView.clearHighlightAnimated(true) - listView.forEachItemNode { itemNode in - if let itemNode = itemNode as? ListMessageFileItemNode { - if let messageId = itemNode.item?.message.id, messageId == self.message.id { - itemNode.activateMedia() + if case let .selectable(selected) = self.selection { + self.controllerInteraction.toggleMessagesSelection([self.message.id], !selected) + } else { + listView.forEachItemNode { itemNode in + if let itemNode = itemNode as? ListMessageFileItemNode { + if let messageId = itemNode.item?.message.id, messageId == self.message.id { + itemNode.activateMedia() + } + } else if let itemNode = itemNode as? ListMessageSnippetItemNode { + if let messageId = itemNode.item?.message.id, messageId == self.message.id { + itemNode.activateMedia() + } } } } } + func getDateAtBottom(top: ListViewItem?, bottom: ListViewItem?) -> Bool { + var dateAtBottom = false + if let top = top as? ListMessageItem, top.header != nil { + if top.header?.id != self.header?.id { + dateAtBottom = true + } + } else { + dateAtBottom = true + } + + return dateAtBottom + } + public var description: String { return "(ListMessageItem id: \(self.message.id), text: \"\(self.message.text)\")" } diff --git a/TelegramUI/ListMessageNode.swift b/TelegramUI/ListMessageNode.swift index 9fccd56ccf..e56678e1bd 100644 --- a/TelegramUI/ListMessageNode.swift +++ b/TelegramUI/ListMessageNode.swift @@ -15,12 +15,12 @@ class ListMessageNode: ListViewItemNode { self.item = item } - override public func layoutForWidth(_ width: CGFloat, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { + override public func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { } - func asyncLayout() -> (_ item: ListMessageItem, _ width: CGFloat, _ mergedTop: Bool, _ mergedBottom: Bool, _ dateAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { - return { _, width, _, _, _ in - return (ListViewItemNodeLayout(contentSize: CGSize(width: width, height: 1.0), insets: UIEdgeInsets()), { _ in + func asyncLayout() -> (_ item: ListMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool, _ dateAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { + return { _, params, _, _, _ in + return (ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 1.0), insets: UIEdgeInsets()), { _ in }) } diff --git a/TelegramUI/ListMessagePlaybackOverlayNode.swift b/TelegramUI/ListMessagePlaybackOverlayNode.swift new file mode 100644 index 0000000000..93a9d07024 --- /dev/null +++ b/TelegramUI/ListMessagePlaybackOverlayNode.swift @@ -0,0 +1,105 @@ +import Foundation +import AsyncDisplayKit +import Display + +private let backgroundImage = generateStretchableFilledCircleImage(radius: 4.0, color: UIColor(white: 0.0, alpha: 0.5)) + +final class ListMessagePlaybackOverlayNode: ASDisplayNode { + private let backgroundNode: ASImageNode + private let barNodes: [ASDisplayNode] + + var isPlaying: Bool = false { + didSet { + if self.isPlaying != oldValue { + if self.isInHierarchy { + if self.isPlaying { + self.animateToPlaying() + } else { + self.animateToPaused() + } + } + } + } + } + + override init() { + self.backgroundNode = ASImageNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.image = backgroundImage + + let baseSize = CGSize(width: 48.0, height: 48.0) + let barSize = CGSize(width: 3.0, height: 13.0) + let barSpacing: CGFloat = 2.0 + + self.backgroundNode.frame = CGRect(origin: CGPoint(), size: baseSize) + + let barsOrigin = CGPoint(x: floor((baseSize.width - (barSize.width * 4.0 + barSpacing * 3.0)) / 2.0), y: 23.0) + + var barNodes: [ASDisplayNode] = [] + for i in 0 ..< 4 { + let barNode = ASDisplayNode() + barNode.frame = CGRect(origin: barsOrigin.offsetBy(dx: CGFloat(i) * (barSize.width + barSpacing), dy: 0.0), size: barSize) + barNode.isLayerBacked = true + barNode.backgroundColor = .white + barNode.anchorPoint = CGPoint(x: 0.5, y: 1.0) + barNode.transform = CATransform3DMakeScale(1.0, 0.2, 1.0) + barNodes.append(barNode) + } + self.barNodes = barNodes + + super.init() + + self.isLayerBacked = true + + self.addSubnode(self.backgroundNode) + + for barNode in self.barNodes { + self.addSubnode(barNode) + } + } + + override func willEnterHierarchy() { + super.willEnterHierarchy() + + if self.isPlaying { + self.animateToPlaying() + } + } + + override func didExitHierarchy() { + super.didExitHierarchy() + + for barNode in self.barNodes { + barNode.layer.removeAnimation(forKey: "transform.scale.y") + } + } + + private func animateToPlaying() { + for barNode in self.barNodes { + let randValueMul = Float(arc4random()) / Float(UInt32.max) + let randDurationMul = Double(arc4random()) / Double(UInt32.max) + + let animation = CABasicAnimation(keyPath: "transform.scale.y") + animation.toValue = Float(0.5 + 0.5 * randValueMul) as NSNumber + animation.autoreverses = true + animation.duration = 0.25 + 0.25 * randDurationMul + animation.repeatCount = Float.greatestFiniteMagnitude; + animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn) + + barNode.layer.removeAnimation(forKey: "transform.scale.y") + barNode.layer.add(animation, forKey: "transform.scale.y") + } + } + + private func animateToPaused() { + for barNode in self.barNodes { + if let presentationLayer = barNode.layer.presentation() { + let animation = CABasicAnimation(keyPath: "transform.scale.y") + animation.fromValue = (presentationLayer.value(forKeyPath: "transform.scale.y") as? NSNumber)?.floatValue ?? 1.0 + animation.toValue = 0.2 as NSNumber + animation.duration = 0.25 + barNode.layer.add(animation, forKey: "transform.scale.y") + } + } + } +} diff --git a/TelegramUI/ListMessageSnippetItemNode.swift b/TelegramUI/ListMessageSnippetItemNode.swift index 1f4cc03d81..d5d43fdf64 100644 --- a/TelegramUI/ListMessageSnippetItemNode.swift +++ b/TelegramUI/ListMessageSnippetItemNode.swift @@ -14,8 +14,12 @@ private let iconTextBackgroundImage = generateStretchableFilledCircleImage(radiu final class ListMessageSnippetItemNode: ListMessageNode { private let highlightedBackgroundNode: ASDisplayNode private let separatorNode: ASDisplayNode + + private var selectionNode: ItemListSelectableControlNode? + private let titleNode: TextNode private let descriptionNode: TextNode + private var linkHighlightingNode: LinkHighlightingNode? private let iconTextBackgroundNode: ASImageNode private let iconTextNode: TextNode @@ -23,9 +27,14 @@ final class ListMessageSnippetItemNode: ListMessageNode { private var currentIconImageRepresentation: TelegramMediaImageRepresentation? private var currentMedia: Media? + private var currentPrimaryUrl: String? private var appliedItem: ListMessageItem? + override var canBeLongTapped: Bool { + return true + } + public required init() { self.separatorNode = ASDisplayNode() self.separatorNode.displaysAsynchronously = false @@ -49,7 +58,6 @@ final class ListMessageSnippetItemNode: ListMessageNode { self.iconTextNode.isLayerBacked = true self.iconImageNode = TransformImageNode() - self.iconImageNode.isLayerBacked = true self.iconImageNode.displaysAsynchronously = false super.init() @@ -64,15 +72,33 @@ final class ListMessageSnippetItemNode: ListMessageNode { fatalError("init(coder:) has not been implemented") } + override func didLoad() { + super.didLoad() + + let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:))) + recognizer.tapActionAtPoint = { [weak self] point in + if let strongSelf = self, let _ = strongSelf.urlAtPoint(point) { + return .waitForSingleTap + } + return .fail + } + recognizer.highlight = { [weak self] point in + if let strongSelf = self { + strongSelf.updateTouchesAtPoint(point) + } + } + self.view.addGestureRecognizer(recognizer) + } + override func setupItem(_ item: ListMessageItem) { self.item = item } - override public func layoutForWidth(_ width: CGFloat, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { + override public func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { if let item = item as? ListMessageItem { let doLayout = self.asyncLayout() - let merged = (top: false, bottom: false, dateAtBottom: false)//item.mergedWithItems(top: previousItem, bottom: nextItem) - let (layout, apply) = doLayout(item, width, merged.top, merged.bottom, merged.dateAtBottom) + let merged = (top: false, bottom: false, dateAtBottom: item.getDateAtBottom(top: previousItem, bottom: nextItem)) + let (layout, apply) = doLayout(item, params, merged.top, merged.bottom, merged.dateAtBottom) self.contentSize = layout.contentSize self.insets = layout.insets apply(.None) @@ -87,7 +113,7 @@ final class ListMessageSnippetItemNode: ListMessageNode { //self.layer.animateBoundsOriginYAdditive(from: -self.bounds.size.height * 1.4, to: 0.0, duration: duration) } - override func asyncLayout() -> (_ item: ListMessageItem, _ width: CGFloat, _ mergedTop: Bool, _ mergedBottom: Bool, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { + override func asyncLayout() -> (_ item: ListMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { let titleNodeMakeLayout = TextNode.asyncLayout(self.titleNode) let descriptionNodeMakeLayout = TextNode.asyncLayout(self.descriptionNode) let iconTextMakeLayout = TextNode.asyncLayout(self.iconTextNode) @@ -97,14 +123,24 @@ final class ListMessageSnippetItemNode: ListMessageNode { let currentItem = self.appliedItem - return { [weak self] item, width, _, _, _ in + let selectionNodeLayout = ItemListSelectableControlNode.asyncLayout(self.selectionNode) + + return { [weak self] item, params, _, _, dateHeaderAtBottom in var updatedTheme: PresentationTheme? if currentItem?.theme !== item.theme { updatedTheme = item.theme } - let leftInset: CGFloat = 65.0 + let leftInset: CGFloat = 65.0 + params.leftInset + + var leftOffset: CGFloat = 0.0 + var selectionNodeWidthAndApply: (CGFloat, (CGSize, Bool) -> ItemListSelectableControlNode)? + if case let .selectable(selected) = item.selection { + let (selectionWidth, selectionApply) = selectionNodeLayout(item.theme.list.itemCheckColors.strokeColor, item.theme.list.itemCheckColors.fillColor, item.theme.list.itemCheckColors.foregroundColor, selected) + selectionNodeWidthAndApply = (selectionWidth, selectionApply) + leftOffset += selectionWidth + } var title: NSAttributedString? var descriptionText: NSAttributedString? @@ -115,22 +151,28 @@ final class ListMessageSnippetItemNode: ListMessageNode { let applyIconTextBackgroundImage = iconTextBackgroundImage + var primaryUrl: String? + var selectedMedia: TelegramMediaWebpage? + var processed = false for media in item.message.media { if let webpage = media as? TelegramMediaWebpage { selectedMedia = webpage if case let .Loaded(content) = webpage.content { + primaryUrl = content.url + + processed = true var hostName: String = "" if let url = URL(string: content.url), let host = url.host, !host.isEmpty { hostName = host - iconText = NSAttributedString(string: host.substring(to: host.index(after: host.startIndex)).uppercased(), font: iconFont, textColor: UIColor.white) + iconText = NSAttributedString(string: host[.. nsString.length { + range.location = max(0, nsString.length - range.length) + range.length = nsString.length - range.location + } + var urlString = nsString.substring(with: range) + var parsedUrl = URL(string: urlString) + if parsedUrl == nil || parsedUrl!.host == nil || parsedUrl!.host!.isEmpty { + urlString = "http://" + urlString + parsedUrl = URL(string: urlString) + } + if let url = parsedUrl, let host = url.host { + primaryUrl = urlString + + iconText = NSAttributedString(string: host[.. Void)? if let iconImageRepresentation = iconImageRepresentation { @@ -169,35 +252,70 @@ final class ListMessageSnippetItemNode: ListMessageNode { if currentIconImageRepresentation != iconImageRepresentation { if let iconImageRepresentation = iconImageRepresentation { - let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [iconImageRepresentation]) + let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [iconImageRepresentation], reference: nil) updateIconImageSignal = chatWebpageSnippetPhoto(account: item.account, photo: tmpImage) } else { updateIconImageSignal = .complete() } } - let contentHeight = 39.0 + descriptionNodeLayout.size.height + let contentHeight = 40.0 + descriptionNodeLayout.size.height - return (ListViewItemNodeLayout(contentSize: CGSize(width: width, height: contentHeight), insets: UIEdgeInsets()), { _ in + var insets = UIEdgeInsets() + if dateHeaderAtBottom, let header = item.header { + insets.top += header.height + } + + return (ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: contentHeight), insets: insets), { animation in if let strongSelf = self { + let transition: ContainedViewLayoutTransition + if animation.isAnimated { + transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) + } else { + transition = .immediate + } + strongSelf.appliedItem = item + strongSelf.currentMedia = selectedMedia + + strongSelf.currentPrimaryUrl = primaryUrl if let _ = updatedTheme { - strongSelf.separatorNode.backgroundColor = item.theme.list.itemSeparatorColor + strongSelf.separatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor } - strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentHeight - UIScreenPixel), size: CGSize(width: width - leftInset, height: UIScreenPixel)) - strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: width, height: contentHeight + UIScreenPixel)) + if let (selectionWidth, selectionApply) = selectionNodeWidthAndApply { + let selectionFrame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: selectionWidth, height: contentHeight)) + let selectionNode = selectionApply(selectionFrame.size, transition.isAnimated) + if selectionNode !== strongSelf.selectionNode { + strongSelf.selectionNode?.removeFromSupernode() + strongSelf.selectionNode = selectionNode + strongSelf.addSubnode(selectionNode) + selectionNode.frame = selectionFrame + transition.animatePosition(node: selectionNode, from: CGPoint(x: -selectionFrame.size.width / 2.0, y: selectionFrame.midY)) + } else { + transition.updateFrame(node: selectionNode, frame: selectionFrame) + } + } else if let selectionNode = strongSelf.selectionNode { + strongSelf.selectionNode = nil + let selectionFrame = selectionNode.frame + transition.updatePosition(node: selectionNode, position: CGPoint(x: -selectionFrame.size.width / 2.0, y: selectionFrame.midY), completion: { [weak selectionNode] _ in + selectionNode?.removeFromSupernode() + }) + } - strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 9.0), size: titleNodeLayout.size) + transition.updateFrame(node: strongSelf.separatorNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset, y: contentHeight - UIScreenPixel), size: CGSize(width: params.width - leftInset - leftOffset, height: UIScreenPixel))) + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: contentHeight + UIScreenPixel)) + + transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset, y: 9.0), size: titleNodeLayout.size)) let _ = titleNodeApply() - strongSelf.descriptionNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 29.0), size: descriptionNodeLayout.size) + transition.updateFrame(node: strongSelf.descriptionNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset - 1.0, y: 32.0), size: descriptionNodeLayout.size)) let _ = descriptionNodeApply() - let iconFrame = CGRect(origin: CGPoint(x: 9.0, y: 12.0), size: CGSize(width: 42.0, height: 42.0)) - strongSelf.iconTextNode.frame = CGRect(origin: CGPoint(x: iconFrame.minX + floor((42.0 - iconTextLayout.size.width) / 2.0), y: iconFrame.minY + floor((42.0 - iconTextLayout.size.height) / 2.0) + 3.0), size: iconTextLayout.size) + let iconFrame = CGRect(origin: CGPoint(x: params.leftInset + leftOffset + 9.0, y: 12.0), size: CGSize(width: 42.0, height: 42.0)) + transition.updateFrame(node: strongSelf.iconTextNode, frame: CGRect(origin: CGPoint(x: iconFrame.minX + floor((42.0 - iconTextLayout.size.width) / 2.0), y: iconFrame.minY + floor((42.0 - iconTextLayout.size.height) / 2.0) + 3.0), size: iconTextLayout.size)) let _ = iconTextApply() @@ -205,15 +323,16 @@ final class ListMessageSnippetItemNode: ListMessageNode { if let iconImageApply = iconImageApply { if let updateImageSignal = updateIconImageSignal { - strongSelf.iconImageNode.setSignal(account: item.account, signal: updateImageSignal) + strongSelf.iconImageNode.setSignal(updateImageSignal) } if strongSelf.iconImageNode.supernode == nil { strongSelf.addSubnode(strongSelf.iconImageNode) + strongSelf.iconImageNode.frame = iconFrame + } else { + transition.updateFrame(node: strongSelf.iconImageNode, frame: iconFrame) } - strongSelf.iconImageNode.frame = iconFrame - iconImageApply() if strongSelf.iconTextBackgroundNode.supernode != nil { @@ -222,14 +341,18 @@ final class ListMessageSnippetItemNode: ListMessageNode { if strongSelf.iconTextNode.supernode != nil { strongSelf.iconTextNode.removeFromSupernode() } - } else if strongSelf.iconImageNode.supernode != nil { - strongSelf.iconImageNode.removeFromSupernode() + } else { + if strongSelf.iconImageNode.supernode != nil { + strongSelf.iconImageNode.removeFromSupernode() + } if strongSelf.iconTextBackgroundNode.supernode == nil { strongSelf.iconTextBackgroundNode.image = applyIconTextBackgroundImage strongSelf.addSubnode(strongSelf.iconTextBackgroundNode) + strongSelf.iconTextBackgroundNode.frame = iconFrame + } else { + transition.updateFrame(node: strongSelf.iconTextBackgroundNode, frame: iconFrame) } - strongSelf.iconTextBackgroundNode.frame = iconFrame if strongSelf.iconTextNode.supernode == nil { strongSelf.addSubnode(strongSelf.iconTextNode) } @@ -239,10 +362,10 @@ final class ListMessageSnippetItemNode: ListMessageNode { } } - override func setHighlighted(_ highlighted: Bool, animated: Bool) { - super.setHighlighted(highlighted, animated: animated) + override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { + super.setHighlighted(highlighted, at: point, animated: animated) - if highlighted { + if highlighted, let item = self.item, case .none = item.selection, self.urlAtPoint(point) == nil { self.highlightedBackgroundNode.alpha = 1.0 if self.highlightedBackgroundNode.supernode == nil { self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: self.separatorNode) @@ -266,18 +389,135 @@ final class ListMessageSnippetItemNode: ListMessageNode { } override func transitionNode(id: MessageId, media: Media) -> ASDisplayNode? { + if let item = self.item, item.message.id == id, self.iconImageNode.supernode != nil { + return self.iconImageNode + } return nil } override func updateHiddenMedia() { + if let controllerInteraction = self.controllerInteraction, let item = self.item, controllerInteraction.hiddenMedia[item.message.id] != nil { + self.iconImageNode.isHidden = true + } else { + self.iconImageNode.isHidden = false + } } override func updateSelectionState(animated: Bool) { } func activateMedia() { - if let webpage = self.currentMedia as? TelegramMediaWebpage { + if let item = self.item, let currentPrimaryUrl = self.currentPrimaryUrl { + if let webpage = self.currentMedia as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content, content.instantPage != nil { + item.controllerInteraction.openInstantPage(item.message.id) + } else { + if !item.controllerInteraction.openMessage(item.message.id) { + item.controllerInteraction.openUrl(currentPrimaryUrl) + } + } + } + } + + override func header() -> ListViewItemHeader? { + return self.item?.header + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if let item = self.item, case .selectable = item.selection { + if self.bounds.contains(point) { + return self.view + } + } + if let _ = self.urlAtPoint(point) { + return self.view + } + return super.hitTest(point, with: event) + } + + private func urlAtPoint(_ point: CGPoint) -> String? { + let textNodeFrame = self.descriptionNode.frame + if let (_, attributes) = self.descriptionNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { + let possibleNames: [String] = [ + TextNode.UrlAttribute, + ] + for name in possibleNames { + if let value = attributes[NSAttributedStringKey(rawValue: name)] as? String { + return value + } + } + } + return nil + } + + @objc func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { + switch recognizer.state { + case .began: + break + case .ended: + if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation { + switch gesture { + case .tap, .longTap: + if let item = self.item, let url = self.urlAtPoint(location) { + if case .longTap = gesture { + item.controllerInteraction.longTap(ChatControllerInteractionLongTapAction.url(url)) + } else if url == self.currentPrimaryUrl { + item.controllerInteraction.openMessage(item.message.id) + } else { + item.controllerInteraction.openUrl(url) + } + } + case .hold, .doubleTap: + break + } + } + case .cancelled: + break + default: + break + } + } + + private func updateTouchesAtPoint(_ point: CGPoint?) { + if let item = self.item { + var rects: [CGRect]? + if let point = point { + let textNodeFrame = self.descriptionNode.frame + if let (index, attributes) = self.descriptionNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { + let possibleNames: [String] = [ + TextNode.UrlAttribute + ] + for name in possibleNames { + if let _ = attributes[NSAttributedStringKey(rawValue: name)] { + rects = self.descriptionNode.attributeRects(name: name, at: index) + break + } + } + } + } + if let rects = rects { + let linkHighlightingNode: LinkHighlightingNode + if let current = self.linkHighlightingNode { + linkHighlightingNode = current + } else { + linkHighlightingNode = LinkHighlightingNode(color: item.message.effectivelyIncoming(item.account.peerId) ? item.theme.chat.bubble.incomingLinkHighlightColor : item.theme.chat.bubble.outgoingLinkHighlightColor) + self.linkHighlightingNode = linkHighlightingNode + self.insertSubnode(linkHighlightingNode, belowSubnode: self.descriptionNode) + } + linkHighlightingNode.frame = self.descriptionNode.frame.offsetBy(dx: 0.0, dy: -4.0) + linkHighlightingNode.updateRects(rects.map { $0.insetBy(dx: -1.0, dy: -1.0) }) + } else if let linkHighlightingNode = self.linkHighlightingNode { + self.linkHighlightingNode = nil + linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in + linkHighlightingNode?.removeFromSupernode() + }) + } + } + } + + override func longTapped() { + if let item = self.item { + item.controllerInteraction.openMessageContextMenu(item.message.id, self, self.bounds) } } } diff --git a/TelegramUI/ListSectionHeaderNode.swift b/TelegramUI/ListSectionHeaderNode.swift index b4a2017148..29b15f7d36 100644 --- a/TelegramUI/ListSectionHeaderNode.swift +++ b/TelegramUI/ListSectionHeaderNode.swift @@ -65,17 +65,15 @@ final class ListSectionHeaderNode: ASDisplayNode { } } - override func layout() { - let size = self.bounds.size - + func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) { let makeLayout = TextNode.asyncLayout(self.label) - let (labelLayout, labelApply) = makeLayout(NSAttributedString(string: self.title ?? "", font: Font.medium(12.0), textColor: self.theme.chatList.sectionHeaderTextColor), self.backgroundColor, 1, .end, CGSize(width: max(0.0, size.width - 18.0), height: size.height), .natural, nil, UIEdgeInsets()) + let (labelLayout, labelApply) = makeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: self.title ?? "", font: Font.medium(12.0), textColor: self.theme.chatList.sectionHeaderTextColor), backgroundColor: self.backgroundColor, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(0.0, size.width - leftInset - rightInset - 18.0), height: size.height), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let _ = labelApply() - self.label.frame = CGRect(origin: CGPoint(x: 9.0, y: 6.0), size: labelLayout.size) + self.label.frame = CGRect(origin: CGPoint(x: leftInset + 9.0, y: 7.0), size: labelLayout.size) if let actionButton = self.actionButton { let buttonSize = actionButton.measure(CGSize(width: size.width, height: size.height)) - actionButton.frame = CGRect(origin: CGPoint(x: size.width - 9.0 - buttonSize.width, y: 6.0), size: buttonSize) + actionButton.frame = CGRect(origin: CGPoint(x: size.width - rightInset - 9.0 - buttonSize.width, y: 7.0), size: buttonSize) } } diff --git a/TelegramUI/LocalAuth.swift b/TelegramUI/LocalAuth.swift index 478c784512..28a04829db 100644 --- a/TelegramUI/LocalAuth.swift +++ b/TelegramUI/LocalAuth.swift @@ -2,9 +2,30 @@ import Foundation import LocalAuthentication import SwiftSignalKit +enum LocalAuthBiometricAuthentication { + case touchId + case faceId +} + struct LocalAuth { - static let isTouchIDAvailable: Bool = { - return LAContext().canEvaluatePolicy(LAPolicy(rawValue: Int(kLAPolicyDeviceOwnerAuthenticationWithBiometrics))!, error: nil) + static let biometricAuthentication: LocalAuthBiometricAuthentication? = { + let context = LAContext() + if context.canEvaluatePolicy(LAPolicy(rawValue: Int(kLAPolicyDeviceOwnerAuthenticationWithBiometrics))!, error: nil) { + if #available(iOSApplicationExtension 11.0, *) { + switch context.biometryType { + case .faceID: + return .faceId + case .touchID: + return .touchId + case .none: + return nil + } + } else { + return .touchId + } + } else { + return nil + } }() static func auth(reason: String) -> Signal { diff --git a/TelegramUI/ManagedAudioPlaylistPlayer.swift b/TelegramUI/ManagedAudioPlaylistPlayer.swift index 417a2ce64a..e35e562b57 100644 --- a/TelegramUI/ManagedAudioPlaylistPlayer.swift +++ b/TelegramUI/ManagedAudioPlaylistPlayer.swift @@ -307,7 +307,7 @@ final class ManagedAudioPlaylistPlayer { if let instantVideo = instantVideo { if let mediaManager = mediaManager, let account = account { let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } - let videoNode = InstantVideoNode(theme: presentationData.theme, manager: mediaManager, account: account, source: .messageMedia(stableId: instantVideo.2, file: instantVideo.0), priority: 0, withSound: true) + let videoNode = InstantVideoNode(theme: presentationData.theme, manager: mediaManager, postbox: account.postbox, source: .messageMedia(stableId: instantVideo.2, file: instantVideo.0), priority: 0, withSound: true, forceAudioToSpeaker: false) videoNode.tapped = { [weak videoNode] in videoNode?.togglePlayPause() } diff --git a/TelegramUI/ManagedAudioRecorder.swift b/TelegramUI/ManagedAudioRecorder.swift index 4d957ca1b7..1f50d0c795 100644 --- a/TelegramUI/ManagedAudioRecorder.swift +++ b/TelegramUI/ManagedAudioRecorder.swift @@ -128,34 +128,6 @@ private func rendererInputProc(refCon: UnsafeMutableRawPointer, ioActionFlags: U } }) - /* - if (status == noErr) - { - [[TGOpusAudioRecorder processingQueue] dispatchOnQueue:^ - { - TGOpusAudioRecorder *recorder = globalRecorder; - if (recorder != nil && recorder.recorderId == (int)(intptr_t)inRefCon && recorder->_recording) { - - if (recorder->_waitForTone) { - if (CACurrentMediaTime() - recorder->_waitForToneStart > 0.3) { - [recorder _processBuffer:&buffer]; - } - } else { - [recorder _processBuffer:&buffer]; - } - } - - free(buffer.mData); - }]; - } - - sharedQueue.async { - withAudioRecorderContext(Int32(unsafeBitCast(refCon, to: intptr_t.self)), { context in - if !context.isPaused { - - } - }) - }*/ return noErr } @@ -169,6 +141,8 @@ final class ManagedAudioRecorderContext { private let id: Int32 private let micLevel: ValuePromise private let recordingState: ValuePromise + private let beginWithTone: Bool + private let beganWithTone: (Bool) -> Void private var paused = true @@ -191,13 +165,25 @@ final class ManagedAudioRecorderContext { private var recordingStateUpdateTimestamp: Double? + private var hasAudioSession = false private var audioSessionDisposable: Disposable? - init(queue: Queue, mediaManager: MediaManager, micLevel: ValuePromise, recordingState: ValuePromise) { + private var toneRenderer: MediaPlayerAudioRenderer? + private var toneRendererAudioSession: MediaPlayerAudioSessionCustomControl? + private var toneRendererAudioSessionActivated = false + + private var processSamples = false + + private var toneTimer: SwiftSignalKit.Timer? + + init(queue: Queue, mediaManager: MediaManager, micLevel: ValuePromise, recordingState: ValuePromise, beginWithTone: Bool, beganWithTone: @escaping (Bool) -> Void) { assert(queue.isCurrent()) self.id = getNextRecorderContextId() self.micLevel = micLevel + self.beginWithTone = beginWithTone + self.beganWithTone = beganWithTone + self.recordingState = recordingState self.queue = queue @@ -205,6 +191,94 @@ final class ManagedAudioRecorderContext { self.dataItem = TGDataItem() self.oggWriter = TGOggOpusWriter() + if let toneData = audioRecordingToneData { + self.processSamples = false + let toneRenderer = MediaPlayerAudioRenderer(audioSession: .custom({ [weak self] control in + queue.async { + if let strongSelf = self { + strongSelf.toneRendererAudioSession = control + if !strongSelf.paused && strongSelf.hasAudioSession { + strongSelf.toneRendererAudioSessionActivated = true + control.activate() + } + } + } + return ActionDisposable { + } + }), playAndRecord: true, forceAudioToSpeaker: false, updatedRate: { + }, audioPaused: {}) + self.toneRenderer = toneRenderer + + let toneDataOffset = Atomic(value: 0) + toneRenderer.beginRequestingFrames(queue: DispatchQueue.global(), takeFrame: { + let frameSize = 44100 + + var takeRange: Range? + let _ = toneDataOffset.modify { current in + let count = min(toneData.count - current, frameSize) + if count > 0 { + takeRange = current ..< (current + count) + } + return current + count + } + + if let takeRange = takeRange { + var blockBuffer: CMBlockBuffer? + + let bytes = malloc(takeRange.count)! + toneData.withUnsafeBytes { (dataBytes: UnsafePointer) -> Void in + memcpy(bytes, dataBytes.advanced(by: takeRange.lowerBound), takeRange.count) + } + let status = CMBlockBufferCreateWithMemoryBlock(nil, bytes, takeRange.count, nil, nil, 0, takeRange.count, 0, &blockBuffer) + if status != noErr { + return .finished + } + + let sampleCount = takeRange.count / 2 + + let pts = CMTime(value: Int64(takeRange.lowerBound / 2), timescale: 44100) + var timingInfo = CMSampleTimingInfo(duration: CMTime(value: Int64(sampleCount), timescale: 44100), presentationTimeStamp: pts, decodeTimeStamp: pts) + var sampleBuffer: CMSampleBuffer? + var sampleSize = takeRange.count + guard CMSampleBufferCreate(nil, blockBuffer, true, nil, nil, nil, 1, 1, &timingInfo, 1, &sampleSize, &sampleBuffer) == noErr else { + return .finished + } + + if let sampleBuffer = sampleBuffer { + return .frame(MediaTrackFrame(type: .audio, sampleBuffer: sampleBuffer, resetDecoder: false, decoded: true)) + } else { + return .finished + } + } else { + return .finished + } + }) + toneRenderer.start() + let toneTimer = SwiftSignalKit.Timer(timeout: 0.05, repeat: true, completion: { [weak self] in + if let strongSelf = self { + var wait = false + + if let toneRenderer = strongSelf.toneRenderer { + let toneTime = CMTimebaseGetTime(toneRenderer.audioTimebase) + let endTime = CMTime(value: Int64(toneData.count / 2), timescale: 44100) + if CMTimeCompare(toneTime, endTime) >= 0 { + strongSelf.processSamples = true + } else { + wait = true + } + } + + if !wait { + strongSelf.toneTimer?.invalidate() + } + } + }, queue: queue) + self.toneTimer = toneTimer + toneTimer.start() + } else { + self.processSamples = true + } + addAudioRecorderContext(self.id, self) addAudioUnitHolder(self.id, queue, self.audioUnit) @@ -220,6 +294,9 @@ final class ManagedAudioRecorderContext { self.stop() self.audioSessionDisposable?.dispose() + + self.toneRenderer?.stop() + self.toneTimer?.invalidate() } func start() { @@ -281,18 +358,22 @@ final class ManagedAudioRecorderContext { let _ = self.audioUnit.swap(audioUnit) + self.toneRenderer?.setRate(1.0) + if self.audioSessionDisposable == nil { let queue = self.queue - self.audioSessionDisposable = self.mediaManager.audioSession.push(audioSessionType: .playAndRecord, activate: { [weak self] in + self.audioSessionDisposable = self.mediaManager.audioSession.push(audioSessionType: .playAndRecord, activate: { [weak self] state in queue.async { if let strongSelf = self, !strongSelf.paused { - strongSelf.audioSessionAcquired() + strongSelf.hasAudioSession = true + strongSelf.audioSessionAcquired(headset: state.isHeadsetConnected) } } }, deactivate: { [weak self] in return Signal { subscriber in queue.async { if let strongSelf = self { + strongSelf.hasAudioSession = false strongSelf.stop() subscriber.putCompletion() } @@ -304,7 +385,18 @@ final class ManagedAudioRecorderContext { } } - func audioSessionAcquired() { + func audioSessionAcquired(headset: Bool) { + if let toneRendererAudioSession = self.toneRendererAudioSession, headset || self.beginWithTone { + self.beganWithTone(true) + if !self.toneRendererAudioSessionActivated { + self.toneRendererAudioSessionActivated = true + toneRendererAudioSession.activate() + } + } else { + self.processSamples = true + self.beganWithTone(false) + } + self.audioUnit.with { audioUnit -> Void in if let audioUnit = audioUnit { guard AudioOutputUnitStart(audioUnit) == noErr else { @@ -339,6 +431,11 @@ final class ManagedAudioRecorderContext { } } + if let toneRendererAudioSession = self.toneRendererAudioSession, self.toneRendererAudioSessionActivated { + self.toneRendererAudioSessionActivated = false + toneRendererAudioSession.deactivate() + } + let audioSessionDisposable = self.audioSessionDisposable self.audioSessionDisposable = nil audioSessionDisposable?.dispose() @@ -351,6 +448,10 @@ final class ManagedAudioRecorderContext { free(buffer.mData) } + if !self.processSamples { + return + } + let millisecondsPerPacket = 60 let encoderPacketSizeInBytes = 16000 / 1000 * millisecondsPerPacket * 2 @@ -466,8 +567,8 @@ final class ManagedAudioRecorderContext { memset(scaledSamples, 0, 100 * 2); var waveform: Data? + let count = self.waveformSamples.count / 2 self.waveformSamples.withUnsafeMutableBytes { (samples: UnsafeMutablePointer) -> Void in - let count = self.waveformSamples.count / 2 for i in 0 ..< count { let sample = samples[i] let index = i * 100 / count @@ -539,6 +640,8 @@ final class ManagedAudioRecorder { private let micLevelValue = ValuePromise(0.0) private let recordingStateValue = ValuePromise(.paused(duration: 0.0)) + let beginWithTone: Bool + var micLevel: Signal { return self.micLevelValue.get() } @@ -547,9 +650,10 @@ final class ManagedAudioRecorder { return self.recordingStateValue.get() } - init(mediaManager: MediaManager) { + init(mediaManager: MediaManager, beginWithTone: Bool, beganWithTone: @escaping (Bool) -> Void) { + self.beginWithTone = beginWithTone self.queue.async { - let context = ManagedAudioRecorderContext(queue: self.queue, mediaManager: mediaManager, micLevel: self.micLevelValue, recordingState: self.recordingStateValue) + let context = ManagedAudioRecorderContext(queue: self.queue, mediaManager: mediaManager, micLevel: self.micLevelValue, recordingState: self.recordingStateValue, beginWithTone: beginWithTone, beganWithTone: beganWithTone) self.contextRef = Unmanaged.passRetained(context) } } diff --git a/TelegramUI/ManagedAudioSession.swift b/TelegramUI/ManagedAudioSession.swift index f4e6941cd5..87e3cff207 100644 --- a/TelegramUI/ManagedAudioSession.swift +++ b/TelegramUI/ManagedAudioSession.swift @@ -26,49 +26,96 @@ private func allowBluetoothForType(_ type: ManagedAudioSessionType) -> Bool { } } +public enum AudioSessionOutput { + case speaker +} + +public enum AudioSessionOutputMode: Equatable { + case system + case speakerIfNoHeadphones + case custom(AudioSessionOutput) + + public static func ==(lhs: AudioSessionOutputMode, rhs: AudioSessionOutputMode) -> Bool { + switch lhs { + case .system: + if case .system = rhs { + return true + } else { + return false + } + case .speakerIfNoHeadphones: + if case .speakerIfNoHeadphones = rhs { + return true + } else { + return false + } + case let .custom(output): + if case .custom(output) = rhs { + return true + } else { + return false + } + } + } +} + private final class HolderRecord { let id: Int32 let audioSessionType: ManagedAudioSessionType let control: ManagedAudioSessionControl let activate: (ManagedAudioSessionControl) -> Void let deactivate: () -> Signal + let headsetConnectionStatusChanged: (Bool) -> Void let once: Bool - var overrideSpeaker: Bool + var outputMode: AudioSessionOutputMode var active: Bool = false var deactivatingDisposable: Disposable? = nil - init(id: Int32, audioSessionType: ManagedAudioSessionType, control: ManagedAudioSessionControl, activate: @escaping (ManagedAudioSessionControl) -> Void, deactivate: @escaping () -> Signal, once: Bool, overrideSpeaker: Bool) { + init(id: Int32, audioSessionType: ManagedAudioSessionType, control: ManagedAudioSessionControl, activate: @escaping (ManagedAudioSessionControl) -> Void, deactivate: @escaping () -> Signal, headsetConnectionStatusChanged: @escaping (Bool) -> Void, once: Bool, outputMode: AudioSessionOutputMode) { self.id = id self.audioSessionType = audioSessionType self.control = control self.activate = activate self.deactivate = deactivate + self.headsetConnectionStatusChanged = headsetConnectionStatusChanged self.once = once - self.overrideSpeaker = overrideSpeaker + self.outputMode = outputMode } } +private final class ManagedAudioSessionControlActivate { + let f: (AudioSessionActivationState) -> Void + + init(_ f: @escaping (AudioSessionActivationState) -> Void) { + self.f = f + } +} + +public struct AudioSessionActivationState { + public let isHeadsetConnected: Bool +} + public class ManagedAudioSessionControl { private let setupImpl: (Bool) -> Void - private let activateImpl: () -> Void - private let setSpeakerImpl: (Bool) -> Void + private let activateImpl: (ManagedAudioSessionControlActivate) -> Void + private let setOutputModeImpl: (AudioSessionOutputMode) -> Void - fileprivate init(setupImpl: @escaping (Bool) -> Void, activateImpl: @escaping () -> Void, setSpeakerImpl: @escaping (Bool) -> Void) { + fileprivate init(setupImpl: @escaping (Bool) -> Void, activateImpl: @escaping (ManagedAudioSessionControlActivate) -> Void, setOutputModeImpl: @escaping (AudioSessionOutputMode) -> Void) { self.setupImpl = setupImpl self.activateImpl = activateImpl - self.setSpeakerImpl = setSpeakerImpl + self.setOutputModeImpl = setOutputModeImpl } public func setup(synchronous: Bool = false) { self.setupImpl(synchronous) } - public func activate() { - self.activateImpl() + public func activate(_ completion: @escaping (AudioSessionActivationState) -> Void) { + self.activateImpl(ManagedAudioSessionControlActivate(completion)) } - public func setSpeaker(_ value: Bool) { - self.setSpeakerImpl(value) + public func setOutputMode(_ mode: AudioSessionOutputMode) { + self.setOutputModeImpl(mode) } } @@ -76,22 +123,121 @@ public final class ManagedAudioSession { private var nextId: Int32 = 0 private let queue = Queue() private var holders: [HolderRecord] = [] - private var currentTypeAndOverrideSpeaker: (ManagedAudioSessionType, Bool)? + private var currentTypeAndOutputMode: (ManagedAudioSessionType, AudioSessionOutputMode)? private var deactivateTimer: SwiftSignalKit.Timer? + private var isHeadsetPluggedInValue = false + private let outputsToHeadphonesSubscribers = Bag<(Bool) -> Void>() + private let isActiveSubscribers = Bag<(Bool) -> Void>() + + init() { + let queue = self.queue + NotificationCenter.default.addObserver(forName: .AVAudioSessionRouteChange, object: AVAudioSession.sharedInstance(), queue: nil, using: { [weak self] _ in + queue.async { + if let strongSelf = self { + let value = strongSelf.isHeadsetPluggedIn() + if strongSelf.isHeadsetPluggedInValue != value { + strongSelf.isHeadsetPluggedInValue = value + if let (_, outputMode) = strongSelf.currentTypeAndOutputMode { + if case .speakerIfNoHeadphones = outputMode { + strongSelf.updateOutputMode(outputMode) + } + } + for subscriber in strongSelf.outputsToHeadphonesSubscribers.copyItems() { + subscriber(value) + } + for i in 0 ..< strongSelf.holders.count { + if strongSelf.holders[i].active { + strongSelf.holders[i].headsetConnectionStatusChanged(value) + break + } + } + } + } + } + }) + + NotificationCenter.default.addObserver(forName: .AVAudioSessionInterruption, object: AVAudioSession.sharedInstance(), queue: nil, using: { [weak self] notification in + guard let info = notification.userInfo, + let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt, + let type = AVAudioSessionInterruptionType(rawValue: typeValue) else { + return + } + + queue.async { + if let strongSelf = self { + if type == .began { + strongSelf.updateHolders(interruption: true) + } + } + } + }) + + queue.async { + self.isHeadsetPluggedInValue = self.isHeadsetPluggedIn() + } + } + deinit { self.deactivateTimer?.invalidate() } - func push(audioSessionType: ManagedAudioSessionType, overrideSpeaker: Bool = false, once: Bool = false, activate: @escaping () -> Void, deactivate: @escaping () -> Signal) -> Disposable { + func headsetConnected() -> Signal { + let queue = self.queue + return Signal { [weak self] subscriber in + if let strongSelf = self { + subscriber.putNext(strongSelf.isHeadsetPluggedInValue) + + let index = strongSelf.outputsToHeadphonesSubscribers.add({ value in + subscriber.putNext(value) + }) + + return ActionDisposable { + queue.async { + if let strongSelf = self { + strongSelf.outputsToHeadphonesSubscribers.remove(index) + } + } + } + } else { + return EmptyDisposable + } + } |> runOn(queue) + } + + public func isActive() -> Signal { + let queue = self.queue + return Signal { [weak self] subscriber in + if let strongSelf = self { + subscriber.putNext(strongSelf.currentTypeAndOutputMode != nil) + + let index = strongSelf.isActiveSubscribers.add({ value in + subscriber.putNext(value) + }) + + return ActionDisposable { + queue.async { + if let strongSelf = self { + strongSelf.isActiveSubscribers.remove(index) + } + } + } + } else { + return EmptyDisposable + } + } |> runOn(queue) + } + + func push(audioSessionType: ManagedAudioSessionType, outputMode: AudioSessionOutputMode = .system, once: Bool = false, activate: @escaping (AudioSessionActivationState) -> Void, deactivate: @escaping () -> Signal) -> Disposable { return self.push(audioSessionType: audioSessionType, once: once, manualActivate: { control in control.setup() - control.activate() - activate() + control.activate({ state in + activate(state) + }) }, deactivate: deactivate) } - func push(audioSessionType: ManagedAudioSessionType, overrideSpeaker: Bool = false, once: Bool = false, manualActivate: @escaping (ManagedAudioSessionControl) -> Void, deactivate: @escaping () -> Signal) -> Disposable { + func push(audioSessionType: ManagedAudioSessionType, outputMode: AudioSessionOutputMode = .system, once: Bool = false, manualActivate: @escaping (ManagedAudioSessionControl) -> Void, deactivate: @escaping () -> Signal, headsetConnectionStatusChanged: @escaping (Bool) -> Void = { _ in }) -> Disposable { let id = OSAtomicIncrement32(&self.nextId) self.queue.async { self.holders.append(HolderRecord(id: id, audioSessionType: audioSessionType, control: ManagedAudioSessionControl(setupImpl: { [weak self] synchronous in @@ -99,7 +245,7 @@ public final class ManagedAudioSession { let f: () -> Void = { for holder in strongSelf.holders { if holder.id == id && holder.active { - strongSelf.setup(type: audioSessionType, overrideSpeaker: holder.overrideSpeaker) + strongSelf.setup(type: audioSessionType, outputMode: holder.outputMode) break } } @@ -111,34 +257,35 @@ public final class ManagedAudioSession { strongSelf.queue.async(f) } } - }, activateImpl: { [weak self] in + }, activateImpl: { [weak self] completion in if let strongSelf = self { strongSelf.queue.async { for holder in strongSelf.holders { if holder.id == id && holder.active { strongSelf.activate() + completion.f(AudioSessionActivationState(isHeadsetConnected: strongSelf.isHeadsetPluggedInValue)) break } } } } - }, setSpeakerImpl: { [weak self] value in + }, setOutputModeImpl: { [weak self] value in if let strongSelf = self { strongSelf.queue.async { for holder in strongSelf.holders { if holder.id == id { - if holder.overrideSpeaker != value { - holder.overrideSpeaker = value + if holder.outputMode != value { + holder.outputMode = value } if holder.active { - strongSelf.update(overrideSpeaker: value) + strongSelf.updateOutputMode(value) } } } } } - }), activate: manualActivate, deactivate: deactivate, once: once, overrideSpeaker: overrideSpeaker)) + }), activate: manualActivate, deactivate: deactivate, headsetConnectionStatusChanged: headsetConnectionStatusChanged, once: once, outputMode: outputMode)) self.updateHolders() } return ActionDisposable { [weak self] in @@ -150,6 +297,12 @@ public final class ManagedAudioSession { } } + func dropAll() { + self.queue.async { + self.updateHolders(interruption: true) + } + } + private func removeDeactivatedHolder(id: Int32) { assert(self.queue.isCurrent()) @@ -163,7 +316,7 @@ public final class ManagedAudioSession { } } - private func updateHolders() { + private func updateHolders(interruption: Bool = false) { assert(self.queue.isCurrent()) print("holder count \(self.holders.count)") @@ -186,11 +339,15 @@ public final class ManagedAudioSession { if let activeIndex = activeIndex { var deactivate = false - if activeIndex != self.holders.count - 1 { - if self.holders[activeIndex].audioSessionType == .voiceCall { - deactivate = false - } else { - deactivate = true + if interruption { + deactivate = true + } else { + if activeIndex != self.holders.count - 1 { + if self.holders[activeIndex].audioSessionType == .voiceCall { + deactivate = false + } else { + deactivate = true + } } } @@ -232,7 +389,7 @@ public final class ManagedAudioSession { private func applyNoneDelayed() { self.deactivateTimer?.invalidate() - if self.currentTypeAndOverrideSpeaker?.0 == .voiceCall { + if self.currentTypeAndOutputMode?.0 == .voiceCall { self.applyNone() } else { let deactivateTimer = SwiftSignalKit.Timer(timeout: 1.0, repeat: false, completion: { [weak self] in @@ -245,25 +402,49 @@ public final class ManagedAudioSession { } } + private func isHeadsetPluggedIn() -> Bool { + assert(self.queue.isCurrent()) + + let route = AVAudioSession.sharedInstance().currentRoute + //print("\(route)") + for desc in route.outputs { + if desc.portType == AVAudioSessionPortHeadphones || desc.portType == AVAudioSessionPortBluetoothA2DP || desc.portType == AVAudioSessionPortBluetoothHFP { + return true + } + } + + return false + } + private func applyNone() { self.deactivateTimer?.invalidate() self.deactivateTimer = nil - self.currentTypeAndOverrideSpeaker = nil + let wasActive = self.currentTypeAndOutputMode != nil + self.currentTypeAndOutputMode = nil + print("ManagedAudioSession setting active false") do { - try AVAudioSession.sharedInstance().setActive(false) + try AVAudioSession.sharedInstance().setActive(false, with: [.notifyOthersOnDeactivation]) } catch let error { print("ManagedAudioSession applyNone error \(error)") } + + if wasActive { + for subscriber in self.isActiveSubscribers.copyItems() { + subscriber(false) + } + } } - private func setup(type: ManagedAudioSessionType, overrideSpeaker: Bool) { + private func setup(type: ManagedAudioSessionType, outputMode: AudioSessionOutputMode) { self.deactivateTimer?.invalidate() self.deactivateTimer = nil - if self.currentTypeAndOverrideSpeaker == nil || self.currentTypeAndOverrideSpeaker! != (type, overrideSpeaker) { - self.currentTypeAndOverrideSpeaker = (type, overrideSpeaker) + let wasActive = self.currentTypeAndOutputMode != nil + + if self.currentTypeAndOutputMode == nil || self.currentTypeAndOutputMode! != (type, outputMode) { + self.currentTypeAndOutputMode = (type, outputMode) do { print("ManagedAudioSession setting category for \(type)") @@ -274,13 +455,36 @@ public final class ManagedAudioSession { print("ManagedAudioSession setup error \(error)") } } + + if !wasActive { + for subscriber in self.isActiveSubscribers.copyItems() { + subscriber(true) + } + } + } + + private func setupOutputMode(_ outputMode: AudioSessionOutputMode) throws { + switch outputMode { + case .system: + try AVAudioSession.sharedInstance().overrideOutputAudioPort(.none) + case let .custom(output): + switch output { + case .speaker: + try AVAudioSession.sharedInstance().overrideOutputAudioPort(.speaker) + } + case .speakerIfNoHeadphones: + if !self.isHeadsetPluggedInValue { + try AVAudioSession.sharedInstance().overrideOutputAudioPort(.speaker) + } + } } private func activate() { - if let (type, overrideSpeaker) = self.currentTypeAndOverrideSpeaker { + if let (type, outputMode) = self.currentTypeAndOutputMode { do { try AVAudioSession.sharedInstance().setActive(true) - try AVAudioSession.sharedInstance().overrideOutputAudioPort(overrideSpeaker ? .speaker : .none) + + try self.setupOutputMode(outputMode) if case .voiceCall = type { try AVAudioSession.sharedInstance().setPreferredIOBufferDuration(0.005) @@ -291,11 +495,11 @@ public final class ManagedAudioSession { } } - private func update(overrideSpeaker: Bool) { - if let (type, currentOverrideSpeaker) = self.currentTypeAndOverrideSpeaker, currentOverrideSpeaker != overrideSpeaker { - self.currentTypeAndOverrideSpeaker = (type, overrideSpeaker) + private func updateOutputMode(_ outputMode: AudioSessionOutputMode) { + if let (type, currentOutputMode) = self.currentTypeAndOutputMode, currentOutputMode != outputMode { + self.currentTypeAndOutputMode = (type, outputMode) do { - try AVAudioSession.sharedInstance().overrideOutputAudioPort(overrideSpeaker ? .speaker : .none) + try self.setupOutputMode(outputMode) } catch let error { print("ManagedAudioSession overrideOutputAudioPort error \(error)") } diff --git a/TelegramUI/MediaFrameSource.swift b/TelegramUI/MediaFrameSource.swift index d31c3a2b98..1dee04c0e0 100644 --- a/TelegramUI/MediaFrameSource.swift +++ b/TelegramUI/MediaFrameSource.swift @@ -7,9 +7,14 @@ enum MediaTrackEvent { case endOfStream } -struct MediaFrameSourceSeekResult { +final class MediaFrameSourceSeekResult { let buffers: MediaPlaybackBuffers let timestamp: CMTime + + init(buffers: MediaPlaybackBuffers, timestamp: CMTime) { + self.buffers = buffers + self.timestamp = timestamp + } } enum MediaFrameSourceSeekError { @@ -20,5 +25,5 @@ protocol MediaFrameSource { func addEventSink(_ f: @escaping (MediaTrackEvent) -> Void) -> Int func removeEventSink(_ index: Int) func generateFrames(until timestamp: Double) - func seek(timestamp: Double) -> Signal + func seek(timestamp: Double) -> Signal, MediaFrameSourceSeekError> } diff --git a/TelegramUI/MediaManager.swift b/TelegramUI/MediaManager.swift index dbd68477b8..a896c1ddc3 100644 --- a/TelegramUI/MediaManager.swift +++ b/TelegramUI/MediaManager.swift @@ -6,6 +6,8 @@ import MobileCoreServices import TelegramCore import MediaPlayer +import TelegramUIPrivateModule + private struct WrappedAudioPlaylistItemId: Hashable, Equatable { let playlistId: AudioPlaylistId let itemId: AudioPlaylistItemId @@ -158,13 +160,86 @@ enum SharedMediaPlayerGroup: Int { case voiceAndInstantVideo = 1 } +public enum MediaManagerPlayerType { + case voice + case music +} + +private let sharedAudioSession: ManagedAudioSession = { + let audioSession = ManagedAudioSession() + let _ = (audioSession.headsetConnected() |> deliverOnMainQueue).start(next: { value in + DeviceProximityManager.shared().setGloballyEnabled(!value) + }) + return audioSession +}() + public final class MediaManager: NSObject { + public static var globalAudioSession: ManagedAudioSession { + return sharedAudioSession + } + private let queue = Queue.mainQueue() + private let postbox: Postbox + private let inForeground: Signal + public let audioSession: ManagedAudioSession let overlayMediaManager = OverlayMediaManager() let sharedVideoContextManager = SharedVideoContextManager() + private var nextPlayerIndex: Int32 = 0 + + private var voiceMediaPlayer: SharedMediaPlayer? { + didSet { + if self.voiceMediaPlayer !== oldValue { + if let voiceMediaPlayer = self.voiceMediaPlayer { + self.voiceMediaPlayerStateValue.set(voiceMediaPlayer.playbackState |> map { state in + if let state = state, case let .item(item) = state { + return item + } else { + return nil + } + } |> deliverOnMainQueue) + } else { + self.voiceMediaPlayerStateValue.set(.single(nil)) + } + } + } + } + private let voiceMediaPlayerStateValue = Promise(nil) + var voiceMediaPlayerState: Signal { + return self.voiceMediaPlayerStateValue.get() + } + + private var musicMediaPlayer: SharedMediaPlayer? { + didSet { + if self.musicMediaPlayer !== oldValue { + if let musicMediaPlayer = self.musicMediaPlayer { + self.musicMediaPlayerStateValue.set(musicMediaPlayer.playbackState |> map { state in + if let state = state, case let .item(item) = state { + return item + } else { + return nil + } + } |> deliverOnMainQueue) + } else { + self.musicMediaPlayerStateValue.set(.single(nil)) + } + } + } + } + private let musicMediaPlayerStateValue = Promise(nil) + var musicMediaPlayerState: Signal { + return self.musicMediaPlayerStateValue.get() + } + + private let globalMediaPlayerStateValue = Promise<(SharedMediaPlayerItemPlaybackState, MediaManagerPlayerType)?>() + var globalMediaPlayerState: Signal<(SharedMediaPlayerItemPlaybackState, MediaManagerPlayerType)?, NoError> { + return self.globalMediaPlayerStateValue.get() + } + + private let setPlaylistByTypeDisposables = DisposableDict() + private let playlistPlayer = Atomic(value: nil) private let playlistPlayerStateAndStatusValue = Promise(nil) var playlistPlayerStateAndStatus: Signal { @@ -178,16 +253,37 @@ public final class MediaManager: NSObject { private let globalControlsStatus = Promise(nil) private let globalControlsDisposable = MetaDisposable() + private let globalControlsArtworkDisposable = MetaDisposable() + private let globalControlsArtwork = Promise(nil) private let globalControlsStatusDisposable = MetaDisposable() + private let globalAudioSessionForegroundDisposable = MetaDisposable() private var managedVideoContexts: [WrappedManagedMediaId: ActiveManagedVideoContext] = [:] let universalVideoManager = UniversalVideoContentManager() - override init() { - self.audioSession = ManagedAudioSession() + let galleryHiddenMediaManager = GalleryHiddenMediaManager() + + init(postbox: Postbox, inForeground: Signal) { + self.postbox = postbox + self.inForeground = inForeground + + self.audioSession = sharedAudioSession super.init() + + let combinedPlayersSignal: Signal<(SharedMediaPlayerItemPlaybackState, MediaManagerPlayerType)?, NoError> = combineLatest(self.voiceMediaPlayerState, self.musicMediaPlayerState) |> map { voice, music -> (SharedMediaPlayerItemPlaybackState, MediaManagerPlayerType)? in + if let voice = voice { + return (voice, .voice) + } else if let music = music { + return (music, .music) + } else { + return nil + } + } + self.globalMediaPlayerStateValue.set(combinedPlayersSignal |> distinctUntilChanged(isEqual: { lhs, rhs in + return lhs?.0 == rhs?.0 && lhs?.1 == rhs?.1 + })) let commandCenter = MPRemoteCommandCenter.shared() commandCenter.playCommand.addTarget(self, action: #selector(playCommandEvent(_:))) @@ -198,61 +294,71 @@ public final class MediaManager: NSObject { if #available(iOSApplicationExtension 9.1, *) { commandCenter.changePlaybackPositionCommand.addTarget(handler: { [weak self] event in if let strongSelf = self, let event = event as? MPChangePlaybackPositionCommandEvent { - strongSelf.playlistPlayerControl(.playback(.seek(event.positionTime))) + strongSelf.playlistControl(.seek(event.positionTime)) } return .success }) } - var previousStateAndStatus: AudioPlaylistStateAndStatus? + var previousState: SharedMediaPlayerItemPlaybackState? + var previousDisplayData: SharedMediaPlaybackDisplayData? + let globalControlsArtwork = self.globalControlsArtwork let globalControlsStatus = self.globalControlsStatus var baseNowPlayingInfo: [String: Any]? - self.globalControlsDisposable.set((self.playlistPlayerStateAndStatusValue.get() |> deliverOnMainQueue).start(next: { next in - if let next = next, let item = next.state.item, let info = item.info { - let commandCenter = MPRemoteCommandCenter.shared() - commandCenter.playCommand.isEnabled = true - commandCenter.pauseCommand.isEnabled = true - commandCenter.previousTrackCommand.isEnabled = true - commandCenter.nextTrackCommand.isEnabled = true - commandCenter.togglePlayPauseCommand.isEnabled = true + self.globalControlsDisposable.set((self.globalMediaPlayerState |> deliverOnMainQueue).start(next: { stateAndType in + if let (state, _) = stateAndType, let displayData = state.item.displayData { + if previousDisplayData != displayData { + previousDisplayData = displayData - var nowPlayingInfo: [String: Any] = [:] - - switch info.labelInfo { - case let .music(title, performer): - let titleText: String = title ?? "Unknown Track" - let subtitleText: String = performer ?? "Unknown Artist" - - nowPlayingInfo[MPMediaItemPropertyTitle] = titleText - nowPlayingInfo[MPMediaItemPropertyArtist] = subtitleText - case .voice: - let titleText: String = "Voice Message" - - nowPlayingInfo[MPMediaItemPropertyTitle] = titleText - case .video: - let titleText: String = "Video Message" - - nowPlayingInfo[MPMediaItemPropertyTitle] = titleText + let commandCenter = MPRemoteCommandCenter.shared() + commandCenter.playCommand.isEnabled = true + commandCenter.pauseCommand.isEnabled = true + commandCenter.previousTrackCommand.isEnabled = true + commandCenter.nextTrackCommand.isEnabled = true + commandCenter.togglePlayPauseCommand.isEnabled = true + + var nowPlayingInfo: [String: Any] = [:] + + var artwork: SharedMediaPlaybackAlbumArt? + + switch displayData { + case let .music(title, performer, artworkValue): + artwork = artworkValue + + let titleText: String = title ?? "Unknown Track" + let subtitleText: String = performer ?? "Unknown Artist" + + nowPlayingInfo[MPMediaItemPropertyTitle] = titleText + nowPlayingInfo[MPMediaItemPropertyArtist] = subtitleText + case let .voice(author, _): + let titleText: String = author?.displayTitle ?? "" + + nowPlayingInfo[MPMediaItemPropertyTitle] = titleText + case let .instantVideo(author, _): + let titleText: String = author?.displayTitle ?? "" + + nowPlayingInfo[MPMediaItemPropertyTitle] = titleText + } + + globalControlsArtwork.set(.single(artwork)) + + baseNowPlayingInfo = nowPlayingInfo + + MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo } - baseNowPlayingInfo = nowPlayingInfo - - MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo - - if previousStateAndStatus != next { - previousStateAndStatus = next - if let status = next.status { - globalControlsStatus.set(status |> map { Optional($0) }) - } else { - globalControlsStatus.set(.single(nil)) - } + if previousState != state { + previousState = state + globalControlsStatus.set(.single(state.status)) } } else { - previousStateAndStatus = nil + previousState = nil + previousDisplayData = nil baseNowPlayingInfo = nil globalControlsStatus.set(.single(nil)) + globalControlsArtwork.set(.single(nil)) let commandCenter = MPRemoteCommandCenter.shared() commandCenter.playCommand.isEnabled = false @@ -265,6 +371,44 @@ public final class MediaManager: NSObject { } })) + self.globalControlsArtworkDisposable.set((self.globalControlsArtwork.get() + |> distinctUntilChanged(isEqual: { $0 == $1 }) + |> mapToSignal { value -> Signal in + if let value = value { + return Signal { subscriber in + let fetched = postbox.mediaBox.fetchedResource(value.fullSizeResource, tag: TelegramMediaResourceFetchTag(statsCategory: .image)).start() + let data = postbox.mediaBox.resourceData(value.fullSizeResource, pathExtension: nil, option: .complete(waitUntilFetchStatus: false)).start(next: { data in + if data.complete, let value = try? Data(contentsOf: URL(fileURLWithPath: data.path)) { + subscriber.putNext(UIImage(data: value)) + subscriber.putCompletion() + } + }) + return ActionDisposable { + fetched.dispose() + data.dispose() + } + } + } else { + return .single(nil) + } + } |> deliverOnMainQueue).start(next: { image in + if var nowPlayingInfo = baseNowPlayingInfo { + if let image = image { + if #available(iOSApplicationExtension 10.0, *) { + nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: image.size, requestHandler: { size in + return image + }) + } else { + nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(image: image) + } + } else { + nowPlayingInfo.removeValue(forKey: MPMediaItemPropertyArtwork) + } + MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo + baseNowPlayingInfo = nowPlayingInfo + } + })) + self.globalControlsStatusDisposable.set((self.globalControlsStatus.get() |> deliverOnMainQueue).start(next: { next in if let next = next { if var nowPlayingInfo = baseNowPlayingInfo { @@ -288,12 +432,51 @@ public final class MediaManager: NSObject { } }*/ })) + + + let shouldKeepAudioSession: Signal = combineLatest(self.globalMediaPlayerState |> deliverOnMainQueue, inForeground |> deliverOnMainQueue) + |> map { stateAndType, inForeground -> Bool in + var isPlaying = false + if let (state, _) = stateAndType { + switch state.status.status { + case .playing: + isPlaying = true + case let .buffering(_, whilePlaying): + isPlaying = whilePlaying + default: + break + } + } + if !inForeground { + if !isPlaying { + return true + } + } + return false + } + |> distinctUntilChanged + |> mapToSignal { value -> Signal in + if value { + return .single(true) |> delay(0.8, queue: Queue.mainQueue()) + } else { + return .single(false) + } + } + + self.globalAudioSessionForegroundDisposable.set((shouldKeepAudioSession |> deliverOnMainQueue).start(next: { [weak self] value in + if value { + self?.audioSession.dropAll() + } + })) } deinit { self.playlistPlayerStateValueDisposable.dispose() self.globalControlsDisposable.dispose() + self.globalControlsArtworkDisposable.dispose() self.globalControlsStatusDisposable.dispose() + self.setPlaylistByTypeDisposables.dispose() + self.globalAudioSessionForegroundDisposable.dispose() } func videoContext(postbox: Postbox, id: ManagedMediaId, resource: MediaResource, preferSoftwareDecoding: Bool, backgroundThread: Bool, priority: Int32, initiatePlayback: Bool, activate: @escaping (MediaPlayerNode) -> Void, deactivate: @escaping () -> Signal) -> (MediaPlayer, Disposable) { @@ -306,7 +489,7 @@ public final class MediaManager: NSObject { activeContext = currentActiveContext } else { let mediaPlayer = MediaPlayer(audioSessionManager: self.audioSession, postbox: postbox, resource: resource, streamable: false, video: true, preferSoftwareDecoding: preferSoftwareDecoding, enableSound: false) - mediaPlayer.actionAtEnd = .loop + mediaPlayer.actionAtEnd = .loop(nil) let playerNode = MediaPlayerNode(backgroundThread: backgroundThread) mediaPlayer.attachPlayerNode(playerNode) @@ -329,12 +512,12 @@ public final class MediaManager: NSObject { return (activeContext.mediaPlayer, activeContext.addContextSubscriber(priority: priority, activate: activate, deactivate: deactivate)) } - func audioRecorder() -> Signal { + func audioRecorder(beginWithTone: Bool, beganWithTone: @escaping (Bool) -> Void) -> Signal { return Signal { subscriber in let disposable = MetaDisposable() self.queue.async { - let audioRecorder = ManagedAudioRecorder(mediaManager: self) + let audioRecorder = ManagedAudioRecorder(mediaManager: self, beginWithTone: beginWithTone, beganWithTone: beganWithTone) subscriber.putNext(audioRecorder) disposable.set(ActionDisposable { @@ -345,6 +528,71 @@ public final class MediaManager: NSObject { } } + func setPlaylist(_ playlist: SharedMediaPlaylist?, type: MediaManagerPlayerType) { + assert(Queue.mainQueue().isCurrent()) + self.setPlaylistByTypeDisposables.set((self.postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.musicPlaybackSettings]) + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] view in + if let strongSelf = self { + let settings = (view.values[ApplicationSpecificPreferencesKeys.musicPlaybackSettings] as? MusicPlaybackSettings) ?? MusicPlaybackSettings.defaultSettings + let nextPlayerIndex = strongSelf.nextPlayerIndex + strongSelf.nextPlayerIndex += 1 + switch type { + case .voice: + strongSelf.musicMediaPlayer?.control(.playback(.pause)) + strongSelf.voiceMediaPlayer?.stop() + if let playlist = playlist { + let voiceMediaPlayer = SharedMediaPlayer(mediaManager: strongSelf, inForeground: strongSelf.inForeground, postbox: strongSelf.postbox, audioSession: strongSelf.audioSession, overlayMediaManager: strongSelf.overlayMediaManager, playlist: playlist, initialOrder: .reversed, initialLooping: .none, playerIndex: nextPlayerIndex, controlPlaybackWithProximity: true) + strongSelf.voiceMediaPlayer = voiceMediaPlayer + voiceMediaPlayer.playedToEnd = { [weak voiceMediaPlayer] in + if let strongSelf = self, let voiceMediaPlayer = voiceMediaPlayer, voiceMediaPlayer === strongSelf.voiceMediaPlayer { + strongSelf.voiceMediaPlayer = nil + } + } + voiceMediaPlayer.control(.playback(.play)) + } else { + strongSelf.voiceMediaPlayer = nil + } + case .music: + strongSelf.musicMediaPlayer?.stop() + strongSelf.voiceMediaPlayer?.control(.playback(.pause)) + if let playlist = playlist { + strongSelf.musicMediaPlayer = SharedMediaPlayer(mediaManager: strongSelf, inForeground: strongSelf.inForeground, postbox: strongSelf.postbox, audioSession: strongSelf.audioSession, overlayMediaManager: strongSelf.overlayMediaManager, playlist: playlist, initialOrder: settings.order, initialLooping: settings.looping, playerIndex: nextPlayerIndex, controlPlaybackWithProximity: false) + strongSelf.musicMediaPlayer?.control(.playback(.play)) + } else { + strongSelf.musicMediaPlayer = nil + } + } + } + }), forKey: type) + } + + func playlistControl(_ control: SharedMediaPlayerControlAction, type: MediaManagerPlayerType? = nil) { + assert(Queue.mainQueue().isCurrent()) + let selectedType: MediaManagerPlayerType + if let type = type { + selectedType = type + } else if self.voiceMediaPlayer != nil { + selectedType = .voice + } else { + selectedType = .music + } + switch selectedType { + case .voice: + self.voiceMediaPlayer?.control(control) + case .music: + if self.voiceMediaPlayer != nil { + switch control { + case .playback(.play), .playback(.togglePlayPause): + self.setPlaylist(nil, type: .voice) + default: + break + } + } + self.musicMediaPlayer?.control(control) + } + } + func setPlaylistPlayer(_ player: ManagedAudioPlaylistPlayer?) { var disposePlayer: ManagedAudioPlaylistPlayer? var updatedPlayer = false @@ -393,6 +641,26 @@ public final class MediaManager: NSObject { } } + func filteredPlaylistState(playlistId: SharedMediaPlaylistId, itemId: SharedMediaPlaylistItemId, type: MediaManagerPlayerType) -> Signal { + let signal: Signal + switch type { + case .voice: + signal = self.voiceMediaPlayerState + case .music: + signal = self.musicMediaPlayerState + } + return signal |> map { state in + if let state = state { + if state.playlistId.isEqual(to: playlistId) && state.item.id.isEqual(to: itemId) { + return state + } + } + return nil + } |> distinctUntilChanged(isEqual: { lhs, rhs in + return lhs == rhs + }) + } + func filteredPlaylistPlayerStateAndStatus(playlistId: AudioPlaylistId, itemId: AudioPlaylistItemId) -> Signal { return self.playlistPlayerStateAndStatusValue.get() |> map { state -> AudioPlaylistStateAndStatus? in @@ -404,23 +672,23 @@ public final class MediaManager: NSObject { } @objc func playCommandEvent(_ command: AnyObject) { - self.playlistPlayerControl(.playback(.play)) + self.playlistControl(.playback(.play)) } @objc func pauseCommandEvent(_ command: AnyObject) { - self.playlistPlayerControl(.playback(.pause)) + self.playlistControl(.playback(.pause)) } @objc func previousTrackCommandEvent(_ command: AnyObject) { - self.playlistPlayerControl(.navigation(.previous)) + self.playlistControl(.previous) } @objc func nextTrackCommandEvent(_ command: AnyObject) { - self.playlistPlayerControl(.navigation(.next)) + self.playlistControl(.next) } @objc func togglePlayPauseCommandEvent(_ command: AnyObject) { - self.playlistPlayerControl(.playback(.togglePlayPause)) + self.playlistControl(.playback(.togglePlayPause)) } func setOverlayVideoNode(_ node: OverlayMediaItemNode?) { diff --git a/TelegramUI/MediaNavigationAccessoryContainerNode.swift b/TelegramUI/MediaNavigationAccessoryContainerNode.swift index bdf28b576e..9598c94f29 100644 --- a/TelegramUI/MediaNavigationAccessoryContainerNode.swift +++ b/TelegramUI/MediaNavigationAccessoryContainerNode.swift @@ -6,17 +6,8 @@ import TelegramCore final class MediaNavigationAccessoryContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { let backgroundNode: ASDisplayNode let headerNode: MediaNavigationAccessoryHeaderNode - let itemListNode: MediaNavigationAccessoryItemListNode - private var currentHeaderHeight: CGFloat = MediaNavigationAccessoryHeaderNode.minimizedHeight - private var draggingHeaderHeight: CGFloat? - private var effectiveHeaderHeight: CGFloat { - if let draggingHeaderHeight = self.draggingHeaderHeight { - return draggingHeaderHeight - } else { - return self.currentHeaderHeight - } - } + private let currentHeaderHeight: CGFloat = MediaNavigationAccessoryHeaderNode.minimizedHeight private var presentationData: PresentationData @@ -25,117 +16,31 @@ final class MediaNavigationAccessoryContainerNode: ASDisplayNode, UIGestureRecog self.backgroundNode = ASDisplayNode() self.headerNode = MediaNavigationAccessoryHeaderNode(theme: self.presentationData.theme, strings: self.presentationData.strings) - self.itemListNode = MediaNavigationAccessoryItemListNode(account: account) super.init() self.backgroundNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.backgroundColor self.addSubnode(self.backgroundNode) - self.addSubnode(self.itemListNode) self.addSubnode(self.headerNode) - self.headerNode.expand = { [weak self] in - if let strongSelf = self, strongSelf.draggingHeaderHeight == nil { - let middleHeight = MediaNavigationAccessoryHeaderNode.maximizedHeight + MediaNavigationAccessoryItemListNode.minimizedPanelHeight - strongSelf.currentHeaderHeight = middleHeight - strongSelf.updateLayout(size: strongSelf.bounds.size, transition: .animated(duration: 0.3, curve: .spring)) + self.headerNode.tapAction = { [weak self] in + if let strongSelf = self { + } } - - self.itemListNode.collapse = { [weak self] in - if let strongSelf = self, strongSelf.draggingHeaderHeight == nil { - let middleHeight = MediaNavigationAccessoryHeaderNode.maximizedHeight + MediaNavigationAccessoryItemListNode.minimizedPanelHeight - if middleHeight.isLess(than: strongSelf.currentHeaderHeight) { - strongSelf.currentHeaderHeight = middleHeight - } else { - strongSelf.currentHeaderHeight = strongSelf.bounds.size.height - } - strongSelf.updateLayout(size: strongSelf.bounds.size, transition: .animated(duration: 0.3, curve: .spring)) - } - } - } - - override func didLoad() { - super.didLoad() - - let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))) - panRecognizer.cancelsTouchesInView = true - panRecognizer.delegate = self - self.view.addGestureRecognizer(panRecognizer) } func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { - transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: self.effectiveHeaderHeight))) + transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: self.currentHeaderHeight))) - let headerHeight = max(MediaNavigationAccessoryHeaderNode.minimizedHeight, min(MediaNavigationAccessoryHeaderNode.maximizedHeight, self.effectiveHeaderHeight)) + let headerHeight = self.currentHeaderHeight transition.updateFrame(node: self.headerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: headerHeight))) self.headerNode.updateLayout(size: CGSize(width: size.width, height: headerHeight), transition: transition) - - let itemListHeight = max(0.0, self.effectiveHeaderHeight - headerHeight) - transition.updateFrame(node: self.itemListNode, frame: CGRect(origin: CGPoint(x: 0.0, y: headerHeight), size: CGSize(width: size.width, height: itemListHeight))) - self.itemListNode.updateLayout(size: CGSize(width: size.width, height: itemListHeight), maximizedHeight: max(10.0, size.height - MediaNavigationAccessoryHeaderNode.maximizedHeight), transition: transition) - } - - func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { - if let result = self.hitTest(touch.location(in: self.view), with: nil) { - if result.disablesInteractiveTransitionGestureRecognizer { - return false - } - } - return true - } - - @objc func panGesture(_ recognizer: UIPanGestureRecognizer) { - let middleHeight = MediaNavigationAccessoryHeaderNode.maximizedHeight + MediaNavigationAccessoryItemListNode.minimizedPanelHeight - switch recognizer.state { - case .began: - self.draggingHeaderHeight = self.currentHeaderHeight - case .changed: - if let _ = self.draggingHeaderHeight { - let translation = recognizer.translation(in: self.view).y - self.draggingHeaderHeight = max(MediaNavigationAccessoryHeaderNode.minimizedHeight, self.currentHeaderHeight + translation) - self.updateLayout(size: self.bounds.size, transition: .immediate) - } - case .ended: - if let draggingHeaderHeight = self.draggingHeaderHeight { - self.draggingHeaderHeight = nil - let velocity = recognizer.velocity(in: self.view).y - if abs(velocity) > 500.0 { - if draggingHeaderHeight <= middleHeight { - if velocity < 0.0 { - self.currentHeaderHeight = MediaNavigationAccessoryHeaderNode.minimizedHeight - } else { - self.currentHeaderHeight = middleHeight - } - } else { - if velocity < 0.0 { - self.currentHeaderHeight = middleHeight - } else { - self.currentHeaderHeight = self.bounds.size.height - } - } - } else { - if draggingHeaderHeight < MediaNavigationAccessoryHeaderNode.maximizedHeight * 2.0 / 3.0 { - self.currentHeaderHeight = MediaNavigationAccessoryHeaderNode.minimizedHeight - } else if draggingHeaderHeight <= middleHeight + 100.0 { - self.currentHeaderHeight = middleHeight - } else { - self.currentHeaderHeight = self.bounds.size.height - } - } - self.updateLayout(size: self.bounds.size, transition: .animated(duration: 0.3, curve: .spring)) - } - case .cancelled: - self.draggingHeaderHeight = nil - self.updateLayout(size: self.bounds.size, transition: .animated(duration: 0.3, curve: .spring)) - default: - break - } } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - if !self.headerNode.frame.contains(point) && !self.itemListNode.frame.contains(point) { + if !self.headerNode.frame.contains(point) { return nil } return super.hitTest(point, with: event) diff --git a/TelegramUI/MediaNavigationAccessoryHeaderNode.swift b/TelegramUI/MediaNavigationAccessoryHeaderNode.swift index 00d7c24720..33b646abad 100644 --- a/TelegramUI/MediaNavigationAccessoryHeaderNode.swift +++ b/TelegramUI/MediaNavigationAccessoryHeaderNode.swift @@ -1,6 +1,7 @@ import Foundation import AsyncDisplayKit import Display +import SwiftSignalKit private let titleFont = Font.regular(12.0) private let subtitleFont = Font.regular(10.0) @@ -9,52 +10,44 @@ private let maximizedSubtitleFont = Font.regular(12.0) final class MediaNavigationAccessoryHeaderNode: ASDisplayNode { static let minimizedHeight: CGFloat = 37.0 - static let maximizedHeight: CGFloat = 166.0 private var theme: PresentationTheme private var strings: PresentationStrings private let titleNode: TextNode private let subtitleNode: TextNode - private let maximizedTitleNode: TextNode - private let maximizedSubtitleNode: TextNode private let closeButton: HighlightableButtonNode private let actionButton: HighlightTrackingButtonNode private let actionPauseNode: ASImageNode private let actionPlayNode: ASImageNode - private let maximizedLeftTimestampNode: MediaPlayerTimeTextNode - private let maximizedRightTimestampNode: MediaPlayerTimeTextNode - private let maximizedActionButton: HighlightableButtonNode - private let maximizedActionPauseNode: ASImageNode - private let maximizedActionPlayNode: ASImageNode - private let maximizedPreviousButton: HighlightableButtonNode - private let maximizedNextButton: HighlightableButtonNode - private let maximizedShuffleButton: HighlightableButtonNode - private let maximizedRepeatButton: HighlightableButtonNode - private let scrubbingNode: MediaPlayerScrubbingNode - private let maximizedScrubbingNode: MediaPlayerScrubbingNode + + var displayScrubber: Bool = true { + didSet { + self.scrubbingNode.isHidden = !self.displayScrubber + } + } + + private let separatorNode: ASDisplayNode private var tapRecognizer: UITapGestureRecognizer? - var expand: (() -> Void)? - + var tapAction: (() -> Void)? var close: (() -> Void)? var togglePlayPause: (() -> Void)? - var previous: (() -> Void)? - var next: (() -> Void)? - var seek: ((Double) -> Void)? - var stateAndStatus: AudioPlaylistStateAndStatus? { + var playbackStatus: Signal? { didSet { - if self.stateAndStatus != oldValue { + self.scrubbingNode.status = self.playbackStatus + } + } + + var playbackItem: SharedMediaPlaylistItem? { + didSet { + if !arePlaylistItemsEqual(self.playbackItem, oldValue) { self.updateLayout(size: self.bounds.size, transition: .immediate) - self.scrubbingNode.status = stateAndStatus?.status - self.maximizedScrubbingNode.status = stateAndStatus?.status - self.maximizedLeftTimestampNode.status = stateAndStatus?.status - self.maximizedRightTimestampNode.status = stateAndStatus?.status } } } @@ -68,11 +61,6 @@ final class MediaNavigationAccessoryHeaderNode: ASDisplayNode { self.subtitleNode = TextNode() self.subtitleNode.isLayerBacked = true - self.maximizedTitleNode = TextNode() - self.maximizedTitleNode.isLayerBacked = true - self.maximizedSubtitleNode = TextNode() - self.maximizedSubtitleNode.isLayerBacked = true - self.closeButton = HighlightableButtonNode() self.closeButton.setImage(PresentationResourcesRootController.navigationPlayerCloseButton(self.theme), for: []) self.closeButton.hitTestSlop = UIEdgeInsetsMake(-8.0, -8.0, -8.0, -8.0) @@ -97,60 +85,11 @@ final class MediaNavigationAccessoryHeaderNode: ASDisplayNode { self.actionPlayNode.image = PresentationResourcesRootController.navigationPlayerPlayIcon(self.theme) self.actionPlayNode.isHidden = true - self.maximizedLeftTimestampNode = MediaPlayerTimeTextNode(textColor: self.theme.rootController.navigationBar.secondaryTextColor) - self.maximizedRightTimestampNode = MediaPlayerTimeTextNode(textColor: self.theme.rootController.navigationBar.secondaryTextColor) - self.maximizedLeftTimestampNode.alignment = .right - self.maximizedRightTimestampNode.mode = .reversed + self.scrubbingNode = MediaPlayerScrubbingNode(content: .standard(lineHeight: 2.0, lineCap: .square, scrubberHandle: .none, backgroundColor: .clear, foregroundColor: self.theme.rootController.navigationBar.accentTextColor)) - self.maximizedActionButton = HighlightableButtonNode() - self.maximizedActionButton.hitTestSlop = UIEdgeInsetsMake(-8.0, -8.0, -8.0, -8.0) - self.maximizedActionButton.displaysAsynchronously = false - - self.maximizedActionPauseNode = ASImageNode() - self.maximizedActionPauseNode.isLayerBacked = true - self.maximizedActionPauseNode.displaysAsynchronously = false - self.maximizedActionPauseNode.displayWithoutProcessing = true - self.maximizedActionPauseNode.image = PresentationResourcesRootController.navigationPlayerMaximizedPauseIcon(self.theme) - - self.maximizedActionPlayNode = ASImageNode() - self.maximizedActionPlayNode.isLayerBacked = true - self.maximizedActionPlayNode.displaysAsynchronously = false - self.maximizedActionPlayNode.displayWithoutProcessing = true - self.maximizedActionPlayNode.image = PresentationResourcesRootController.navigationPlayerMaximizedPlayIcon(self.theme) - self.maximizedActionPlayNode.isHidden = true - - let maximizedActionButtonSize = CGSize(width: 66.0, height: 50.0) - self.maximizedActionButton.frame = CGRect(origin: CGPoint(), size: maximizedActionButtonSize) - if let maximizedPauseIcon = self.maximizedActionPauseNode.image { - self.maximizedActionPauseNode.frame = CGRect(origin: CGPoint(x: floor((maximizedActionButtonSize.width - maximizedPauseIcon.size.width) / 2.0), y: floor((maximizedActionButtonSize.height - maximizedPauseIcon.size.height) / 2.0)), size: maximizedPauseIcon.size) - } - if let maximizedPlayIcon = self.maximizedActionPlayNode.image { - self.maximizedActionPlayNode.frame = CGRect(origin: CGPoint(x: floor((maximizedActionButtonSize.width - maximizedPlayIcon.size.width) / 2.0) + 2.0, y: floor((maximizedActionButtonSize.height - maximizedPlayIcon.size.height) / 2.0)), size: maximizedPlayIcon.size) - } - - self.maximizedPreviousButton = HighlightableButtonNode() - self.maximizedPreviousButton.setImage(PresentationResourcesRootController.navigationPlayerMaximizedPreviousIcon(self.theme), for: []) - self.maximizedPreviousButton.hitTestSlop = UIEdgeInsetsMake(-8.0, -8.0, -8.0, -8.0) - self.maximizedPreviousButton.displaysAsynchronously = false - - self.maximizedNextButton = HighlightableButtonNode() - self.maximizedNextButton.setImage(PresentationResourcesRootController.navigationPlayerMaximizedNextIcon(self.theme), for: []) - self.maximizedNextButton.hitTestSlop = UIEdgeInsetsMake(-8.0, -8.0, -8.0, -8.0) - self.maximizedNextButton.displaysAsynchronously = false - - self.maximizedShuffleButton = HighlightableButtonNode() - self.maximizedShuffleButton.setImage(PresentationResourcesRootController.navigationPlayerMaximizedShuffleIcon(self.theme), for: []) - self.maximizedShuffleButton.hitTestSlop = UIEdgeInsetsMake(-8.0, -8.0, -8.0, -8.0) - self.maximizedShuffleButton.displaysAsynchronously = false - - self.maximizedRepeatButton = HighlightableButtonNode() - self.maximizedRepeatButton.setImage(PresentationResourcesRootController.navigationPlayerMaximizedRepeatIcon(self.theme), for: []) - self.maximizedRepeatButton.hitTestSlop = UIEdgeInsetsMake(-8.0, -8.0, -8.0, -8.0) - self.maximizedRepeatButton.displaysAsynchronously = false - - self.scrubbingNode = MediaPlayerScrubbingNode(lineHeight: 2.0, lineCap: .square, scrubberHandle: false, backgroundColor: .clear, foregroundColor: self.theme.rootController.navigationBar.accentTextColor) - self.maximizedScrubbingNode = MediaPlayerScrubbingNode(lineHeight: 3.0, lineCap: .round, scrubberHandle: true, backgroundColor: self.theme.rootController.navigationBar.secondaryTextColor, foregroundColor: self.theme.rootController.navigationBar.accentTextColor) - + self.separatorNode = ASDisplayNode() + self.separatorNode.isLayerBacked = true + self.separatorNode.backgroundColor = theme.rootController.navigationBar.separatorColor super.init() @@ -158,8 +97,6 @@ final class MediaNavigationAccessoryHeaderNode: ASDisplayNode { self.addSubnode(self.titleNode) self.addSubnode(self.subtitleNode) - self.addSubnode(self.maximizedTitleNode) - self.addSubnode(self.maximizedSubtitleNode) self.addSubnode(self.closeButton) @@ -167,28 +104,13 @@ final class MediaNavigationAccessoryHeaderNode: ASDisplayNode { self.actionButton.addSubnode(self.actionPlayNode) self.addSubnode(self.actionButton) - self.addSubnode(self.maximizedLeftTimestampNode) - self.addSubnode(self.maximizedRightTimestampNode) - - self.maximizedActionButton.addSubnode(self.maximizedActionPauseNode) - self.maximizedActionButton.addSubnode(self.maximizedActionPlayNode) - self.addSubnode(self.maximizedActionButton) - self.addSubnode(self.maximizedPreviousButton) - self.addSubnode(self.maximizedNextButton) - self.addSubnode(self.maximizedShuffleButton) - self.addSubnode(self.maximizedRepeatButton) - self.closeButton.addTarget(self, action: #selector(self.closeButtonPressed), forControlEvents: .touchUpInside) self.actionButton.addTarget(self, action: #selector(self.actionButtonPressed), forControlEvents: .touchUpInside) - self.maximizedActionButton.addTarget(self, action: #selector(self.actionButtonPressed), forControlEvents: .touchUpInside) - self.maximizedPreviousButton.addTarget(self, action: #selector(self.previousButtonPressed), forControlEvents: .touchUpInside) - self.maximizedNextButton.addTarget(self, action: #selector(self.nextButtonPressed), forControlEvents: .touchUpInside) - self.maximizedShuffleButton.addTarget(self, action: #selector(self.shuffleButtonPressed), forControlEvents: .touchUpInside) - self.maximizedRepeatButton.addTarget(self, action: #selector(self.repeatButtonPressed), forControlEvents: .touchUpInside) - self.addSubnode(self.maximizedScrubbingNode) self.addSubnode(self.scrubbingNode) + self.addSubnode(self.separatorNode) + self.actionButton.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { if highlighted { @@ -208,7 +130,7 @@ final class MediaNavigationAccessoryHeaderNode: ASDisplayNode { switch status { case .paused: paused = true - case let .buffering(whilePlaying): + case let .buffering(_, whilePlaying): paused = !whilePlaying case .playing: paused = false @@ -218,14 +140,8 @@ final class MediaNavigationAccessoryHeaderNode: ASDisplayNode { } strongSelf.actionPlayNode.isHidden = !paused strongSelf.actionPauseNode.isHidden = paused - strongSelf.maximizedActionPlayNode.isHidden = !paused - strongSelf.maximizedActionPauseNode.isHidden = paused } } - - self.maximizedScrubbingNode.seek = { [weak self] timestamp in - self?.seek?(timestamp) - } } override func didLoad() { @@ -238,157 +154,74 @@ final class MediaNavigationAccessoryHeaderNode: ASDisplayNode { func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { let minHeight = MediaNavigationAccessoryHeaderNode.minimizedHeight - let maxHeight = MediaNavigationAccessoryHeaderNode.maximizedHeight - let maximizationFactor = (size.height - minHeight) / (maxHeight - minHeight) - - let enableExpandTap = maximizationFactor.isEqual(to: 0.0) - if let tapRecognizer = self.tapRecognizer, tapRecognizer.isEnabled != enableExpandTap { - tapRecognizer.isEnabled = enableExpandTap - } var titleString: NSAttributedString? var subtitleString: NSAttributedString? - var maximizedTitleString: NSAttributedString? - var maximizedSubtitleString: NSAttributedString? - if let stateAndStatus = self.stateAndStatus, let item = stateAndStatus.state.item, let info = item.info { - switch info.labelInfo { - case let .music(title, performer): + if let playbackItem = self.playbackItem, let displayData = playbackItem.displayData { + switch displayData { + case let .music(title, performer, _): let titleText: String = title ?? "Unknown Track" let subtitleText: String = performer ?? "Unknown Artist" titleString = NSAttributedString(string: titleText, font: titleFont, textColor: self.theme.rootController.navigationBar.primaryTextColor) subtitleString = NSAttributedString(string: subtitleText, font: subtitleFont, textColor: self.theme.rootController.navigationBar.secondaryTextColor) - - maximizedTitleString = NSAttributedString(string: titleText, font: maximizedTitleFont, textColor: self.theme.rootController.navigationBar.primaryTextColor) - maximizedSubtitleString = NSAttributedString(string: subtitleText, font: maximizedSubtitleFont, textColor: self.theme.rootController.navigationBar.secondaryTextColor) - case .voice: - let titleText: String = self.strings.Message_Audio - titleString = NSAttributedString(string: titleText, font: titleFont, textColor: self.theme.rootController.navigationBar.primaryTextColor) - - maximizedTitleString = NSAttributedString(string: titleText, font: maximizedTitleFont, textColor: self.theme.rootController.navigationBar.primaryTextColor) - case .video: - let titleText: String = self.strings.Message_VideoMessage - titleString = NSAttributedString(string: titleText, font: titleFont, textColor: self.theme.rootController.navigationBar.primaryTextColor) + case let .voice(author, peer): + let titleText: String = author?.displayTitle ?? "" + let subtitleText: String + if author?.id == peer?.id { + subtitleText = self.strings.MusicPlayer_VoiceNote + } else { + subtitleText = peer?.displayTitle ?? "" + } - maximizedTitleString = NSAttributedString(string: titleText, font: maximizedTitleFont, textColor: self.theme.rootController.navigationBar.primaryTextColor) + titleString = NSAttributedString(string: titleText, font: titleFont, textColor: self.theme.rootController.navigationBar.primaryTextColor) + subtitleString = NSAttributedString(string: subtitleText, font: subtitleFont, textColor: self.theme.rootController.navigationBar.secondaryTextColor) + case let .instantVideo(author, peer): + let titleText: String = author?.displayTitle ?? "" + let subtitleText: String = peer?.displayTitle ?? "" + + titleString = NSAttributedString(string: titleText, font: titleFont, textColor: self.theme.rootController.navigationBar.primaryTextColor) + subtitleString = NSAttributedString(string: subtitleText, font: subtitleFont, textColor: self.theme.rootController.navigationBar.secondaryTextColor) } } let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeSubtitleLayout = TextNode.asyncLayout(self.subtitleNode) - let makeMaximizedTitleLayout = TextNode.asyncLayout(self.maximizedTitleNode) - let makeMaximizedSubtitleLayout = TextNode.asyncLayout(self.maximizedSubtitleNode) - let (titleLayout, titleApply) = makeTitleLayout(titleString, nil, 1, .middle, CGSize(width: size.width - 80.0, height: 100.0), .natural, nil, UIEdgeInsets()) - let (subtitleLayout, subtitleApply) = makeSubtitleLayout(subtitleString, nil, 1, .middle, CGSize(width: size.width - 80.0, height: 100.0), .natural, nil, UIEdgeInsets()) - - let (maximizedTitleLayout, maximizedTitleApply) = makeMaximizedTitleLayout(maximizedTitleString, nil, 1, .middle, CGSize(width: size.width - 80.0, height: 100.0), .natural, nil, UIEdgeInsets()) - let (maximizedSubtitleLayout, maximizedSubtitleApply) = makeMaximizedSubtitleLayout(maximizedSubtitleString, nil, 1, .middle, CGSize(width: size.width - 80.0, height: 100.0), .natural, nil, UIEdgeInsets()) + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: CGSize(width: size.width - 80.0, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: subtitleString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: CGSize(width: size.width - 80.0, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let _ = titleApply() let _ = subtitleApply() - let _ = maximizedTitleApply() - let _ = maximizedSubtitleApply() let minimizedTitleOffset: CGFloat = subtitleString == nil ? 6.0 : 0.0 - let maximizedTitleOffset: CGFloat = subtitleString == nil ? 12.0 : 0.0 let minimizedTitleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleLayout.size.width) / 2.0), y: 4.0 + minimizedTitleOffset), size: titleLayout.size) let minimizedSubtitleFrame = CGRect(origin: CGPoint(x: floor((size.width - subtitleLayout.size.width) / 2.0), y: 20.0), size: subtitleLayout.size) - let maximizedTitleFrame = CGRect(origin: CGPoint(x: floor((size.width - maximizedTitleLayout.size.width) / 2.0), y: 57.0 + maximizedTitleOffset), size: maximizedTitleLayout.size) - let maximizedSubtitleFrame = CGRect(origin: CGPoint(x: floor((size.width - maximizedSubtitleLayout.size.width) / 2.0), y: 80.0), size: maximizedSubtitleLayout.size) - - let maximizedTitleDistance = maximizedTitleFrame.midY - minimizedTitleFrame.midY - let maximizedSubtitleDistance = maximizedSubtitleFrame.midY - minimizedSubtitleFrame.midY - - var updatedMinimizedTitleFrame = minimizedTitleFrame.offsetBy(dx: 0.0, dy: maximizedTitleDistance * maximizationFactor) - var updatedMaximizedTitleFrame = maximizedTitleFrame.offsetBy(dx: 0.0, dy: -maximizedTitleDistance * (1.0 - maximizationFactor)) - - transition.updateFrame(node: self.titleNode, frame: updatedMinimizedTitleFrame) - transition.updateFrame(node: self.subtitleNode, frame: minimizedSubtitleFrame.offsetBy(dx: 0.0, dy: maximizedSubtitleDistance * maximizationFactor)) - - updatedMinimizedTitleFrame.origin.y -= minimizedTitleOffset - updatedMaximizedTitleFrame.origin.y -= maximizedTitleOffset - - transition.updateFrame(node: self.maximizedTitleNode, frame: updatedMaximizedTitleFrame) - transition.updateFrame(node: self.maximizedSubtitleNode, frame: maximizedSubtitleFrame.offsetBy(dx: 0.0, dy: -maximizedSubtitleDistance * (1.0 - maximizationFactor))) + transition.updateFrame(node: self.titleNode, frame: minimizedTitleFrame) + transition.updateFrame(node: self.subtitleNode, frame: minimizedSubtitleFrame) let closeButtonSize = self.closeButton.measure(CGSize(width: 100.0, height: 100.0)) - transition.updateFrame(node: self.closeButton, frame: CGRect(origin: CGPoint(x: bounds.size.width - 18.0 - closeButtonSize.width, y: updatedMinimizedTitleFrame.minY + 8.0), size: closeButtonSize)) + transition.updateFrame(node: self.closeButton, frame: CGRect(origin: CGPoint(x: bounds.size.width - 18.0 - closeButtonSize.width, y: minimizedTitleFrame.minY + 8.0), size: closeButtonSize)) transition.updateFrame(node: self.actionPlayNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 40.0, height: 37.0))) transition.updateFrame(node: self.actionPauseNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 40.0, height: 37.0))) - transition.updateFrame(node: self.actionButton, frame: CGRect(origin: CGPoint(x: 0.0, y: updatedMinimizedTitleFrame.minY - 4.0), size: CGSize(width: 40.0, height: 37.0))) - transition.updateFrame(node: self.scrubbingNode, frame: CGRect(origin: CGPoint(x: 0.0, y: (37.0 + (maxHeight - minHeight) * maximizationFactor) - 2.0), size: CGSize(width: size.width, height: 2.0))) - transition.updateFrame(node: self.maximizedScrubbingNode, frame: CGRect(origin: CGPoint(x: 57.0, y: updatedMaximizedTitleFrame.minY - 38.0), size: CGSize(width: size.width - 114.0, height: 15.0))) - - transition.updateFrame(node: self.maximizedLeftTimestampNode, frame: CGRect(origin: CGPoint(x: 0.0, y: updatedMaximizedTitleFrame.minY - 39.0), size: CGSize(width: 57.0 - 13.0, height: 20.0))) - transition.updateFrame(node: self.maximizedRightTimestampNode, frame: CGRect(origin: CGPoint(x: size.width - 57.0 + 13.0, y: updatedMaximizedTitleFrame.minY - 39.0), size: CGSize(width: 57.0 - 13.0, height: 20.0))) - - let maximizedActionButtonSize = self.maximizedActionButton.bounds.size - let maximizedActionButtonFrame = CGRect(origin: CGPoint(x: floor((size.width - maximizedActionButtonSize.width) / 2.0), y: updatedMaximizedTitleFrame.maxY + 26.0), size: maximizedActionButtonSize) - transition.updateFrame(node: self.maximizedActionButton, frame: maximizedActionButtonFrame) - - let actionButtonSpacing: CGFloat = 10.0 - transition.updateFrame(node: self.maximizedPreviousButton, frame: CGRect(origin: CGPoint(x: maximizedActionButtonFrame.minX - maximizedActionButtonSize.width - actionButtonSpacing, y: maximizedActionButtonFrame.minY), size: maximizedActionButtonSize)) - transition.updateFrame(node: self.maximizedNextButton, frame: CGRect(origin: CGPoint(x: maximizedActionButtonFrame.maxX + actionButtonSpacing, y: maximizedActionButtonFrame.minY), size: maximizedActionButtonSize)) - transition.updateFrame(node: self.maximizedShuffleButton, frame: CGRect(origin: CGPoint(x: 0.0, y: maximizedActionButtonFrame.minY), size: CGSize(width: 56.0, height: 50.0))) - transition.updateFrame(node: self.maximizedRepeatButton, frame: CGRect(origin: CGPoint(x: size.width - 56.0, y: maximizedActionButtonFrame.minY), size: CGSize(width: 56.0, height: 50.0))) - - transition.updateAlpha(node: self.actionButton, alpha: 1.0 - maximizationFactor) - transition.updateAlpha(node: self.closeButton, alpha: 1.0 - maximizationFactor) - - transition.updateAlpha(node: self.maximizedActionButton, alpha: maximizationFactor) - transition.updateAlpha(node: self.maximizedPreviousButton, alpha: maximizationFactor) - transition.updateAlpha(node: self.maximizedNextButton, alpha: maximizationFactor) - transition.updateAlpha(node: self.maximizedPreviousButton, alpha: maximizationFactor) - transition.updateAlpha(node: self.maximizedShuffleButton, alpha: maximizationFactor) - transition.updateAlpha(node: self.maximizedRepeatButton, alpha: maximizationFactor) - - transition.updateAlpha(node: self.titleNode, alpha: 1.0 - maximizationFactor) - transition.updateAlpha(node: self.subtitleNode, alpha: 1.0 - maximizationFactor) - transition.updateAlpha(node: self.scrubbingNode, alpha: 1.0 - maximizationFactor) - transition.updateAlpha(node: self.maximizedScrubbingNode, alpha: maximizationFactor) - transition.updateAlpha(node: self.maximizedTitleNode, alpha: maximizationFactor) - transition.updateAlpha(node: self.maximizedSubtitleNode, alpha: maximizationFactor) - transition.updateAlpha(node: self.maximizedLeftTimestampNode, alpha: maximizationFactor) - transition.updateAlpha(node: self.maximizedRightTimestampNode, alpha: maximizationFactor) + transition.updateFrame(node: self.actionButton, frame: CGRect(origin: CGPoint(x: 0.0, y: minimizedTitleFrame.minY - 4.0), size: CGSize(width: 40.0, height: 37.0))) + transition.updateFrame(node: self.scrubbingNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 37.0 - 2.0), size: CGSize(width: size.width, height: 2.0))) + + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: minHeight - UIScreenPixel), size: CGSize(width: size.width, height: UIScreenPixel))) } @objc func closeButtonPressed() { - if let close = self.close { - close() - } + self.close?() } @objc func actionButtonPressed() { - if let togglePlayPause = self.togglePlayPause { - togglePlayPause() - } - } - - @objc func previousButtonPressed() { - if let previous = self.previous { - previous() - } - } - - @objc func nextButtonPressed() { - if let next = self.next { - next() - } - } - - @objc func shuffleButtonPressed() { - - } - - @objc func repeatButtonPressed() { - + self.togglePlayPause?() } @objc func tapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { - self.expand?() + self.tapAction?() } } } diff --git a/TelegramUI/MediaNavigationAccessoryItemListNode.swift b/TelegramUI/MediaNavigationAccessoryItemListNode.swift index 9594c1ba13..e2497cd414 100644 --- a/TelegramUI/MediaNavigationAccessoryItemListNode.swift +++ b/TelegramUI/MediaNavigationAccessoryItemListNode.swift @@ -64,10 +64,11 @@ final class MediaNavigationAccessoryItemListNode: ASDisplayNode { } } } - }, openSecretMessagePreview: { _ in }, closeSecretMessagePreview: { }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _ in }, navigateToMessage: { _, _ in }, clickThroughMessage: { }, toggleMessageSelection: { _ in }, sendMessage: { _ in }, sendSticker: { _ in }, sendGif: { _ in }, requestMessageActionCallback: { _, _, _ in }, openUrl: { _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, openMessageShareMenu: { _ in - }, presentController: { _, _ in }, callPeer: { _ in }, longTap: { _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, automaticMediaDownloadSettings: .none) + return false + }, openSecretMessagePreview: { _ in }, closeSecretMessagePreview: { }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _ in }, navigateToMessage: { _, _ in }, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendMessage: { _ in }, sendSticker: { _ in }, sendGif: { _ in }, requestMessageActionCallback: { _, _, _ in }, openUrl: { _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, openMessageShareMenu: { _ in + }, presentController: { _, _ in }, callPeer: { _ in }, longTap: { _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in }, canSetupReply: { return false }, automaticMediaDownloadSettings: .none) - let listNode = ChatHistoryListNode(account: account, peerId: updatedPlaylistPeerId, tagMask: .music, messageId: nil, controllerInteraction: controllerInteraction, mode: .list) + let listNode = ChatHistoryListNode(account: account, chatLocation: .peer(updatedPlaylistPeerId), tagMask: .music, messageId: nil, controllerInteraction: controllerInteraction, selectedMessages: .single(nil), mode: .list(search: false, reversed: false)) listNode.preloadPages = true self.listNode = listNode self.contentNode.addSubnode(listNode) @@ -90,7 +91,7 @@ final class MediaNavigationAccessoryItemListNode: ASDisplayNode { if let foundItemNode = foundItemNode { listNode.ensureItemNodeVisible(foundItemNode) } else if let message = listNode.messageInCurrentHistoryView(updatedPlaylistMessageId) { - listNode.scrollToMessage(from: MessageIndex(message), to: MessageIndex(message)) + listNode.scrollToMessage(from: MessageIndex(message), to: MessageIndex(message), animated: true) } } } diff --git a/TelegramUI/MediaNavigationAccessoryPanel.swift b/TelegramUI/MediaNavigationAccessoryPanel.swift index f7f15e58b1..e98e200f12 100644 --- a/TelegramUI/MediaNavigationAccessoryPanel.swift +++ b/TelegramUI/MediaNavigationAccessoryPanel.swift @@ -8,9 +8,7 @@ final class MediaNavigationAccessoryPanel: ASDisplayNode { var close: (() -> Void)? var togglePlayPause: (() -> Void)? - var previous: (() -> Void)? - var next: (() -> Void)? - var seek: ((Double) -> Void)? + var tapAction: (() -> Void)? init(account: Account) { self.containerNode = MediaNavigationAccessoryContainerNode(account: account) @@ -29,20 +27,9 @@ final class MediaNavigationAccessoryPanel: ASDisplayNode { togglePlayPause() } } - containerNode.headerNode.previous = { [weak self] in - if let strongSelf = self, let previous = strongSelf.previous { - previous() - } - } - containerNode.headerNode.next = { [weak self] in - if let strongSelf = self, let next = strongSelf.next { - next() - } - } - - containerNode.headerNode.seek = { [weak self] timestamp in - if let strongSelf = self, let seek = strongSelf.seek { - seek(timestamp) + containerNode.headerNode.tapAction = { [weak self] in + if let strongSelf = self, let tapAction = strongSelf.tapAction { + tapAction() } } } diff --git a/TelegramUI/MediaPlaybackData.swift b/TelegramUI/MediaPlaybackData.swift index 3b0f44758f..687daac556 100644 --- a/TelegramUI/MediaPlaybackData.swift +++ b/TelegramUI/MediaPlaybackData.swift @@ -1,6 +1,12 @@ import Foundation +import SwiftSignalKit -struct MediaPlaybackBuffers { +final class MediaPlaybackBuffers { let audioBuffer: MediaTrackFrameBuffer? let videoBuffer: MediaTrackFrameBuffer? + + init(audioBuffer: MediaTrackFrameBuffer?, videoBuffer: MediaTrackFrameBuffer?) { + self.audioBuffer = audioBuffer + self.videoBuffer = videoBuffer + } } diff --git a/TelegramUI/MediaPlayer.swift b/TelegramUI/MediaPlayer.swift index 2c2e8338e5..773dc00370 100644 --- a/TelegramUI/MediaPlayer.swift +++ b/TelegramUI/MediaPlayer.swift @@ -17,26 +17,42 @@ private enum MediaPlayerPlaybackAction { case pause } -private struct MediaPlayerLoadedState { - fileprivate let frameSource: MediaFrameSource - fileprivate let mediaBuffers: MediaPlaybackBuffers - fileprivate let controlTimebase: MediaPlayerControlTimebase +private final class MediaPlayerLoadedState { + let frameSource: MediaFrameSource + let mediaBuffers: MediaPlaybackBuffers + let controlTimebase: MediaPlayerControlTimebase + var lostAudioSession: Bool = false + + init(frameSource: MediaFrameSource, mediaBuffers: MediaPlaybackBuffers, controlTimebase: MediaPlayerControlTimebase) { + self.frameSource = frameSource + self.mediaBuffers = mediaBuffers + self.controlTimebase = controlTimebase + } } private enum MediaPlayerState { case empty - case seeking(frameSource: MediaFrameSource, timestamp: Double, disposable: Disposable, action: MediaPlayerPlaybackAction) + case seeking(frameSource: MediaFrameSource, timestamp: Double, disposable: Disposable, action: MediaPlayerPlaybackAction, enableSound: Bool) case paused(MediaPlayerLoadedState) case playing(MediaPlayerLoadedState) } enum MediaPlayerActionAtEnd { - case loop + case loop((() -> Void)?) case action(() -> Void) case loopDisablingSound(() -> Void) case stop } +private final class MediaPlayerAudioRendererContext { + let renderer: MediaPlayerAudioRenderer + var requestedFrames = false + + init(renderer: MediaPlayerAudioRenderer) { + self.renderer = renderer + } +} + private final class MediaPlayerContext { private let queue: Queue private let audioSessionManager: ManagedAudioSession @@ -47,9 +63,14 @@ private final class MediaPlayerContext { private let video: Bool private let preferSoftwareDecoding: Bool private var enableSound: Bool + private var playAndRecord: Bool + private var keepAudioSessionWhilePaused: Bool + + private var seekId: Int = 0 private var state: MediaPlayerState = .empty - private var audioRenderer: MediaPlayerAudioRenderer? + private var audioRenderer: MediaPlayerAudioRendererContext? + private var forceAudioToSpeaker = false fileprivate let videoRenderer: VideoPlayerProxy private var tickTimer: SwiftSignalKit.Timer? @@ -61,7 +82,7 @@ private final class MediaPlayerContext { private var stoppedAtEnd = false - init(queue: Queue, audioSessionManager: ManagedAudioSession, playerStatus: ValuePromise, postbox: Postbox, resource: MediaResource, streamable: Bool, video: Bool, preferSoftwareDecoding: Bool, playAutomatically: Bool, enableSound: Bool) { + init(queue: Queue, audioSessionManager: ManagedAudioSession, playerStatus: ValuePromise, postbox: Postbox, resource: MediaResource, streamable: Bool, video: Bool, preferSoftwareDecoding: Bool, playAutomatically: Bool, enableSound: Bool, playAndRecord: Bool, keepAudioSessionWhilePaused: Bool) { assert(queue.isCurrent()) self.queue = queue @@ -73,6 +94,8 @@ private final class MediaPlayerContext { self.video = video self.preferSoftwareDecoding = preferSoftwareDecoding self.enableSound = enableSound + self.playAndRecord = playAndRecord + self.keepAudioSessionWhilePaused = keepAudioSessionWhilePaused self.videoRenderer = VideoPlayerProxy(queue: queue) @@ -91,9 +114,9 @@ private final class MediaPlayerContext { } case .playing: if !value { - strongSelf.pause() + strongSelf.pause(lostAudioSession: false) } - case let .seeking(_, _, _, action): + case let .seeking(_, _, _, action, _): switch action { case .pause: if value { @@ -101,7 +124,7 @@ private final class MediaPlayerContext { } case .play: if !value { - strongSelf.pause() + strongSelf.pause(lostAudioSession: false) } } } @@ -141,7 +164,7 @@ private final class MediaPlayerContext { self.tickTimer?.invalidate() - if case let .seeking(_, _, disposable, _) = self.state { + if case let .seeking(_, _, disposable, _, _) = self.state { disposable.dispose() } } @@ -155,7 +178,7 @@ private final class MediaPlayerContext { action = .pause case .playing: action = .play - case let .seeking(_, _, _, currentAction): + case let .seeking(_, _, _, currentAction, _): action = currentAction } self.seek(timestamp: timestamp, action: action) @@ -172,9 +195,9 @@ private final class MediaPlayerContext { loadedState = currentLoadedState case let .paused(currentLoadedState): loadedState = currentLoadedState - case let .seeking(previousFrameSource, previousTimestamp, previousDisposable, _): - if previousTimestamp.isEqual(to: timestamp) { - self.state = .seeking(frameSource: previousFrameSource, timestamp: previousTimestamp, disposable: previousDisposable, action: action) + case let .seeking(previousFrameSource, previousTimestamp, previousDisposable, _, previousEnableSound): + if previousTimestamp.isEqual(to: timestamp) && self.enableSound == previousEnableSound { + self.state = .seeking(frameSource: previousFrameSource, timestamp: previousTimestamp, disposable: previousDisposable, action: action, enableSound: self.enableSound) return } else { previousDisposable.dispose() @@ -183,42 +206,55 @@ private final class MediaPlayerContext { self.tickTimer?.invalidate() if let loadedState = loadedState { + self.seekId += 1 + if loadedState.controlTimebase.isAudio { - self.audioRenderer?.setRate(0.0) + self.audioRenderer?.renderer.setRate(0.0) } else { if !CMTimebaseGetRate(loadedState.controlTimebase.timebase).isEqual(to: 0.0) { CMTimebaseSetRate(loadedState.controlTimebase.timebase, 0.0) } } - let timestamp = CMTimeGetSeconds(CMTimebaseGetTime(loadedState.controlTimebase.timebase)) + let currentTimestamp = CMTimeGetSeconds(CMTimebaseGetTime(loadedState.controlTimebase.timebase)) var duration: Double = 0.0 var videoStatus: MediaTrackFrameBufferStatus? if let videoTrackFrameBuffer = loadedState.mediaBuffers.videoBuffer { - videoStatus = videoTrackFrameBuffer.status(at: timestamp) + videoStatus = videoTrackFrameBuffer.status(at: currentTimestamp) duration = max(duration, CMTimeGetSeconds(videoTrackFrameBuffer.duration)) } var audioStatus: MediaTrackFrameBufferStatus? if let audioTrackFrameBuffer = loadedState.mediaBuffers.audioBuffer { - audioStatus = audioTrackFrameBuffer.status(at: timestamp) + audioStatus = audioTrackFrameBuffer.status(at: currentTimestamp) duration = max(duration, CMTimeGetSeconds(audioTrackFrameBuffer.duration)) } - let status = MediaPlayerStatus(generationTimestamp: CACurrentMediaTime(), duration: duration, timestamp: min(max(timestamp, 0.0), duration), status: .buffering(whilePlaying: action == .play)) + let status = MediaPlayerStatus(generationTimestamp: CACurrentMediaTime(), duration: duration, timestamp: min(max(timestamp, 0.0), duration), seekId: self.seekId, status: .buffering(initial: false, whilePlaying: action == .play)) self.playerStatus.set(status) } else { - let status = MediaPlayerStatus(generationTimestamp: CACurrentMediaTime(), duration: 0.0, timestamp: 0.0, status: .buffering(whilePlaying: action == .play)) + let status = MediaPlayerStatus(generationTimestamp: CACurrentMediaTime(), duration: 0.0, timestamp: 0.0, seekId: self.seekId, status: .buffering(initial: false, whilePlaying: action == .play)) self.playerStatus.set(status) } let frameSource = FFMpegMediaFrameSource(queue: self.queue, postbox: self.postbox, resource: self.resource, streamable: self.streamable, video: self.video, preferSoftwareDecoding: self.preferSoftwareDecoding) let disposable = MetaDisposable() - self.state = .seeking(frameSource: frameSource, timestamp: timestamp, disposable: disposable, action: action) + self.state = .seeking(frameSource: frameSource, timestamp: timestamp, disposable: disposable, action: action, enableSound: self.enableSound) + + self.lastStatusUpdateTimestamp = nil let seekResult = frameSource.seek(timestamp: timestamp) |> deliverOn(self.queue) disposable.set(seekResult.start(next: { [weak self] seekResult in if let strongSelf = self { - strongSelf.seekingCompleted(seekResult: seekResult) + var result: MediaFrameSourceSeekResult? + seekResult.with { object in + assert(strongSelf.queue.isCurrent()) + result = object + } + if let result = result { + strongSelf.seekingCompleted(seekResult: result) + } else { + assertionFailure() + } } }, error: { _ in })) @@ -231,7 +267,7 @@ private final class MediaPlayerContext { assert(self.queue.isCurrent()) - guard case let .seeking(frameSource, _, _, action) = self.state else { + guard case let .seeking(frameSource, _, _, action, _) = self.state else { assertionFailure() return } @@ -250,28 +286,38 @@ private final class MediaPlayerContext { let controlTimebase: MediaPlayerControlTimebase if let _ = buffers.audioBuffer { - self.audioRenderer?.stop() - self.audioRenderer = nil - let renderer: MediaPlayerAudioRenderer - if let currentRenderer = self.audioRenderer { - renderer = currentRenderer + if let currentRenderer = self.audioRenderer, !currentRenderer.requestedFrames { + renderer = currentRenderer.renderer } else { + self.audioRenderer?.renderer.stop() + self.audioRenderer = nil + let queue = self.queue - renderer = MediaPlayerAudioRenderer(audioSessionManager: self.audioSessionManager, audioPaused: { [weak self] in + renderer = MediaPlayerAudioRenderer(audioSession: .manager(self.audioSessionManager), playAndRecord: self.playAndRecord, forceAudioToSpeaker: self.forceAudioToSpeaker, updatedRate: { [weak self] in queue.async { if let strongSelf = self { - strongSelf.pause() + strongSelf.tick() + } + } + }, audioPaused: { [weak self] in + queue.async { + if let strongSelf = self { + if strongSelf.enableSound { + strongSelf.pause(lostAudioSession: true) + } else { + strongSelf.seek(timestamp: 0.0, action: .play) + } } } }) - self.audioRenderer = renderer + self.audioRenderer = MediaPlayerAudioRendererContext(renderer: renderer) renderer.start() } controlTimebase = MediaPlayerControlTimebase(timebase: renderer.audioTimebase, isAudio: true) } else { - self.audioRenderer?.stop() + self.audioRenderer?.renderer.stop() self.audioRenderer = nil var timebase: CMTimebase? @@ -282,7 +328,7 @@ private final class MediaPlayerContext { let loadedState = MediaPlayerLoadedState(frameSource: frameSource, mediaBuffers: buffers, controlTimebase: controlTimebase) - if let audioRenderer = self.audioRenderer { + if let audioRenderer = self.audioRenderer?.renderer { let queue = self.queue audioRenderer.flushBuffers(at: seekResult.timestamp, completion: { [weak self] in queue.async { [weak self] in @@ -290,7 +336,7 @@ private final class MediaPlayerContext { switch action { case .play: strongSelf.state = .playing(loadedState) - strongSelf.audioRenderer?.start() + strongSelf.audioRenderer?.renderer.start() case .pause: strongSelf.state = .paused(loadedState) } @@ -319,29 +365,57 @@ private final class MediaPlayerContext { switch self.state { case .empty: self.lastStatusUpdateTimestamp = nil + if self.enableSound { + let queue = self.queue + let renderer = MediaPlayerAudioRenderer(audioSession: .manager(self.audioSessionManager), playAndRecord: self.playAndRecord, forceAudioToSpeaker: self.forceAudioToSpeaker, updatedRate: { [weak self] in + queue.async { + if let strongSelf = self { + strongSelf.tick() + } + } + }, audioPaused: { [weak self] in + queue.async { + if let strongSelf = self { + if strongSelf.enableSound { + strongSelf.pause(lostAudioSession: true) + } else { + strongSelf.seek(timestamp: 0.0, action: .play) + } + } + } + }) + self.audioRenderer = MediaPlayerAudioRendererContext(renderer: renderer) + renderer.start() + } self.seek(timestamp: 0.0, action: .play) - case let .seeking(frameSource, timestamp, disposable, _): - self.state = .seeking(frameSource: frameSource, timestamp: timestamp, disposable: disposable, action: .play) + case let .seeking(frameSource, timestamp, disposable, _, enableSound): + self.state = .seeking(frameSource: frameSource, timestamp: timestamp, disposable: disposable, action: .play, enableSound: enableSound) self.lastStatusUpdateTimestamp = nil case let .paused(loadedState): - self.lastStatusUpdateTimestamp = nil - if self.stoppedAtEnd { - self.seek(timestamp: 0.0, action: .play) + if loadedState.lostAudioSession { + let timestamp = CMTimeGetSeconds(CMTimebaseGetTime(loadedState.controlTimebase.timebase)) + self.seek(timestamp: timestamp, action: .play) } else { - self.state = .playing(loadedState) - self.tick() + self.lastStatusUpdateTimestamp = nil + if self.stoppedAtEnd { + self.seek(timestamp: 0.0, action: .play) + } else { + self.state = .playing(loadedState) + self.tick() + } } case .playing: break } } - fileprivate func playOnceWithSound() { + fileprivate func playOnceWithSound(playAndRecord: Bool) { assert(self.queue.isCurrent()) if !self.enableSound { self.lastStatusUpdateTimestamp = nil self.enableSound = true + self.playAndRecord = playAndRecord self.seek(timestamp: 0.0, action: .play) } } @@ -358,30 +432,75 @@ private final class MediaPlayerContext { loadedState = currentLoadedState case let .paused(currentLoadedState): loadedState = currentLoadedState - case let .seeking: - self.enableSound = false + case let .seeking(_, timestamp, disposable, action, _): + if self.enableSound { + self.state = .empty + disposable.dispose() + self.enableSound = false + self.seek(timestamp: timestamp, action: action) + } } if let loadedState = loadedState { self.enableSound = false + self.playAndRecord = false let timestamp = CMTimeGetSeconds(CMTimebaseGetTime(loadedState.controlTimebase.timebase)) self.seek(timestamp: timestamp, action: .play) } } } - fileprivate func pause() { + fileprivate func setForceAudioToSpeaker(_ value: Bool) { + if self.forceAudioToSpeaker != value { + self.forceAudioToSpeaker = value + + self.audioRenderer?.renderer.setForceAudioToSpeaker(value) + } + } + + fileprivate func setKeepAudioSessionWhilePaused(_ value: Bool) { + if self.keepAudioSessionWhilePaused != value { + self.keepAudioSessionWhilePaused = value + + var isPlaying = false + switch self.state { + case .playing: + isPlaying = true + case let .seeking(_, _, _, action, _): + switch action { + case .play: + isPlaying = true + default: + break + } + default: + break + } + if value && !isPlaying { + self.audioRenderer?.renderer.stop() + } else { + self.audioRenderer?.renderer.start() + } + } + } + + fileprivate func pause(lostAudioSession: Bool) { assert(self.queue.isCurrent()) switch self.state { case .empty: break - case let .seeking(frameSource, timestamp, disposable, _): - self.state = .seeking(frameSource: frameSource, timestamp: timestamp, disposable: disposable, action: .pause) + case let .seeking(frameSource, timestamp, disposable, _, enableSound): + self.state = .seeking(frameSource: frameSource, timestamp: timestamp, disposable: disposable, action: .pause, enableSound: enableSound) self.lastStatusUpdateTimestamp = nil - case .paused: - break + case let .paused(loadedState): + if lostAudioSession { + loadedState.lostAudioSession = true + } case let .playing(loadedState): + if lostAudioSession { + loadedState.lostAudioSession = true + } self.state = .paused(loadedState) self.lastStatusUpdateTimestamp = nil self.tick() @@ -394,17 +513,17 @@ private final class MediaPlayerContext { switch self.state { case .empty: self.play() - case let .seeking(_, _, _, action): + case let .seeking(_, _, _, action, _): switch action { case .play: - self.pause() + self.pause(lostAudioSession: false) case .pause: self.play() } case .paused: self.play() case .playing: - self.pause() + self.pause(lostAudioSession: false) } } @@ -486,7 +605,7 @@ private final class MediaPlayerContext { } } - let rate: Double + var rate: Double var buffering = false if let worstStatus = worstStatus, case let .full(fullUntil) = worstStatus, fullUntil.isFinite { @@ -525,8 +644,19 @@ private final class MediaPlayerContext { rate = 0.0 } + var reportRate = rate + if loadedState.controlTimebase.isAudio { - self.audioRenderer?.setRate(rate) + if rate.isEqual(to: 1.0) { + self.audioRenderer?.renderer.start() + } + self.audioRenderer?.renderer.setRate(rate) + if rate.isEqual(to: 1.0), let audioRenderer = self.audioRenderer { + let timebaseRate = CMTimebaseGetRate(audioRenderer.renderer.audioTimebase) + if !timebaseRate.isEqual(to: rate) { + reportRate = timebaseRate + } + } } else { if !CMTimebaseGetRate(loadedState.controlTimebase.timebase).isEqual(to: rate) { CMTimebaseSetRate(loadedState.controlTimebase.timebase, rate) @@ -534,20 +664,14 @@ private final class MediaPlayerContext { } if let videoTrackFrameBuffer = loadedState.mediaBuffers.videoBuffer, videoTrackFrameBuffer.hasFrames { - self.videoRenderer.state = (loadedState.controlTimebase.timebase, true, videoTrackFrameBuffer.rotationAngle) - /*let queue = self.queue.queue - playerNode.beginRequestingFrames(queue: queue, takeFrame: { [weak videoTrackFrameBuffer] in - if let videoTrackFrameBuffer = videoTrackFrameBuffer { - return videoTrackFrameBuffer.takeFrame() - } else { - return .noFrames - } - })*/ + self.videoRenderer.state = (loadedState.controlTimebase.timebase, true, videoTrackFrameBuffer.rotationAngle, videoTrackFrameBuffer.aspect) } if let audioRenderer = self.audioRenderer, let audioTrackFrameBuffer = loadedState.mediaBuffers.audioBuffer, audioTrackFrameBuffer.hasFrames { - let queue = self.queue.queue - audioRenderer.beginRequestingFrames(queue: queue, takeFrame: { [weak audioTrackFrameBuffer] in + let queue = self.queue + audioRenderer.requestedFrames = true + audioRenderer.renderer.beginRequestingFrames(queue: queue.queue, takeFrame: { [weak audioTrackFrameBuffer] in + assert(queue.isCurrent()) if let audioTrackFrameBuffer = audioTrackFrameBuffer { return audioTrackFrameBuffer.takeFrame() } else { @@ -556,36 +680,47 @@ private final class MediaPlayerContext { }) } + var statusTimestamp = CACurrentMediaTime() let playbackStatus: MediaPlayerPlaybackStatus if buffering { var whilePlaying = false if case .playing = self.state { whilePlaying = true } - playbackStatus = .buffering(whilePlaying: whilePlaying) + playbackStatus = .buffering(initial: false, whilePlaying: whilePlaying) } else if rate.isEqual(to: 1.0) { - playbackStatus = .playing + if reportRate.isZero { + //playbackStatus = .buffering(initial: false, whilePlaying: true) + playbackStatus = .playing + statusTimestamp = 0.0 + } else { + playbackStatus = .playing + } } else { playbackStatus = .paused } - let statusTimestamp = CACurrentMediaTime() if self.lastStatusUpdateTimestamp == nil || self.lastStatusUpdateTimestamp! < statusTimestamp + 500 { lastStatusUpdateTimestamp = statusTimestamp - let status = MediaPlayerStatus(generationTimestamp: statusTimestamp, duration: duration, timestamp: min(max(timestamp, 0.0), duration), status: playbackStatus) + var reportTimestamp = timestamp + if case .seeking(_, timestamp, _, _, _) = self.state { + reportTimestamp = timestamp + } + let status = MediaPlayerStatus(generationTimestamp: statusTimestamp, duration: duration, timestamp: min(max(reportTimestamp, 0.0), duration), seekId: self.seekId, status: playbackStatus) self.playerStatus.set(status) } if performActionAtEndNow { switch self.actionAtEnd { - case .loop: + case let .loop(f): self.stoppedAtEnd = false self.seek(timestamp: 0.0, action: .play) + f?() case .stop: self.stoppedAtEnd = true - self.pause() + self.pause(lostAudioSession: false) case let .action(f): self.stoppedAtEnd = true - self.pause() + self.pause(lostAudioSession: false) f() case let .loopDisablingSound(f): self.stoppedAtEnd = false @@ -602,7 +737,7 @@ private final class MediaPlayerContext { enum MediaPlayerPlaybackStatus: Equatable { case playing case paused - case buffering(whilePlaying: Bool) + case buffering(initial: Bool, whilePlaying: Bool) static func ==(lhs: MediaPlayerPlaybackStatus, rhs: MediaPlayerPlaybackStatus) -> Bool { switch lhs { @@ -618,8 +753,8 @@ enum MediaPlayerPlaybackStatus: Equatable { } else { return false } - case let .buffering(whilePlaying): - if case .buffering(whilePlaying) = rhs { + case let .buffering(initial, whilePlaying): + if case .buffering(initial, whilePlaying) = rhs { return true } else { return false @@ -632,6 +767,7 @@ struct MediaPlayerStatus: Equatable { let generationTimestamp: Double let duration: Double let timestamp: Double + let seekId: Int let status: MediaPlayerPlaybackStatus static func ==(lhs: MediaPlayerStatus, rhs: MediaPlayerStatus) -> Bool { @@ -644,6 +780,9 @@ struct MediaPlayerStatus: Equatable { if !lhs.timestamp.isEqual(to: rhs.timestamp) { return false } + if lhs.seekId != rhs.seekId { + return false + } if lhs.status != rhs.status { return false } @@ -655,7 +794,7 @@ final class MediaPlayer { private let queue = Queue() private var contextRef: Unmanaged? - private let statusValue = ValuePromise(MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, timestamp: 0.0, status: .paused), ignoreRepeated: true) + private let statusValue = ValuePromise(MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, timestamp: 0.0, seekId: 0, status: .paused), ignoreRepeated: true) var status: Signal { return self.statusValue.get() @@ -672,9 +811,9 @@ final class MediaPlayer { } } - init(audioSessionManager: ManagedAudioSession, postbox: Postbox, resource: MediaResource, streamable: Bool, video: Bool, preferSoftwareDecoding: Bool, playAutomatically: Bool = false, enableSound: Bool) { + init(audioSessionManager: ManagedAudioSession, postbox: Postbox, resource: MediaResource, streamable: Bool, video: Bool, preferSoftwareDecoding: Bool, playAutomatically: Bool = false, enableSound: Bool, playAndRecord: Bool = false, keepAudioSessionWhilePaused: Bool = true) { self.queue.async { - let context = MediaPlayerContext(queue: self.queue, audioSessionManager: audioSessionManager, playerStatus: self.statusValue, postbox: postbox, resource: resource, streamable: streamable, video: video, preferSoftwareDecoding: preferSoftwareDecoding, playAutomatically: playAutomatically, enableSound: enableSound) + let context = MediaPlayerContext(queue: self.queue, audioSessionManager: audioSessionManager, playerStatus: self.statusValue, postbox: postbox, resource: resource, streamable: streamable, video: video, preferSoftwareDecoding: preferSoftwareDecoding, playAutomatically: playAutomatically, enableSound: enableSound, playAndRecord: playAndRecord, keepAudioSessionWhilePaused: keepAudioSessionWhilePaused) self.contextRef = Unmanaged.passRetained(context) } } @@ -694,10 +833,10 @@ final class MediaPlayer { } } - func playOnceWithSound() { + func playOnceWithSound(playAndRecord: Bool) { self.queue.async { if let context = self.contextRef?.takeUnretainedValue() { - context.playOnceWithSound() + context.playOnceWithSound(playAndRecord: playAndRecord) } } } @@ -710,10 +849,26 @@ final class MediaPlayer { } } + func setForceAudioToSpeaker(_ value: Bool) { + self.queue.async { + if let context = self.contextRef?.takeUnretainedValue() { + context.setForceAudioToSpeaker(value) + } + } + } + + func setKeepAudioSessionWhilePaused(_ value: Bool) { + self.queue.async { + if let context = self.contextRef?.takeUnretainedValue() { + context.setKeepAudioSessionWhilePaused(value) + } + } + } + func pause() { self.queue.async { if let context = self.contextRef?.takeUnretainedValue() { - context.pause() + context.pause(lostAudioSession: false) } } } diff --git a/TelegramUI/MediaPlayerAudioRenderer.swift b/TelegramUI/MediaPlayerAudioRenderer.swift index 22f0bc29ed..c260117600 100644 --- a/TelegramUI/MediaPlayerAudioRenderer.swift +++ b/TelegramUI/MediaPlayerAudioRenderer.swift @@ -16,16 +16,18 @@ private final class AudioPlayerRendererBufferContext { var bufferMaxChannelSampleIndex: Int64 = 0 var lowWaterSize: Int var notifyLowWater: () -> Void + var updatedRate: () -> Void var notifiedLowWater = false var overflowData = Data() var overflowDataMaxChannelSampleIndex: Int64 = 0 var renderTimestampTick: Int64 = 0 - init(timebase: CMTimebase, buffer: RingByteBuffer, lowWaterSize: Int, notifyLowWater: @escaping () -> Void) { + init(timebase: CMTimebase, buffer: RingByteBuffer, lowWaterSize: Int, notifyLowWater: @escaping () -> Void, updatedRate: @escaping () -> Void) { self.timebase = timebase self.buffer = buffer self.lowWaterSize = lowWaterSize self.notifyLowWater = notifyLowWater + self.updatedRate = updatedRate } } @@ -75,6 +77,7 @@ private func rendererInputProc(refCon: UnsafeMutableRawPointer, ioActionFlags: U var rendererFillOffset = (0, 0) var notifyLowWater: (() -> Void)? + var updatedRate: (() -> Void)? withPlayerRendererBuffer(Int32(intptr_t(bitPattern: refCon)), { context in context.with { context in @@ -93,12 +96,14 @@ private func rendererInputProc(refCon: UnsafeMutableRawPointer, ioActionFlags: U masterClock = CMTimebaseGetMaster(context.timebase)! } CMTimebaseSetRateAndAnchorTime(context.timebase, 1.0, CMTimeMake(sampleIndex, 44100), CMSyncGetTime(masterClock)) + updatedRate = context.updatedRate } else { context.renderTimestampTick += 1 if context.renderTimestampTick % 1000 == 0 { let delta = (Double(sampleIndex) / 44100.0) - CMTimeGetSeconds(CMTimebaseGetTime(context.timebase)) if delta > 0.01 { CMTimebaseSetTime(context.timebase, CMTimeMake(sampleIndex, 44100)) + updatedRate = context.updatedRate } } } @@ -156,6 +161,10 @@ private func rendererInputProc(refCon: UnsafeMutableRawPointer, ioActionFlags: U notifyLowWater() } + if let updatedRate = updatedRate { + updatedRate() + } + return noErr } @@ -169,8 +178,9 @@ private final class AudioPlayerRendererContext { let bufferSizeInSeconds: Int = 5 let lowWaterSizeInSeconds: Int = 2 - let audioSessionManager: ManagedAudioSession + let audioSession: MediaPlayerAudioSessionControl let controlTimebase: CMTimebase + let updatedRate: () -> Void let audioPaused: () -> Void var paused = true @@ -182,14 +192,30 @@ private final class AudioPlayerRendererContext { var requestingFramesContext: RequestingFramesContext? let audioSessionDisposable = MetaDisposable() + var audioSessionControl: ManagedAudioSessionControl? + let playAndRecord: Bool + var forceAudioToSpeaker: Bool { + didSet { + if self.forceAudioToSpeaker != oldValue { + if let audioSessionControl = self.audioSessionControl { + audioSessionControl.setOutputMode(self.forceAudioToSpeaker ? .speakerIfNoHeadphones : .system) + } + } + } + } - init(controlTimebase: CMTimebase, audioSessionManager: ManagedAudioSession, audioPaused: @escaping () -> Void) { + init(controlTimebase: CMTimebase, audioSession: MediaPlayerAudioSessionControl, playAndRecord: Bool, forceAudioToSpeaker: Bool, updatedRate: @escaping () -> Void, audioPaused: @escaping () -> Void) { assert(audioPlayerRendererQueue.isCurrent()) - self.audioSessionManager = audioSessionManager + self.audioSession = audioSession + self.forceAudioToSpeaker = forceAudioToSpeaker + self.controlTimebase = controlTimebase + self.updatedRate = updatedRate self.audioPaused = audioPaused + self.playAndRecord = playAndRecord + self.audioStreamDescription = audioRendererNativeStreamDescription() let bufferSize = Int(self.audioStreamDescription.mSampleRate) * self.bufferSizeInSeconds * Int(self.audioStreamDescription.mBytesPerFrame) @@ -199,6 +225,8 @@ private final class AudioPlayerRendererContext { self.bufferContext = Atomic(value: AudioPlayerRendererBufferContext(timebase: controlTimebase, buffer: RingByteBuffer(size: bufferSize), lowWaterSize: lowWaterSize, notifyLowWater: { notifyLowWater() + }, updatedRate: { + updatedRate() })) self.bufferContextId = registerPlayerRendererBufferContext(self.bufferContext) @@ -230,7 +258,10 @@ private final class AudioPlayerRendererContext { self.bufferContext.with { context in if playing { - context.state = .playing(didSetRate: false) + if case .playing = context.state { + } else { + context.state = .playing(didSetRate: false) + } } else { context.state = .paused CMTimebaseSetRate(context.timebase, 0.0) @@ -331,25 +362,64 @@ private final class AudioPlayerRendererContext { self.audioUnit = audioUnit } - self.audioSessionDisposable.set(self.audioSessionManager.push(audioSessionType: .play, once: true, activate: { [weak self] in - audioPlayerRendererQueue.async { - if let strongSelf = self, !strongSelf.paused { - strongSelf.audioSessionAcquired() - } - } - }, deactivate: { [weak self] in - return Signal { subscriber in - audioPlayerRendererQueue.async { - if let strongSelf = self { - strongSelf.audioPaused() - strongSelf.stop() - subscriber.putCompletion() + switch self.audioSession { + case let .manager(manager): + self.audioSessionDisposable.set(manager.push(audioSessionType: self.playAndRecord ? .playAndRecord : .play, outputMode: self.forceAudioToSpeaker ? .speakerIfNoHeadphones : .system, once: true, manualActivate: { [weak self] control in + audioPlayerRendererQueue.async { + if let strongSelf = self { + strongSelf.audioSessionControl = control + if !strongSelf.paused { + control.setup() + control.setOutputMode(strongSelf.forceAudioToSpeaker ? .speakerIfNoHeadphones : .system) + control.activate({ _ in + audioPlayerRendererQueue.async { + if let strongSelf = self, !strongSelf.paused { + strongSelf.audioSessionAcquired() + } + } + }) + } + } } - } - - return EmptyDisposable - } - })) + }, deactivate: { [weak self] in + return Signal { subscriber in + audioPlayerRendererQueue.async { + if let strongSelf = self { + strongSelf.audioSessionControl = nil + strongSelf.audioPaused() + strongSelf.stop() + subscriber.putCompletion() + } + } + + return EmptyDisposable + } + }, headsetConnectionStatusChanged: { [weak self] value in + audioPlayerRendererQueue.async { + if let strongSelf = self, !value { + strongSelf.audioPaused() + } + } + })) + case let .custom(request): + self.audioSessionDisposable.set(request(MediaPlayerAudioSessionCustomControl(activate: { [weak self] in + audioPlayerRendererQueue.async { + if let strongSelf = self { + if !strongSelf.paused { + strongSelf.audioSessionAcquired() + } + } + } + }, deactivate: { [weak self] in + audioPlayerRendererQueue.async { + if let strongSelf = self { + strongSelf.audioSessionControl = nil + strongSelf.audioPaused() + strongSelf.stop() + } + } + }))) + } } private func audioSessionAcquired() { @@ -519,13 +589,28 @@ private func audioRendererNativeStreamDescription() -> AudioStreamBasicDescripti return canonicalBasicStreamDescription } +final class MediaPlayerAudioSessionCustomControl { + let activate: () -> Void + let deactivate: () -> Void + + init(activate: @escaping () -> Void, deactivate: @escaping () -> Void) { + self.activate = activate + self.deactivate = deactivate + } +} + +enum MediaPlayerAudioSessionControl { + case manager(ManagedAudioSession) + case custom((MediaPlayerAudioSessionCustomControl) -> Disposable) +} + final class MediaPlayerAudioRenderer { private var contextRef: Unmanaged? private let audioClock: CMClock let audioTimebase: CMTimebase - init(audioSessionManager: ManagedAudioSession, audioPaused: @escaping () -> Void) { + init(audioSession: MediaPlayerAudioSessionControl, playAndRecord: Bool, forceAudioToSpeaker: Bool, updatedRate: @escaping () -> Void, audioPaused: @escaping () -> Void) { var audioClock: CMClock? CMAudioClockCreate(nil, &audioClock) self.audioClock = audioClock! @@ -535,7 +620,7 @@ final class MediaPlayerAudioRenderer { self.audioTimebase = audioTimebase! audioPlayerRendererQueue.async { - let context = AudioPlayerRendererContext(controlTimebase: audioTimebase!, audioSessionManager: audioSessionManager, audioPaused: audioPaused) + let context = AudioPlayerRendererContext(controlTimebase: audioTimebase!, audioSession: audioSession, playAndRecord: playAndRecord, forceAudioToSpeaker: forceAudioToSpeaker, updatedRate: updatedRate, audioPaused: audioPaused) self.contextRef = Unmanaged.passRetained(context) } } @@ -591,4 +676,13 @@ final class MediaPlayerAudioRenderer { } } } + + func setForceAudioToSpeaker(_ value: Bool) { + audioPlayerRendererQueue.async { + if let contextRef = self.contextRef { + let context = contextRef.takeUnretainedValue() + context.forceAudioToSpeaker = value + } + } + } } diff --git a/TelegramUI/MediaPlayerNode.swift b/TelegramUI/MediaPlayerNode.swift index 524058804e..2f7b543d35 100644 --- a/TelegramUI/MediaPlayerNode.swift +++ b/TelegramUI/MediaPlayerNode.swift @@ -72,15 +72,16 @@ final class MediaPlayerNode: ASDisplayNode { var polling = false var currentRotationAngle = 0.0 + var currentAspect = 1.0 - var state: (timebase: CMTimebase, requestFrames: Bool, rotationAngle: Double)? { + var state: (timebase: CMTimebase, requestFrames: Bool, rotationAngle: Double, aspect: Double)? { didSet { self.updateState() } } private func updateState() { - if let (timebase, requestFrames, rotationAngle) = self.state { + if let (timebase, requestFrames, rotationAngle, aspect) = self.state { if let videoLayer = self.videoLayer { videoQueue.async { if videoLayer.controlTimebase !== timebase || videoLayer.status == .failed { @@ -89,9 +90,14 @@ final class MediaPlayerNode: ASDisplayNode { } } - if !self.currentRotationAngle.isEqual(to: rotationAngle) { + if !self.currentRotationAngle.isEqual(to: rotationAngle) || !self.currentAspect.isEqual(to: aspect) { self.currentRotationAngle = rotationAngle - videoLayer.setAffineTransform(CGAffineTransform(rotationAngle: CGFloat(rotationAngle))) + self.currentAspect = aspect + var transform = CGAffineTransform(rotationAngle: CGFloat(rotationAngle)) + if !rotationAngle.isZero { + transform = transform.scaledBy(x: CGFloat(aspect), y: CGFloat(1.0 / aspect)) + } + videoLayer.setAffineTransform(transform) } if self.videoInHierarchy { @@ -109,12 +115,12 @@ final class MediaPlayerNode: ASDisplayNode { self.poll(completion: { [weak self] status in self?.polling = false - if let strongSelf = self, let (_, requestFrames, _) = strongSelf.state, requestFrames { + if let strongSelf = self, let (_, requestFrames, _, _) = strongSelf.state, requestFrames { strongSelf.timer?.invalidate() switch status { case let .delay(delay): strongSelf.timer = SwiftSignalKit.Timer(timeout: delay, repeat: true, completion: { - if let strongSelf = self, let videoLayer = strongSelf.videoLayer, let (_, requestFrames, _) = strongSelf.state, requestFrames, strongSelf.videoInHierarchy { + if let strongSelf = self, let videoLayer = strongSelf.videoLayer, let (_, requestFrames, _, _) = strongSelf.state, requestFrames, strongSelf.videoInHierarchy { if videoLayer.isReadyForMoreMediaData { strongSelf.timer?.invalidate() strongSelf.timer = nil @@ -132,7 +138,7 @@ final class MediaPlayerNode: ASDisplayNode { } private func poll(completion: @escaping (PollStatus) -> Void) { - if let (takeFrameQueue, takeFrame) = self.takeFrameAndQueue, let videoLayer = self.videoLayer, let (timebase, _, _) = self.state { + if let (takeFrameQueue, takeFrame) = self.takeFrameAndQueue, let videoLayer = self.videoLayer, let (timebase, _, _, _) = self.state { let layerRef = Unmanaged.passRetained(videoLayer) takeFrameQueue.async { let status: PollStatus @@ -235,6 +241,7 @@ final class MediaPlayerNode: ASDisplayNode { self.videoQueue.async { [weak self] in let videoLayer = MediaPlayerNodeLayer() + videoLayer.videoGravity = .resize Queue.mainQueue().async { if let strongSelf = self { strongSelf.videoLayer = videoLayer diff --git a/TelegramUI/MediaPlayerScrubbingNode.swift b/TelegramUI/MediaPlayerScrubbingNode.swift index fc6c0c4c5c..9a89d8dca8 100644 --- a/TelegramUI/MediaPlayerScrubbingNode.swift +++ b/TelegramUI/MediaPlayerScrubbingNode.swift @@ -84,15 +84,44 @@ private final class MediaPlayerScrubbingForegroundNode: ASDisplayNode { } } -final class MediaPlayerScrubbingNode: ASDisplayNode { - private let lineCap: MediaPlayerScrubbingNodeCap - private let lineHeight: CGFloat +enum MediaPlayerScrubbingNodeHandle { + case none + case line + case circle +} + +enum MediaPlayerScrubbingNodeContent { + case standard(lineHeight: CGFloat, lineCap: MediaPlayerScrubbingNodeCap, scrubberHandle: MediaPlayerScrubbingNodeHandle, backgroundColor: UIColor, foregroundColor: UIColor) + case custom(backgroundNode: ASDisplayNode, foregroundContentNode: ASDisplayNode) +} + +private final class StandardMediaPlayerScrubbingNodeContentNode { + let lineHeight: CGFloat + let lineCap: MediaPlayerScrubbingNodeCap + let backgroundNode: ASImageNode + let foregroundContentNode: ASImageNode + let foregroundNode: MediaPlayerScrubbingForegroundNode + let handleNode: ASDisplayNode? + let handleNodeContainer: MediaPlayerScrubbingNodeButton? - private let backgroundNode: ASImageNode - private let foregroundContentNode: ASImageNode - private let foregroundNode: MediaPlayerScrubbingForegroundNode - private let handleNode: ASDisplayNode? - private let handleNodeContainer: MediaPlayerScrubbingNodeButton? + init(lineHeight: CGFloat, lineCap: MediaPlayerScrubbingNodeCap, backgroundNode: ASImageNode, foregroundContentNode: ASImageNode, foregroundNode: MediaPlayerScrubbingForegroundNode, handleNode: ASDisplayNode?, handleNodeContainer: MediaPlayerScrubbingNodeButton?) { + self.lineHeight = lineHeight + self.lineCap = lineCap + self.backgroundNode = backgroundNode + self.foregroundContentNode = foregroundContentNode + self.foregroundNode = foregroundNode + self.handleNode = handleNode + self.handleNodeContainer = handleNodeContainer + } +} + +private enum MediaPlayerScrubbingNodeContentNodes { + case standard(StandardMediaPlayerScrubbingNodeContentNode) + case custom(backgroundNode: ASDisplayNode, foregroundContentNode: ASDisplayNode, foregroundNode: MediaPlayerScrubbingForegroundNode) +} + +final class MediaPlayerScrubbingNode: ASDisplayNode { + private let contentNodes: MediaPlayerScrubbingNodeContentNodes private var playbackStatusValue: MediaPlayerPlaybackStatus? private var scrubbingBeginTimestamp: Double? @@ -102,20 +131,29 @@ final class MediaPlayerScrubbingNode: ASDisplayNode { var playerStatusUpdated: ((MediaPlayerStatus?) -> Void)? var seek: ((Double) -> Void)? + var ignoreSeekId: Int? + + private var _statusValue: MediaPlayerStatus? private var statusValue: MediaPlayerStatus? { - didSet { - if self.statusValue != oldValue { - self.updateProgress() - - let playbackStatus = self.statusValue?.status - if self.playbackStatusValue != playbackStatus { - self.playbackStatusValue = playbackStatus - if let playbackStatusUpdated = self.playbackStatusUpdated { - playbackStatusUpdated(playbackStatus) + get { + return self._statusValue + } set(value) { + if value != self._statusValue { + if let value = value, value.seekId == self.ignoreSeekId { + } else { + self._statusValue = value + self.updateProgress() + + let playbackStatus = value?.status + if self.playbackStatusValue != playbackStatus { + self.playbackStatusValue = playbackStatus + if let playbackStatusUpdated = self.playbackStatusUpdated { + playbackStatusUpdated(playbackStatus) + } } + + self.playerStatusUpdated?(value) } - - self.playerStatusUpdated?(self.statusValue) } } } @@ -133,87 +171,153 @@ final class MediaPlayerScrubbingNode: ASDisplayNode { } } - init(lineHeight: CGFloat, lineCap: MediaPlayerScrubbingNodeCap, scrubberHandle: Bool, backgroundColor: UIColor, foregroundColor: UIColor) { - self.lineHeight = lineHeight - self.lineCap = lineCap - - self.backgroundNode = ASImageNode() - self.backgroundNode.isLayerBacked = true - self.backgroundNode.displaysAsynchronously = false - self.backgroundNode.displayWithoutProcessing = true - - self.foregroundContentNode = ASImageNode() - self.foregroundContentNode.isLayerBacked = true - self.foregroundContentNode.displaysAsynchronously = false - self.foregroundContentNode.displayWithoutProcessing = true - - switch lineCap { - case .round: - self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: lineHeight, color: backgroundColor) - self.foregroundContentNode.image = generateStretchableFilledCircleImage(diameter: lineHeight, color: foregroundColor) - case .square: - self.backgroundNode.backgroundColor = backgroundColor - self.foregroundContentNode.backgroundColor = foregroundColor - } - - self.foregroundNode = MediaPlayerScrubbingForegroundNode() - self.foregroundNode.isLayerBacked = true - self.foregroundNode.clipsToBounds = true - - if scrubberHandle { - let handleNode = ASImageNode() - handleNode.image = generateHandleBackground(color: foregroundColor) - handleNode.isLayerBacked = true - self.handleNode = handleNode - - let handleNodeContainer = MediaPlayerScrubbingNodeButton() - handleNodeContainer.addSubnode(handleNode) - self.handleNodeContainer = handleNodeContainer - } else { - self.handleNode = nil - self.handleNodeContainer = nil + init(content: MediaPlayerScrubbingNodeContent) { + switch content { + case let .standard(lineHeight, lineCap, scrubberHandle, backgroundColor, foregroundColor): + let backgroundNode = ASImageNode() + backgroundNode.isLayerBacked = true + backgroundNode.displaysAsynchronously = false + backgroundNode.displayWithoutProcessing = true + + let foregroundContentNode = ASImageNode() + foregroundContentNode.isLayerBacked = true + foregroundContentNode.displaysAsynchronously = false + foregroundContentNode.displayWithoutProcessing = true + + switch lineCap { + case .round: + backgroundNode.image = generateStretchableFilledCircleImage(diameter: lineHeight, color: backgroundColor) + foregroundContentNode.image = generateStretchableFilledCircleImage(diameter: lineHeight, color: foregroundColor) + case .square: + backgroundNode.backgroundColor = backgroundColor + foregroundContentNode.backgroundColor = foregroundColor + } + + let foregroundNode = MediaPlayerScrubbingForegroundNode() + foregroundNode.isLayerBacked = true + foregroundNode.clipsToBounds = true + + var handleNodeImpl: ASImageNode? + var handleNodeContainerImpl: MediaPlayerScrubbingNodeButton? + + switch scrubberHandle { + case .none: + break + case .line: + let handleNode = ASImageNode() + handleNode.image = generateHandleBackground(color: foregroundColor) + handleNode.isLayerBacked = true + handleNodeImpl = handleNode + + let handleNodeContainer = MediaPlayerScrubbingNodeButton() + handleNodeContainer.addSubnode(handleNode) + handleNodeContainerImpl = handleNodeContainer + case .circle: + let handleNode = ASImageNode() + handleNode.image = generateFilledCircleImage(diameter: 7.0, color: foregroundColor) + handleNode.isLayerBacked = true + handleNodeImpl = handleNode + + let handleNodeContainer = MediaPlayerScrubbingNodeButton() + handleNodeContainer.addSubnode(handleNode) + handleNodeContainerImpl = handleNodeContainer + } + + self.contentNodes = .standard(StandardMediaPlayerScrubbingNodeContentNode(lineHeight: lineHeight, lineCap: lineCap, backgroundNode: backgroundNode, foregroundContentNode: foregroundContentNode, foregroundNode: foregroundNode, handleNode: handleNodeImpl, handleNodeContainer: handleNodeContainerImpl)) + case let .custom(backgroundNode, foregroundContentNode): + let foregroundNode = MediaPlayerScrubbingForegroundNode() + foregroundNode.isLayerBacked = true + foregroundNode.clipsToBounds = true + + self.contentNodes = .custom(backgroundNode: backgroundNode, foregroundContentNode: foregroundContentNode, foregroundNode: foregroundNode) } super.init() - self.addSubnode(self.backgroundNode) - self.foregroundNode.addSubnode(self.foregroundContentNode) - self.addSubnode(self.foregroundNode) - - if let handleNodeContainer = self.handleNodeContainer { - self.addSubnode(handleNodeContainer) - handleNodeContainer.beginScrubbing = { [weak self] in - if let strongSelf = self { - if let statusValue = strongSelf.statusValue, Double(0.0).isLess(than: statusValue.duration) { - strongSelf.scrubbingBeginTimestamp = statusValue.timestamp - strongSelf.scrubbingTimestamp = statusValue.timestamp - strongSelf.updateProgress() + switch self.contentNodes { + case let .standard(node): + self.addSubnode(node.backgroundNode) + node.foregroundNode.addSubnode(node.foregroundContentNode) + self.addSubnode(node.foregroundNode) + + if let handleNodeContainer = node.handleNodeContainer { + self.addSubnode(handleNodeContainer) + handleNodeContainer.beginScrubbing = { [weak self] in + if let strongSelf = self { + if let statusValue = strongSelf.statusValue, Double(0.0).isLess(than: statusValue.duration) { + strongSelf.scrubbingBeginTimestamp = statusValue.timestamp + strongSelf.scrubbingTimestamp = statusValue.timestamp + strongSelf.updateProgress() + } + } + } + handleNodeContainer.updateScrubbing = { [weak self] addedFraction in + if let strongSelf = self { + if let statusValue = strongSelf.statusValue, let scrubbingBeginTimestamp = strongSelf.scrubbingBeginTimestamp, Double(0.0).isLess(than: statusValue.duration) { + strongSelf.scrubbingTimestamp = scrubbingBeginTimestamp + statusValue.duration * Double(addedFraction) + strongSelf.updateProgress() + } + } + } + handleNodeContainer.endScrubbing = { [weak self] apply in + if let strongSelf = self { + strongSelf.scrubbingBeginTimestamp = nil + let scrubbingTimestamp = strongSelf.scrubbingTimestamp + strongSelf.scrubbingTimestamp = nil + if let scrubbingTimestamp = scrubbingTimestamp, apply { + if let statusValue = strongSelf.statusValue { + strongSelf.ignoreSeekId = statusValue.seekId + } + strongSelf.seek?(scrubbingTimestamp) + } + strongSelf.updateProgress() + } } } - } - handleNodeContainer.updateScrubbing = { [weak self] addedFraction in - if let strongSelf = self { - if let statusValue = strongSelf.statusValue, let scrubbingBeginTimestamp = strongSelf.scrubbingBeginTimestamp, Double(0.0).isLess(than: statusValue.duration) { - strongSelf.scrubbingTimestamp = scrubbingBeginTimestamp + statusValue.duration * Double(addedFraction) - strongSelf.updateProgress() - } + + node.foregroundNode.onEnterHierarchy = { [weak self] in + self?.updateProgress() } - } - handleNodeContainer.endScrubbing = { [weak self] apply in - if let strongSelf = self { - strongSelf.scrubbingBeginTimestamp = nil - let scrubbingTimestamp = strongSelf.scrubbingTimestamp - strongSelf.scrubbingTimestamp = nil - if let scrubbingTimestamp = scrubbingTimestamp, apply { - strongSelf.seek?(scrubbingTimestamp) + case let .custom(backgroundNode, foregroundContentNode, foregroundNode): + self.addSubnode(backgroundNode) + foregroundNode.addSubnode(foregroundContentNode) + self.addSubnode(foregroundNode) + + /*if let handleNodeContainer = handleNodeContainer { + self.addSubnode(handleNodeContainer) + handleNodeContainer.beginScrubbing = { [weak self] in + if let strongSelf = self { + if let statusValue = strongSelf.statusValue, Double(0.0).isLess(than: statusValue.duration) { + strongSelf.scrubbingBeginTimestamp = statusValue.timestamp + strongSelf.scrubbingTimestamp = statusValue.timestamp + strongSelf.updateProgress() + } + } } - strongSelf.updateProgress() + handleNodeContainer.updateScrubbing = { [weak self] addedFraction in + if let strongSelf = self { + if let statusValue = strongSelf.statusValue, let scrubbingBeginTimestamp = strongSelf.scrubbingBeginTimestamp, Double(0.0).isLess(than: statusValue.duration) { + strongSelf.scrubbingTimestamp = scrubbingBeginTimestamp + statusValue.duration * Double(addedFraction) + strongSelf.updateProgress() + } + } + } + handleNodeContainer.endScrubbing = { [weak self] apply in + if let strongSelf = self { + strongSelf.scrubbingBeginTimestamp = nil + let scrubbingTimestamp = strongSelf.scrubbingTimestamp + strongSelf.scrubbingTimestamp = nil + if let scrubbingTimestamp = scrubbingTimestamp, apply { + strongSelf.seek?(scrubbingTimestamp) + } + strongSelf.updateProgress() + } + } + }*/ + + foregroundNode.onEnterHierarchy = { [weak self] in + self?.updateProgress() } - } - } - - self.foregroundNode.onEnterHierarchy = { [weak self] in - self?.updateProgress() } self.statusDisposable = (self.statusValuePromise.get() @@ -237,16 +341,21 @@ final class MediaPlayerScrubbingNode: ASDisplayNode { } func updateColors(backgroundColor: UIColor, foregroundColor: UIColor) { - switch lineCap { - case .round: - self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: lineHeight, color: backgroundColor) - self.foregroundContentNode.image = generateStretchableFilledCircleImage(diameter: lineHeight, color: foregroundColor) - case .square: - self.backgroundNode.backgroundColor = backgroundColor - self.foregroundContentNode.backgroundColor = foregroundColor - } - if let handleNode = self.handleNode as? ASImageNode { - handleNode.image = generateHandleBackground(color: foregroundColor) + switch self.contentNodes { + case let .standard(node): + switch node.lineCap { + case .round: + node.backgroundNode.image = generateStretchableFilledCircleImage(diameter: node.lineHeight, color: backgroundColor) + node.foregroundContentNode.image = generateStretchableFilledCircleImage(diameter: node.lineHeight, color: foregroundColor) + case .square: + node.backgroundNode.backgroundColor = backgroundColor + node.foregroundContentNode.backgroundColor = foregroundColor + } + if let handleNode = node.handleNode as? ASImageNode { + handleNode.image = generateHandleBackground(color: foregroundColor) + } + case .custom: + break } } @@ -255,12 +364,10 @@ final class MediaPlayerScrubbingNode: ASDisplayNode { animation.fromValue = from animation.toValue = to animation.duration = duration - //animation.isRemovedOnCompletion = true animation.fillMode = kCAFillModeBoth animation.speed = speed animation.timeOffset = offset animation.isAdditive = false - //animation.repeatCount = Float.infinity if let beginTime = beginTime { animation.beginTime = beginTime } @@ -268,126 +375,210 @@ final class MediaPlayerScrubbingNode: ASDisplayNode { } private func updateProgress() { - self.foregroundNode.layer.removeAnimation(forKey: "playback-bounds") - self.foregroundNode.layer.removeAnimation(forKey: "playback-position") - if let handleNodeContainer = self.handleNodeContainer { - handleNodeContainer.layer.removeAnimation(forKey: "playback-bounds") - } - let bounds = self.bounds - let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: floor((bounds.size.height - self.lineHeight) / 2.0)), size: CGSize(width: bounds.size.width, height: self.lineHeight)) - self.backgroundNode.frame = backgroundFrame - self.foregroundContentNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: backgroundFrame.size.width, height: backgroundFrame.size.height)) - if let handleNode = self.handleNode { - handleNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 2.0, height: bounds.size.height)) - handleNode.layer.removeAnimation(forKey: "playback-position") - } - - if let handleNodeContainer = self.handleNodeContainer { - handleNodeContainer.frame = bounds - } - - let timestampAndDuration: (timestamp: Double, duration: Double)? - if let statusValue = self.statusValue, Double(0.0).isLess(than: statusValue.duration) { - if let scrubbingTimestamp = self.scrubbingTimestamp { - timestampAndDuration = (max(0.0, min(scrubbingTimestamp, statusValue.duration)), statusValue.duration) - } else { - timestampAndDuration = (statusValue.timestamp, statusValue.duration) - } - } else { - timestampAndDuration = nil - } - - if let (timestamp, duration) = timestampAndDuration { - let progress = CGFloat(timestamp / duration) - if let _ = scrubbingTimestamp { - let fromRect = CGRect(origin: backgroundFrame.origin, size: CGSize(width: 0.0, height: backgroundFrame.size.height)) - let toRect = CGRect(origin: backgroundFrame.origin, size: CGSize(width: backgroundFrame.size.width, height: backgroundFrame.size.height)) - - let fromBounds = CGRect(origin: CGPoint(), size: fromRect.size) - let toBounds = CGRect(origin: CGPoint(), size: toRect.size) - - self.foregroundNode.frame = toRect - self.foregroundNode.layer.add(self.preparedAnimation(keyPath: "bounds", from: NSValue(cgRect: fromBounds), to: NSValue(cgRect: toBounds), duration: duration, beginTime: nil, offset: timestamp, speed: 0.0), forKey: "playback-bounds") - self.foregroundNode.layer.add(self.preparedAnimation(keyPath: "position", from: NSValue(cgPoint: CGPoint(x: fromRect.midX, y: fromRect.midY)), to: NSValue(cgPoint: CGPoint(x: toRect.midX, y: toRect.midY)), duration: duration, beginTime: nil, offset: timestamp, speed: 0.0), forKey: "playback-position") - - if let handleNodeContainer = self.handleNodeContainer { - let fromBounds = bounds - let toBounds = bounds.offsetBy(dx: -bounds.size.width, dy: 0.0) - - handleNodeContainer.isHidden = false - handleNodeContainer.layer.add(self.preparedAnimation(keyPath: "bounds", from: NSValue(cgRect: fromBounds), to: NSValue(cgRect: toBounds), duration: duration, beginTime: nil, offset: timestamp, speed: 0.0), forKey: "playback-bounds") + switch self.contentNodes { + case let .standard(node): + node.foregroundNode.layer.removeAnimation(forKey: "playback-bounds") + node.foregroundNode.layer.removeAnimation(forKey: "playback-position") + if let handleNodeContainer = node.handleNodeContainer { + handleNodeContainer.layer.removeAnimation(forKey: "playback-bounds") } - if let handleNode = self.handleNode { - let fromPosition = handleNode.position - let toPosition = CGPoint(x: fromPosition.x - 1.0, y: fromPosition.y) - handleNode.layer.add(self.preparedAnimation(keyPath: "position", from: NSValue(cgPoint: fromPosition), to: NSValue(cgPoint: toPosition), duration: duration / Double(bounds.size.width), beginTime: nil, offset: timestamp, speed: 0.0, repeatForever: true), forKey: "playback-position") - } - } else if let statusValue = self.statusValue, !progress.isNaN && progress.isFinite { - if statusValue.generationTimestamp.isZero { - let fromRect = CGRect(origin: backgroundFrame.origin, size: CGSize(width: 0.0, height: backgroundFrame.size.height)) - let toRect = CGRect(origin: backgroundFrame.origin, size: CGSize(width: backgroundFrame.size.width, height: backgroundFrame.size.height)) + let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: floor((bounds.size.height - node.lineHeight) / 2.0)), size: CGSize(width: bounds.size.width, height: node.lineHeight)) + node.backgroundNode.frame = backgroundFrame + node.foregroundContentNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: backgroundFrame.size.width, height: backgroundFrame.size.height)) + + + if let handleNode = node.handleNode { + var handleSize: CGSize = CGSize(width: 2.0, height: bounds.size.height) - let fromBounds = CGRect(origin: CGPoint(), size: fromRect.size) - let toBounds = CGRect(origin: CGPoint(), size: toRect.size) - - self.foregroundNode.frame = toRect - self.foregroundNode.layer.add(self.preparedAnimation(keyPath: "bounds", from: NSValue(cgRect: fromBounds), to: NSValue(cgRect: toBounds), duration: duration, beginTime: nil, offset: timestamp, speed: 0.0), forKey: "playback-bounds") - self.foregroundNode.layer.add(self.preparedAnimation(keyPath: "position", from: NSValue(cgPoint: CGPoint(x: fromRect.midX, y: fromRect.midY)), to: NSValue(cgPoint: CGPoint(x: toRect.midX, y: toRect.midY)), duration: duration, beginTime: nil, offset: timestamp, speed: 0.0), forKey: "playback-position") - - if let handleNodeContainer = self.handleNodeContainer { - let fromBounds = bounds - let toBounds = bounds.offsetBy(dx: -bounds.size.width, dy: 0.0) - - handleNodeContainer.isHidden = false - handleNodeContainer.layer.add(self.preparedAnimation(keyPath: "bounds", from: NSValue(cgRect: fromBounds), to: NSValue(cgRect: toBounds), duration: duration, beginTime: nil, offset: timestamp, speed: 0.0), forKey: "playback-bounds") + if let handleNode = handleNode as? ASImageNode, let image = handleNode.image, image.size.width.isEqual(to: 7.0) { + handleSize = image.size } - - if let handleNode = self.handleNode { - let fromPosition = handleNode.position - let toPosition = CGPoint(x: fromPosition.x - 1.0, y: fromPosition.y) - handleNode.layer.add(self.preparedAnimation(keyPath: "position", from: NSValue(cgPoint: fromPosition), to: NSValue(cgPoint: toPosition), duration: duration / Double(bounds.size.width), beginTime: nil, offset: timestamp, speed: 0.0, repeatForever: true), forKey: "playback-position") + handleNode.frame = CGRect(origin: CGPoint(x: 0.0, y: floor((bounds.size.height - handleSize.height) / 2.0)), size: handleSize) + handleNode.layer.removeAnimation(forKey: "playback-position") + } + + if let handleNodeContainer = node.handleNodeContainer { + handleNodeContainer.frame = bounds + } + + var initialBuffering = false + var timestampAndDuration: (timestamp: Double, duration: Double)? + if let statusValue = self.statusValue { + if case .buffering(true, _) = statusValue.status { + initialBuffering = true + } else if Double(0.0).isLess(than: statusValue.duration) { + if let scrubbingTimestamp = self.scrubbingTimestamp { + timestampAndDuration = (max(0.0, min(scrubbingTimestamp, statusValue.duration)), statusValue.duration) + } else { + timestampAndDuration = (statusValue.timestamp, statusValue.duration) + } + } + } + + if let (timestamp, duration) = timestampAndDuration { + let progress = CGFloat(timestamp / duration) + if let _ = scrubbingTimestamp { + let fromRect = CGRect(origin: backgroundFrame.origin, size: CGSize(width: 0.0, height: backgroundFrame.size.height)) + let toRect = CGRect(origin: backgroundFrame.origin, size: CGSize(width: backgroundFrame.size.width, height: backgroundFrame.size.height)) + + let fromBounds = CGRect(origin: CGPoint(), size: fromRect.size) + let toBounds = CGRect(origin: CGPoint(), size: toRect.size) + + node.foregroundNode.frame = toRect + node.foregroundNode.layer.add(self.preparedAnimation(keyPath: "bounds", from: NSValue(cgRect: fromBounds), to: NSValue(cgRect: toBounds), duration: duration, beginTime: nil, offset: timestamp, speed: 0.0), forKey: "playback-bounds") + node.foregroundNode.layer.add(self.preparedAnimation(keyPath: "position", from: NSValue(cgPoint: CGPoint(x: fromRect.midX, y: fromRect.midY)), to: NSValue(cgPoint: CGPoint(x: toRect.midX, y: toRect.midY)), duration: duration, beginTime: nil, offset: timestamp, speed: 0.0), forKey: "playback-position") + + if let handleNodeContainer = node.handleNodeContainer { + let fromBounds = bounds + let toBounds = bounds.offsetBy(dx: -bounds.size.width, dy: 0.0) + + handleNodeContainer.isHidden = false + handleNodeContainer.layer.add(self.preparedAnimation(keyPath: "bounds", from: NSValue(cgRect: fromBounds), to: NSValue(cgRect: toBounds), duration: duration, beginTime: nil, offset: timestamp, speed: 0.0), forKey: "playback-bounds") + } + + if let handleNode = node.handleNode { + let fromPosition = handleNode.position + let toPosition = CGPoint(x: fromPosition.x - 1.0, y: fromPosition.y) + handleNode.layer.add(self.preparedAnimation(keyPath: "position", from: NSValue(cgPoint: fromPosition), to: NSValue(cgPoint: toPosition), duration: duration / Double(bounds.size.width), beginTime: nil, offset: timestamp, speed: 0.0, repeatForever: true), forKey: "playback-position") + } + } else if let statusValue = self.statusValue, !progress.isNaN && progress.isFinite { + if statusValue.generationTimestamp.isZero { + let fromRect = CGRect(origin: backgroundFrame.origin, size: CGSize(width: 0.0, height: backgroundFrame.size.height)) + let toRect = CGRect(origin: backgroundFrame.origin, size: CGSize(width: backgroundFrame.size.width, height: backgroundFrame.size.height)) + + let fromBounds = CGRect(origin: CGPoint(), size: fromRect.size) + let toBounds = CGRect(origin: CGPoint(), size: toRect.size) + + node.foregroundNode.frame = toRect + node.foregroundNode.layer.add(self.preparedAnimation(keyPath: "bounds", from: NSValue(cgRect: fromBounds), to: NSValue(cgRect: toBounds), duration: duration, beginTime: nil, offset: timestamp, speed: 0.0), forKey: "playback-bounds") + node.foregroundNode.layer.add(self.preparedAnimation(keyPath: "position", from: NSValue(cgPoint: CGPoint(x: fromRect.midX, y: fromRect.midY)), to: NSValue(cgPoint: CGPoint(x: toRect.midX, y: toRect.midY)), duration: duration, beginTime: nil, offset: timestamp, speed: 0.0), forKey: "playback-position") + + if let handleNodeContainer = node.handleNodeContainer { + let fromBounds = bounds + let toBounds = bounds.offsetBy(dx: -bounds.size.width, dy: 0.0) + + handleNodeContainer.isHidden = false + handleNodeContainer.layer.add(self.preparedAnimation(keyPath: "bounds", from: NSValue(cgRect: fromBounds), to: NSValue(cgRect: toBounds), duration: duration, beginTime: nil, offset: timestamp, speed: 0.0), forKey: "playback-bounds") + } + + if let handleNode = node.handleNode { + let fromPosition = handleNode.position + let toPosition = CGPoint(x: fromPosition.x - 1.0, y: fromPosition.y) + handleNode.layer.add(self.preparedAnimation(keyPath: "position", from: NSValue(cgPoint: fromPosition), to: NSValue(cgPoint: toPosition), duration: duration / Double(bounds.size.width), beginTime: nil, offset: timestamp, speed: 0.0, repeatForever: true), forKey: "playback-position") + } + } else { + let fromRect = CGRect(origin: backgroundFrame.origin, size: CGSize(width: 0.0, height: backgroundFrame.size.height)) + let toRect = CGRect(origin: backgroundFrame.origin, size: CGSize(width: backgroundFrame.size.width, height: backgroundFrame.size.height)) + + let fromBounds = CGRect(origin: CGPoint(), size: fromRect.size) + let toBounds = CGRect(origin: CGPoint(), size: toRect.size) + + node.foregroundNode.frame = toRect + node.foregroundNode.layer.add(self.preparedAnimation(keyPath: "bounds", from: NSValue(cgRect: fromBounds), to: NSValue(cgRect: toBounds), duration: duration, beginTime: statusValue.generationTimestamp, offset: timestamp, speed: statusValue.status == .playing ? 1.0 : 0.0), forKey: "playback-bounds") + node.foregroundNode.layer.add(self.preparedAnimation(keyPath: "position", from: NSValue(cgPoint: CGPoint(x: fromRect.midX, y: fromRect.midY)), to: NSValue(cgPoint: CGPoint(x: toRect.midX, y: toRect.midY)), duration: duration, beginTime: statusValue.generationTimestamp, offset: timestamp, speed: statusValue.status == .playing ? 1.0 : 0.0), forKey: "playback-position") + + if let handleNodeContainer = node.handleNodeContainer { + let fromBounds = bounds + let toBounds = bounds.offsetBy(dx: -bounds.size.width, dy: 0.0) + + handleNodeContainer.isHidden = false + handleNodeContainer.layer.add(self.preparedAnimation(keyPath: "bounds", from: NSValue(cgRect: fromBounds), to: NSValue(cgRect: toBounds), duration: statusValue.duration, beginTime: statusValue.generationTimestamp, offset: statusValue.timestamp, speed: statusValue.status == .playing ? 1.0 : 0.0), forKey: "playback-bounds") + } + + if let handleNode = node.handleNode { + let fromPosition = handleNode.position + let toPosition = CGPoint(x: fromPosition.x - 1.0, y: fromPosition.y) + handleNode.layer.add(self.preparedAnimation(keyPath: "position", from: NSValue(cgPoint: fromPosition), to: NSValue(cgPoint: toPosition), duration: duration / Double(bounds.size.width), beginTime: statusValue.generationTimestamp, offset: timestamp, speed: statusValue.status == .playing ? 1.0 : 0.0, repeatForever: true), forKey: "playback-position") + } + } + } else { + node.handleNodeContainer?.isHidden = true + node.foregroundNode.frame = CGRect(origin: backgroundFrame.origin, size: CGSize(width: 0.0, height: backgroundFrame.size.height)) } } else { - let fromRect = CGRect(origin: backgroundFrame.origin, size: CGSize(width: 0.0, height: backgroundFrame.size.height)) - let toRect = CGRect(origin: backgroundFrame.origin, size: CGSize(width: backgroundFrame.size.width, height: backgroundFrame.size.height)) - - let fromBounds = CGRect(origin: CGPoint(), size: fromRect.size) - let toBounds = CGRect(origin: CGPoint(), size: toRect.size) - - self.foregroundNode.frame = toRect - self.foregroundNode.layer.add(self.preparedAnimation(keyPath: "bounds", from: NSValue(cgRect: fromBounds), to: NSValue(cgRect: toBounds), duration: duration, beginTime: statusValue.generationTimestamp, offset: timestamp, speed: statusValue.status == .playing ? 1.0 : 0.0), forKey: "playback-bounds") - self.foregroundNode.layer.add(self.preparedAnimation(keyPath: "position", from: NSValue(cgPoint: CGPoint(x: fromRect.midX, y: fromRect.midY)), to: NSValue(cgPoint: CGPoint(x: toRect.midX, y: toRect.midY)), duration: duration, beginTime: statusValue.generationTimestamp, offset: timestamp, speed: statusValue.status == .playing ? 1.0 : 0.0), forKey: "playback-position") - - if let handleNodeContainer = self.handleNodeContainer { - let fromBounds = bounds - let toBounds = bounds.offsetBy(dx: -bounds.size.width, dy: 0.0) - - handleNodeContainer.isHidden = false - handleNodeContainer.layer.add(self.preparedAnimation(keyPath: "bounds", from: NSValue(cgRect: fromBounds), to: NSValue(cgRect: toBounds), duration: statusValue.duration, beginTime: statusValue.generationTimestamp, offset: statusValue.timestamp, speed: statusValue.status == .playing ? 1.0 : 0.0), forKey: "playback-bounds") - } - - if let handleNode = self.handleNode { - let fromPosition = handleNode.position - let toPosition = CGPoint(x: fromPosition.x - 1.0, y: fromPosition.y) - handleNode.layer.add(self.preparedAnimation(keyPath: "position", from: NSValue(cgPoint: fromPosition), to: NSValue(cgPoint: toPosition), duration: duration / Double(bounds.size.width), beginTime: statusValue.generationTimestamp, offset: timestamp, speed: statusValue.status == .playing ? 1.0 : 0.0, repeatForever: true), forKey: "playback-position") - } + node.foregroundNode.frame = CGRect(origin: backgroundFrame.origin, size: CGSize(width: 0.0, height: backgroundFrame.size.height)) + node.handleNodeContainer?.isHidden = true + } + + if initialBuffering { + + } else { + + } + case let .custom(backgroundNode, foregroundContentNode, foregroundNode): + foregroundNode.layer.removeAnimation(forKey: "playback-bounds") + foregroundNode.layer.removeAnimation(forKey: "playback-position") + + let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: bounds.size.width, height: bounds.size.height)) + backgroundNode.frame = backgroundFrame + foregroundContentNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: backgroundFrame.size.width, height: backgroundFrame.size.height)) + + let timestampAndDuration: (timestamp: Double, duration: Double)? + if let statusValue = self.statusValue, Double(0.0).isLess(than: statusValue.duration) { + if let scrubbingTimestamp = self.scrubbingTimestamp { + timestampAndDuration = (max(0.0, min(scrubbingTimestamp, statusValue.duration)), statusValue.duration) + } else { + timestampAndDuration = (statusValue.timestamp, statusValue.duration) + } + } else { + timestampAndDuration = nil + } + + if let (timestamp, duration) = timestampAndDuration { + let progress = CGFloat(timestamp / duration) + if let _ = scrubbingTimestamp { + let fromRect = CGRect(origin: backgroundFrame.origin, size: CGSize(width: 0.0, height: backgroundFrame.size.height)) + let toRect = CGRect(origin: backgroundFrame.origin, size: CGSize(width: backgroundFrame.size.width, height: backgroundFrame.size.height)) + + let fromBounds = CGRect(origin: CGPoint(), size: fromRect.size) + let toBounds = CGRect(origin: CGPoint(), size: toRect.size) + + foregroundNode.frame = toRect + foregroundNode.layer.add(self.preparedAnimation(keyPath: "bounds", from: NSValue(cgRect: fromBounds), to: NSValue(cgRect: toBounds), duration: duration, beginTime: nil, offset: timestamp, speed: 0.0), forKey: "playback-bounds") + foregroundNode.layer.add(self.preparedAnimation(keyPath: "position", from: NSValue(cgPoint: CGPoint(x: fromRect.midX, y: fromRect.midY)), to: NSValue(cgPoint: CGPoint(x: toRect.midX, y: toRect.midY)), duration: duration, beginTime: nil, offset: timestamp, speed: 0.0), forKey: "playback-position") + } else if let statusValue = self.statusValue, !progress.isNaN && progress.isFinite { + if statusValue.generationTimestamp.isZero { + let fromRect = CGRect(origin: backgroundFrame.origin, size: CGSize(width: 0.0, height: backgroundFrame.size.height)) + let toRect = CGRect(origin: backgroundFrame.origin, size: CGSize(width: backgroundFrame.size.width, height: backgroundFrame.size.height)) + + let fromBounds = CGRect(origin: CGPoint(), size: fromRect.size) + let toBounds = CGRect(origin: CGPoint(), size: toRect.size) + + foregroundNode.frame = toRect + foregroundNode.layer.add(self.preparedAnimation(keyPath: "bounds", from: NSValue(cgRect: fromBounds), to: NSValue(cgRect: toBounds), duration: duration, beginTime: nil, offset: timestamp, speed: 0.0), forKey: "playback-bounds") + foregroundNode.layer.add(self.preparedAnimation(keyPath: "position", from: NSValue(cgPoint: CGPoint(x: fromRect.midX, y: fromRect.midY)), to: NSValue(cgPoint: CGPoint(x: toRect.midX, y: toRect.midY)), duration: duration, beginTime: nil, offset: timestamp, speed: 0.0), forKey: "playback-position") + } else { + let fromRect = CGRect(origin: backgroundFrame.origin, size: CGSize(width: 0.0, height: backgroundFrame.size.height)) + let toRect = CGRect(origin: backgroundFrame.origin, size: CGSize(width: backgroundFrame.size.width, height: backgroundFrame.size.height)) + + let fromBounds = CGRect(origin: CGPoint(), size: fromRect.size) + let toBounds = CGRect(origin: CGPoint(), size: toRect.size) + + foregroundNode.frame = toRect + foregroundNode.layer.add(self.preparedAnimation(keyPath: "bounds", from: NSValue(cgRect: fromBounds), to: NSValue(cgRect: toBounds), duration: duration, beginTime: statusValue.generationTimestamp, offset: timestamp, speed: statusValue.status == .playing ? 1.0 : 0.0), forKey: "playback-bounds") + foregroundNode.layer.add(self.preparedAnimation(keyPath: "position", from: NSValue(cgPoint: CGPoint(x: fromRect.midX, y: fromRect.midY)), to: NSValue(cgPoint: CGPoint(x: toRect.midX, y: toRect.midY)), duration: duration, beginTime: statusValue.generationTimestamp, offset: timestamp, speed: statusValue.status == .playing ? 1.0 : 0.0), forKey: "playback-position") + } + } else { + foregroundNode.frame = CGRect(origin: backgroundFrame.origin, size: CGSize(width: 0.0, height: backgroundFrame.size.height)) + } + } else { + foregroundNode.frame = CGRect(origin: backgroundFrame.origin, size: CGSize(width: 0.0, height: backgroundFrame.size.height)) } - } else { - self.handleNodeContainer?.isHidden = true - self.foregroundNode.frame = CGRect(origin: backgroundFrame.origin, size: CGSize(width: 0.0, height: backgroundFrame.size.height)) - } - } else { - self.foregroundNode.frame = CGRect(origin: backgroundFrame.origin, size: CGSize(width: 0.0, height: backgroundFrame.size.height)) - self.handleNodeContainer?.isHidden = true } } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if self.bounds.contains(point) { - return self.handleNodeContainer?.view + switch self.contentNodes { + case let .standard(node): + return node.handleNodeContainer?.view + case .custom: + break + } + return super.hitTest(point, with: event) } else { return nil } diff --git a/TelegramUI/MediaPlayerTimeTextNode.swift b/TelegramUI/MediaPlayerTimeTextNode.swift index 52cc8db051..2e01aea385 100644 --- a/TelegramUI/MediaPlayerTimeTextNode.swift +++ b/TelegramUI/MediaPlayerTimeTextNode.swift @@ -55,6 +55,7 @@ final class MediaPlayerTimeTextNode: ASDisplayNode { var alignment: NSTextAlignment = .left var mode: MediaPlayerTimeTextNodeMode = .normal private let textColor: UIColor + var defaultDuration: Double? private var statusValue: MediaPlayerStatus? { didSet { @@ -114,6 +115,9 @@ final class MediaPlayerTimeTextNode: ASDisplayNode { let timestamp = abs(Int32(statusValue.timestamp - statusValue.duration)) self.state = MediaPlayerTimeTextNodeState(hours: timestamp / (60 * 60), minutes: timestamp % (60 * 60) / 60, seconds: timestamp % 60) } + } else if let defaultDuration = self.defaultDuration { + let timestamp = Int32(defaultDuration) + self.state = MediaPlayerTimeTextNodeState(hours: timestamp / (60 * 60), minutes: timestamp % (60 * 60) / 60, seconds: timestamp % 60) } else { self.state = MediaPlayerTimeTextNodeState() } diff --git a/TelegramUI/MediaResources.swift b/TelegramUI/MediaResources.swift index a199c71b49..ed5f6ad6c4 100644 --- a/TelegramUI/MediaResources.swift +++ b/TelegramUI/MediaResources.swift @@ -3,10 +3,10 @@ import Postbox import TelegramCore public final class VideoMediaResourceAdjustments: PostboxCoding, Equatable { - let data: MemoryBuffer - let digest: MemoryBuffer + public let data: MemoryBuffer + public let digest: MemoryBuffer - init(data: MemoryBuffer, digest: MemoryBuffer) { + public init(data: MemoryBuffer, digest: MemoryBuffer) { self.data = data self.digest = digest } @@ -203,3 +203,60 @@ public class PhotoLibraryMediaResource: TelegramMediaResource { } } +public struct ExternalMusicAlbumArtResourceId: MediaResourceId { + public let title: String + public let performer: String + public let isThumbnail: Bool + + public var uniqueId: String { + return "ext-album-art-\(isThumbnail ? "thump" : "full")-\(self.title.replacingOccurrences(of: "/", with: "_"))-\(self.performer.replacingOccurrences(of: "/", with: "_"))" + } + + public var hashValue: Int { + return self.title.hashValue &* 31 &+ self.performer.hashValue + } + + public func isEqual(to: MediaResourceId) -> Bool { + if let to = to as? ExternalMusicAlbumArtResourceId { + return self.title == to.title && self.performer == to.performer && self.isThumbnail == to.isThumbnail + } else { + return false + } + } +} + +public class ExternalMusicAlbumArtResource: TelegramMediaResource { + public let title: String + public let performer: String + public let isThumbnail: Bool + + public init(title: String, performer: String, isThumbnail: Bool) { + self.title = title + self.performer = performer + self.isThumbnail = isThumbnail + } + + public required init(decoder: PostboxDecoder) { + self.title = decoder.decodeStringForKey("t", orElse: "") + self.performer = decoder.decodeStringForKey("p", orElse: "") + self.isThumbnail = decoder.decodeInt32ForKey("th", orElse: 1) != 0 + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeString(self.title, forKey: "t") + encoder.encodeString(self.performer, forKey: "p") + encoder.encodeInt32(self.isThumbnail ? 1 : 0, forKey: "th") + } + + public var id: MediaResourceId { + return ExternalMusicAlbumArtResourceId(title: self.title, performer: self.performer, isThumbnail: self.isThumbnail) + } + + public func isEqual(to: TelegramMediaResource) -> Bool { + if let to = to as? ExternalMusicAlbumArtResource { + return self.title == to.title && self.performer == to.performer && self.isThumbnail == to.isThumbnail + } else { + return false + } + } +} diff --git a/TelegramUI/MediaTrackFrameBuffer.swift b/TelegramUI/MediaTrackFrameBuffer.swift index b19c55658a..75831e8950 100644 --- a/TelegramUI/MediaTrackFrameBuffer.swift +++ b/TelegramUI/MediaTrackFrameBuffer.swift @@ -27,6 +27,7 @@ final class MediaTrackFrameBuffer { private let type: MediaTrackFrameType let duration: CMTime let rotationAngle: Double + let aspect: Double var statusUpdated: () -> Void = { } @@ -36,12 +37,13 @@ final class MediaTrackFrameBuffer { private var endOfStream = false private var bufferedUntilTime: CMTime? - init(frameSource: MediaFrameSource, decoder: MediaTrackFrameDecoder, type: MediaTrackFrameType, duration: CMTime, rotationAngle: Double) { + init(frameSource: MediaFrameSource, decoder: MediaTrackFrameDecoder, type: MediaTrackFrameType, duration: CMTime, rotationAngle: Double, aspect: Double) { self.frameSource = frameSource self.type = type self.decoder = decoder self.duration = duration self.rotationAngle = rotationAngle + self.aspect = aspect self.frameSourceSinkIndex = self.frameSource.addEventSink { [weak self] event in if let strongSelf = self { diff --git a/TelegramUI/MentionChatInputContextPanelNode.swift b/TelegramUI/MentionChatInputContextPanelNode.swift index 6183a569b5..3446517df7 100644 --- a/TelegramUI/MentionChatInputContextPanelNode.swift +++ b/TelegramUI/MentionChatInputContextPanelNode.swift @@ -7,21 +7,22 @@ import Display private struct MentionChatInputContextPanelEntry: Comparable, Identifiable { let index: Int let peer: Peer + let theme: PresentationTheme var stableId: Int64 { return self.peer.id.toInt64() } static func ==(lhs: MentionChatInputContextPanelEntry, rhs: MentionChatInputContextPanelEntry) -> Bool { - return lhs.index == rhs.index && lhs.peer.isEqual(rhs.peer) + return lhs.index == rhs.index && lhs.peer.isEqual(rhs.peer) && lhs.theme === rhs.theme } static func <(lhs: MentionChatInputContextPanelEntry, rhs: MentionChatInputContextPanelEntry) -> Bool { return lhs.index < rhs.index } - func item(account: Account, peerSelected: @escaping (Peer) -> Void) -> ListViewItem { - return MentionChatInputPanelItem(account: account, peer: self.peer, peerSelected: peerSelected) + func item(account: Account, inverted: Bool, peerSelected: @escaping (Peer) -> Void) -> ListViewItem { + return MentionChatInputPanelItem(account: account, theme: self.theme, inverted: inverted, peer: self.peer, peerSelected: peerSelected) } } @@ -31,36 +32,52 @@ private struct CommandChatInputContextPanelTransition { let updates: [ListViewUpdateItem] } -private func preparedTransition(from fromEntries: [MentionChatInputContextPanelEntry], to toEntries: [MentionChatInputContextPanelEntry], account: Account, peerSelected: @escaping (Peer) -> Void) -> CommandChatInputContextPanelTransition { +private func preparedTransition(from fromEntries: [MentionChatInputContextPanelEntry], to toEntries: [MentionChatInputContextPanelEntry], account: Account, inverted: Bool, peerSelected: @escaping (Peer) -> Void) -> CommandChatInputContextPanelTransition { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } - let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, peerSelected: peerSelected), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, peerSelected: peerSelected), directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, inverted: inverted, peerSelected: peerSelected), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, inverted: inverted, peerSelected: peerSelected), directionHint: nil) } return CommandChatInputContextPanelTransition(deletions: deletions, insertions: insertions, updates: updates) } +enum MentionChatInputContextPanelMode { + case input + case search +} + final class MentionChatInputContextPanelNode: ChatInputContextPanelNode { + let mode: MentionChatInputContextPanelMode + + private var theme: PresentationTheme + private let listView: ListView private var currentEntries: [MentionChatInputContextPanelEntry]? private var enqueuedTransitions: [(CommandChatInputContextPanelTransition, Bool)] = [] - private var hasValidLayout = false + private var validLayout: (CGSize, CGFloat, CGFloat)? - override init(account: Account) { + init(account: Account, theme: PresentationTheme, strings: PresentationStrings, mode: MentionChatInputContextPanelMode) { + self.theme = theme + self.mode = mode + self.listView = ListView() self.listView.isOpaque = false self.listView.stackFromBottom = true - self.listView.keepBottomItemOverscrollBackground = .white + self.listView.keepBottomItemOverscrollBackground = theme.list.plainBackgroundColor self.listView.limitHitTestToNodes = true - super.init(account: account) + super.init(account: account, theme: theme, strings: strings) self.isOpaque = false self.clipsToBounds = true self.addSubnode(self.listView) + + if mode == .search { + self.transform = CATransform3DMakeRotation(CGFloat(Double.pi), 0.0, 0.0, 1.0) + } } func updateResults(_ results: [Peer]) { @@ -73,34 +90,47 @@ final class MentionChatInputContextPanelNode: ChatInputContextPanelNode { continue } peerIdSet.insert(peerId) - entries.append(MentionChatInputContextPanelEntry(index: index, peer: peer)) + entries.append(MentionChatInputContextPanelEntry(index: index, peer: peer, theme: self.theme)) index += 1 } let firstTime = self.currentEntries == nil - let transition = preparedTransition(from: self.currentEntries ?? [], to: entries, account: self.account, peerSelected: { [weak self] peer in + let transition = preparedTransition(from: self.currentEntries ?? [], to: entries, account: self.account, inverted: self.mode == .search, peerSelected: { [weak self] peer in if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction { - interfaceInteraction.updateTextInputState { textInputState in - if let (range, type, _) = textInputStateContextQueryRangeAndType(textInputState) { - var inputText = textInputState.inputText - - if let addressName = peer.addressName, !addressName.isEmpty { - let replacementText = addressName + " " - inputText.replaceSubrange(range, with: replacementText) - - guard let lowerBound = range.lowerBound.samePosition(in: inputText.utf16) else { - return textInputState + switch strongSelf.mode { + case .input: + interfaceInteraction.updateTextInputState { textInputState in + var mentionQueryRange: Range? + inner: for (range, type, _) in textInputStateContextQueryRangeAndType(textInputState) { + if type == [.mention] { + mentionQueryRange = range + break inner + } } - let utfLowerIndex = inputText.utf16.distance(from: inputText.utf16.startIndex, to: lowerBound) - let replacementLength = replacementText.utf16.distance(from: replacementText.utf16.startIndex, to: replacementText.utf16.endIndex) - - let utfUpperPosition = utfLowerIndex + replacementLength - - return ChatTextInputState(inputText: inputText, selectionRange: utfUpperPosition ..< utfUpperPosition) + if let range = mentionQueryRange { + var inputText = textInputState.inputText + + if let addressName = peer.addressName, !addressName.isEmpty { + let replacementText = addressName + " " + inputText.replaceSubrange(range, with: replacementText) + + guard let lowerBound = range.lowerBound.samePosition(in: inputText.utf16) else { + return textInputState + } + let utfLowerIndex = inputText.utf16.distance(from: inputText.utf16.startIndex, to: lowerBound) + + let replacementLength = replacementText.utf16.distance(from: replacementText.utf16.startIndex, to: replacementText.utf16.endIndex) + + let utfUpperPosition = utfLowerIndex + replacementLength + + return ChatTextInputState(inputText: inputText, selectionRange: utfUpperPosition ..< utfUpperPosition) + } + } + return textInputState } - } - return textInputState + case .search: + interfaceInteraction.beginMessageSearch(.member(peer)) } } }) @@ -111,7 +141,7 @@ final class MentionChatInputContextPanelNode: ChatInputContextPanelNode { private func enqueueTransition(_ transition: CommandChatInputContextPanelTransition, firstTime: Bool) { enqueuedTransitions.append((transition, firstTime)) - if self.hasValidLayout { + if self.validLayout != nil { while !self.enqueuedTransitions.isEmpty { self.dequeueTransition() } @@ -119,7 +149,7 @@ final class MentionChatInputContextPanelNode: ChatInputContextPanelNode { } private func dequeueTransition() { - if let (transition, firstTime) = self.enqueuedTransitions.first { + if let validLayout = self.validLayout, let (transition, firstTime) = self.enqueuedTransitions.first { self.enqueuedTransitions.remove(at: 0) var options = ListViewDeleteAndInsertOptions() @@ -132,9 +162,11 @@ final class MentionChatInputContextPanelNode: ChatInputContextPanelNode { } var insets = UIEdgeInsets() - insets.top = topInsetForLayout(size: self.listView.bounds.size) + insets.top = topInsetForLayout(size: validLayout.0) + insets.left = validLayout.1 + insets.right = validLayout.2 - let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: self.listView.bounds.size, insets: insets, duration: 0.0, curve: .Default) + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: validLayout.0, insets: insets, duration: 0.0, curve: .Default) self.listView.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: updateSizeAndInsets, updateOpaqueState: nil, completion: { [weak self] _ in if let strongSelf = self, firstTime { @@ -160,9 +192,14 @@ final class MentionChatInputContextPanelNode: ChatInputContextPanelNode { return max(size.height - minimumItemHeights, 0.0) } - override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) { + override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) { + let hadValidLayout = self.validLayout != nil + self.validLayout = (size, leftInset, rightInset) + var insets = UIEdgeInsets() insets.top = topInsetForLayout(size: size) + insets.left = leftInset + insets.right = rightInset transition.updateFrame(node: self.listView, frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)) @@ -174,10 +211,10 @@ final class MentionChatInputContextPanelNode: ChatInputContextPanelNode { case let .animated(animationDuration, animationCurve): duration = animationDuration switch animationCurve { - case .easeInOut: - break - case .spring: - curve = 7 + case .easeInOut: + break + case .spring: + curve = 7 } } @@ -192,8 +229,7 @@ final class MentionChatInputContextPanelNode: ChatInputContextPanelNode { self.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) - if !hasValidLayout { - hasValidLayout = true + if !hadValidLayout { while !self.enqueuedTransitions.isEmpty { self.dequeueTransition() } diff --git a/TelegramUI/MentionChatInputPanelItem.swift b/TelegramUI/MentionChatInputPanelItem.swift index 4e30a93958..d004205075 100644 --- a/TelegramUI/MentionChatInputPanelItem.swift +++ b/TelegramUI/MentionChatInputPanelItem.swift @@ -7,24 +7,28 @@ import Postbox final class MentionChatInputPanelItem: ListViewItem { fileprivate let account: Account + fileprivate let theme: PresentationTheme + fileprivate let inverted: Bool fileprivate let peer: Peer private let peerSelected: (Peer) -> Void let selectable: Bool = true - public init(account: Account, peer: Peer, peerSelected: @escaping (Peer) -> Void) { + public init(account: Account, theme: PresentationTheme, inverted: Bool, peer: Peer, peerSelected: @escaping (Peer) -> Void) { self.account = account + self.theme = theme + self.inverted = inverted self.peer = peer self.peerSelected = peerSelected } - public func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { let configure = { () -> Void in let node = MentionChatInputPanelItemNode() let nodeLayout = node.asyncLayout() let (top, bottom) = (previousItem != nil, nextItem != nil) - let (layout, apply) = nodeLayout(self, width, top, bottom) + let (layout, apply) = nodeLayout(self, params, top, bottom) node.contentSize = layout.contentSize node.insets = layout.insets @@ -42,7 +46,7 @@ final class MentionChatInputPanelItem: ListViewItem { } } - public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { if let node = node as? MentionChatInputPanelItemNode { Queue.mainQueue().async { let nodeLayout = node.asyncLayout() @@ -50,7 +54,7 @@ final class MentionChatInputPanelItem: ListViewItem { async { let (top, bottom) = (previousItem != nil, nextItem != nil) - let (layout, apply) = nodeLayout(self, width, top, bottom) + let (layout, apply) = nodeLayout(self, params, top, bottom) Queue.mainQueue().async { completion(layout, { apply(animation) @@ -69,11 +73,14 @@ final class MentionChatInputPanelItem: ListViewItem { } private let avatarFont: UIFont = UIFont(name: "ArialRoundedMTBold", size: 16.0)! -private let textFont = Font.medium(14.0) +private let primaryFont = Font.medium(14.0) +private let secondaryFont = Font.regular(14.0) final class MentionChatInputPanelItemNode: ListViewItemNode { static let itemHeight: CGFloat = 42.0 + private var item: MentionChatInputPanelItem? + private let avatarNode: AvatarNode private let textNode: TextNode private let topSeparatorNode: ASDisplayNode @@ -85,21 +92,16 @@ final class MentionChatInputPanelItemNode: ListViewItemNode { self.textNode = TextNode() self.topSeparatorNode = ASDisplayNode() - self.topSeparatorNode.backgroundColor = UIColor(rgb: 0xC9CDD1) self.topSeparatorNode.isLayerBacked = true self.separatorNode = ASDisplayNode() - self.separatorNode.backgroundColor = UIColor(rgb: 0xD6D6DA) self.separatorNode.isLayerBacked = true self.highlightedBackgroundNode = ASDisplayNode() - self.highlightedBackgroundNode.backgroundColor = UIColor(rgb: 0xd9d9d9) self.highlightedBackgroundNode.isLayerBacked = true super.init(layerBacked: false, dynamicBounce: false) - self.backgroundColor = .white - self.addSubnode(self.topSeparatorNode) self.addSubnode(self.separatorNode) @@ -107,50 +109,81 @@ final class MentionChatInputPanelItemNode: ListViewItemNode { self.addSubnode(self.textNode) } - override public func layoutForWidth(_ width: CGFloat, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { + override public func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { if let item = item as? MentionChatInputPanelItem { let doLayout = self.asyncLayout() let merged = (top: previousItem != nil, bottom: nextItem != nil) - let (layout, apply) = doLayout(item, width, merged.top, merged.bottom) + let (layout, apply) = doLayout(item, params, merged.top, merged.bottom) self.contentSize = layout.contentSize self.insets = layout.insets apply(.None) } } - func asyncLayout() -> (_ item: MentionChatInputPanelItem, _ width: CGFloat, _ mergedTop: Bool, _ mergedBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { + func asyncLayout() -> (_ item: MentionChatInputPanelItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { let makeTextLayout = TextNode.asyncLayout(self.textNode) - return { [weak self] item, width, mergedTop, mergedBottom in - let leftInset: CGFloat = 55.0 - let rightInset: CGFloat = 10.0 + + let previousItem = self.item + + return { [weak self] item, params, mergedTop, mergedBottom in + let baseWidth = params.width - params.leftInset - params.rightInset - let (textLayout, textApply) = makeTextLayout(NSAttributedString(string: item.peer.displayTitle, font: textFont, textColor: .black), nil, 1, .end, CGSize(width: width - leftInset - rightInset, height: 100.0), .natural, nil, UIEdgeInsets()) + let leftInset: CGFloat = 55.0 + params.leftInset + let rightInset: CGFloat = 10.0 + params.rightInset - let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: width, height: HashtagChatInputPanelItemNode.itemHeight), insets: UIEdgeInsets()) + var updatedInverted: Bool? + if previousItem?.inverted != item.inverted { + updatedInverted = item.inverted + } + + let string = NSMutableAttributedString() + string.append(NSAttributedString(string: item.peer.displayTitle, font: primaryFont, textColor: item.theme.list.itemPrimaryTextColor)) + if let addressName = item.peer.addressName, !addressName.isEmpty { + string.append(NSAttributedString(string: " @\(addressName)", font: secondaryFont, textColor: item.theme.list.itemSecondaryTextColor)) + } + + let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: string, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: HashtagChatInputPanelItemNode.itemHeight), insets: UIEdgeInsets()) return (nodeLayout, { _ in if let strongSelf = self { + strongSelf.item = item + + if let updatedInverted = updatedInverted { + if updatedInverted { + strongSelf.transform = CATransform3DMakeRotation(CGFloat(Double.pi), 0.0, 0.0, 1.0) + } else { + strongSelf.transform = CATransform3DIdentity + } + } + + strongSelf.separatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor + strongSelf.topSeparatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor + strongSelf.backgroundColor = item.theme.list.plainBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor + strongSelf.avatarNode.setPeer(account: item.account, peer: item.peer) - textApply() + let _ = textApply() - strongSelf.avatarNode.frame = CGRect(origin: CGPoint(x: 12.0, y: floor((nodeLayout.contentSize.height - 30.0) / 2.0)), size: CGSize(width: 30.0, height: 30.0)) + strongSelf.avatarNode.frame = CGRect(origin: CGPoint(x: params.leftInset + 12.0, y: floor((nodeLayout.contentSize.height - 30.0) / 2.0)), size: CGSize(width: 30.0, height: 30.0)) strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floor((nodeLayout.contentSize.height - textLayout.size.height) / 2.0)), size: textLayout.size) strongSelf.topSeparatorNode.isHidden = mergedTop strongSelf.separatorNode.isHidden = !mergedBottom - strongSelf.topSeparatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: UIScreenPixel)) - strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: nodeLayout.contentSize.height - UIScreenPixel), size: CGSize(width: width - leftInset, height: UIScreenPixel)) + strongSelf.topSeparatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: item.inverted ? (nodeLayout.contentSize.height - UIScreenPixel) : 0.0), size: CGSize(width: params.width, height: UIScreenPixel)) + strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: !item.inverted ? (nodeLayout.contentSize.height - UIScreenPixel) : 0.0), size: CGSize(width: params.width - leftInset, height: UIScreenPixel)) - strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: nodeLayout.size.height + UIScreenPixel)) + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.width, height: nodeLayout.size.height + UIScreenPixel)) } }) } } - override func setHighlighted(_ highlighted: Bool, animated: Bool) { - super.setHighlighted(highlighted, animated: animated) + override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { + super.setHighlighted(highlighted, at: point, animated: animated) if highlighted { self.highlightedBackgroundNode.alpha = 1.0 diff --git a/TelegramUI/MessageContentKind.swift b/TelegramUI/MessageContentKind.swift index e1222eba8d..d5e4db8084 100644 --- a/TelegramUI/MessageContentKind.swift +++ b/TelegramUI/MessageContentKind.swift @@ -2,7 +2,7 @@ import Foundation import Postbox import TelegramCore -private enum MessageContentKind { +enum MessageContentKind: Equatable { case text(String) case image case video @@ -14,9 +14,80 @@ private enum MessageContentKind { case contact case game(String) case location + + static func ==(lhs: MessageContentKind, rhs: MessageContentKind) -> Bool { + switch lhs { + case let .text(text): + if case .text(text) = rhs { + return true + } else { + return false + } + case .image: + if case .image = rhs { + return true + } else { + return false + } + case .video: + if case .video = rhs { + return true + } else { + return false + } + case .videoMessage: + if case .videoMessage = rhs { + return true + } else { + return false + } + case .audioMessage: + if case .audioMessage = rhs { + return true + } else { + return false + } + case let .sticker(text): + if case .sticker(text) = rhs { + return true + } else { + return false + } + case .animation: + if case .animation = rhs { + return true + } else { + return false + } + case let .file(text): + if case .file(text) = rhs { + return true + } else { + return false + } + case .contact: + if case .contact = rhs { + return true + } else { + return false + } + case let .game(text): + if case .game(text) = rhs { + return true + } else { + return false + } + case .location: + if case .location = rhs { + return true + } else { + return false + } + } + } } -private func messageContentKind(_ message: Message, strings: PresentationStrings, accountPeerId: PeerId) -> MessageContentKind { +func messageContentKind(_ message: Message, strings: PresentationStrings, accountPeerId: PeerId) -> MessageContentKind { for media in message.media { switch media { case _ as TelegramMediaImage: @@ -72,6 +143,9 @@ private func messageContentKind(_ message: Message, strings: PresentationStrings } func descriptionStringForMessage(_ message: Message, strings: PresentationStrings, accountPeerId: PeerId) -> String { + if !message.text.isEmpty { + return message.text + } switch messageContentKind(message, strings: strings, accountPeerId: accountPeerId) { case let .text(text): return text diff --git a/TelegramUI/MessageUtils.swift b/TelegramUI/MessageUtils.swift new file mode 100644 index 0000000000..8d2bc2bd1d --- /dev/null +++ b/TelegramUI/MessageUtils.swift @@ -0,0 +1,21 @@ +import Foundation +import Postbox +import TelegramCore + +extension Message { + func effectivelyIncoming(_ accountPeerId: PeerId) -> Bool { + if self.id.peerId == accountPeerId { + if self.forwardInfo != nil { + return true + } else { + return false + } + } else if self.flags.contains(.Incoming) { + return true + } else if let channel = self.peers[self.id.peerId] as? TelegramChannel, case .broadcast = channel.info { + return true + } else { + return false + } + } +} diff --git a/TelegramUI/MultipleAvatarsNode.swift b/TelegramUI/MultipleAvatarsNode.swift new file mode 100644 index 0000000000..3415773321 --- /dev/null +++ b/TelegramUI/MultipleAvatarsNode.swift @@ -0,0 +1,92 @@ +import Foundation +import Display +import AsyncDisplayKit +import Postbox +import TelegramCore + +private let avatarFont: UIFont = UIFont(name: "ArialRoundedMTBold", size: 13.0)! + +final class MultipleAvatarsNode: ASDisplayNode { + private var nodes: [(Peer, AvatarNode)] = [] + + static func asyncLayout(_ current: MultipleAvatarsNode?) -> (Account, [Peer], CGSize) -> (Bool) -> MultipleAvatarsNode { + let currentNodes: [(Peer, AvatarNode)] = current?.nodes ?? [] + return { account, peers, size in + var node: MultipleAvatarsNode + if let current = current { + node = current + } else { + node = MultipleAvatarsNode() + } + + var resultNodes: [(Peer, AvatarNode)] = [] + for peer in peers { + var found = false + inner: for (currentPeer, currentNode) in currentNodes { + if currentPeer.id == peer.id { + resultNodes.append((peer, currentNode)) + found = true + break inner + } + } + if !found { + resultNodes.append((peer, AvatarNode(font: avatarFont))) + } + if resultNodes.count == 4 { + break + } + } + + return { animated in + let partitionSize = floor(size.width / 2.0) + let singleSize = partitionSize - 1.0 + + var index = 0 + for (peer, avatarNode) in resultNodes { + let xPosition: CGFloat = index % 2 == 0 ? 0.0 : size.width - singleSize + let yPosition = index / 2 == 0 ? 0.0 : size.height - singleSize + let avatarFrame = CGRect(origin: CGPoint(x: xPosition, y: yPosition), size: CGSize(width: singleSize, height: singleSize)) + if avatarNode.supernode == nil { + node.addSubnode(avatarNode) + avatarNode.frame = avatarFrame + if animated { + avatarNode.layer.animateScale(from: 0.2, to: 1.0, duration: 0.2) + avatarNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } else { + let distance = CGPoint(x: avatarNode.frame.midX - avatarFrame.midX, y: avatarNode.frame.midY - avatarFrame.midY) + avatarNode.frame = avatarFrame + if animated { + avatarNode.layer.animatePosition(from: distance, to: CGPoint(), duration: 0.2, timingFunction: kCAMediaTimingFunctionEaseInEaseOut, additive: true) + } + } + avatarNode.setPeer(account: account, peer: peer) + index += 1 + } + index += 1 + for (_, currentNode) in node.nodes { + var found = false + inner: for (_, resultNode) in resultNodes { + if currentNode === resultNode { + found = true + break inner + } + } + if !found { + if animated { + currentNode.layer.animateScale(from: 1.0, to: 0.4, duration: 0.2, removeOnCompletion: false) + currentNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak currentNode] _ in + currentNode?.removeFromSupernode() + }) + } else { + currentNode.removeFromSupernode() + } + } + } + node.nodes = resultNodes + + return node + } + } + } +} diff --git a/TelegramUI/MultiplexedVideoNode.swift b/TelegramUI/MultiplexedVideoNode.swift index 653c217c73..4ff3e301a6 100644 --- a/TelegramUI/MultiplexedVideoNode.swift +++ b/TelegramUI/MultiplexedVideoNode.swift @@ -38,6 +38,12 @@ final class MultiplexedVideoNode: UIScrollView, UIScrollViewDelegate { private let account: Account private let trackingNode: MultiplexedVideoTrackingNode + var bottomInset: CGFloat = 0.0 { + didSet { + self.setNeedsLayout() + } + } + var files: [TelegramMediaFile] = [] { didSet { self.updateVisibleItems() @@ -60,6 +66,7 @@ final class MultiplexedVideoNode: UIScrollView, UIScrollViewDelegate { init(account: Account) { self.account = account self.trackingNode = MultiplexedVideoTrackingNode() + self.trackingNode.isLayerBacked = true var timebase: CMTimebase? CMTimebaseCreateWithMasterClock(nil, CMClockGetHostTimeClock(), &timebase) @@ -349,7 +356,7 @@ final class MultiplexedVideoNode: UIScrollView, UIScrollViewDelegate { i += row.count } - let contentSize = CGSize(width: drawableSize.width, height: contentMaxValueInScrollDirection) + let contentSize = CGSize(width: drawableSize.width, height: contentMaxValueInScrollDirection + self.bottomInset) self.contentSize = contentSize self.displayItems = displayItems diff --git a/TelegramUI/MusicPlaybackSettings.swift b/TelegramUI/MusicPlaybackSettings.swift new file mode 100644 index 0000000000..ebf99104d7 --- /dev/null +++ b/TelegramUI/MusicPlaybackSettings.swift @@ -0,0 +1,73 @@ +import Foundation +import Postbox +import SwiftSignalKit + +public enum MusicPlaybackSettingsOrder: Int32 { + case regular = 0 + case reversed = 1 + case random = 2 +} + +public enum MusicPlaybackSettingsLooping: Int32 { + case none = 0 + case item = 1 + case all = 2 +} + +public struct MusicPlaybackSettings: PreferencesEntry, Equatable { + public let order: MusicPlaybackSettingsOrder + public let looping: MusicPlaybackSettingsLooping + + public static var defaultSettings: MusicPlaybackSettings { + return MusicPlaybackSettings(order: .regular, looping: .none) + } + + public init(order: MusicPlaybackSettingsOrder, looping: MusicPlaybackSettingsLooping) { + self.order = order + self.looping = looping + } + + public init(decoder: PostboxDecoder) { + self.order = MusicPlaybackSettingsOrder(rawValue: decoder.decodeInt32ForKey("order", orElse: 0)) ?? .regular + self.looping = MusicPlaybackSettingsLooping(rawValue: decoder.decodeInt32ForKey("looping", orElse: 0)) ?? .none + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeInt32(self.order.rawValue, forKey: "order") + encoder.encodeInt32(self.looping.rawValue, forKey: "looping") + } + + public func isEqual(to: PreferencesEntry) -> Bool { + if let to = to as? MusicPlaybackSettings { + return self == to + } else { + return false + } + } + + public static func ==(lhs: MusicPlaybackSettings, rhs: MusicPlaybackSettings) -> Bool { + return lhs.order == rhs.order && lhs.looping == rhs.looping + } + + func withUpdatedOrder(_ order: MusicPlaybackSettingsOrder) -> MusicPlaybackSettings { + return MusicPlaybackSettings(order: order, looping: self.looping) + } + + func withUpdatedLooping(_ looping: MusicPlaybackSettingsLooping) -> MusicPlaybackSettings { + return MusicPlaybackSettings(order: self.order, looping: looping) + } +} + +func updateMusicPlaybackSettingsInteractively(postbox: Postbox, _ f: @escaping (MusicPlaybackSettings) -> MusicPlaybackSettings) -> Signal { + return postbox.modify { modifier -> Void in + modifier.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.musicPlaybackSettings, { entry in + let currentSettings: MusicPlaybackSettings + if let entry = entry as? MusicPlaybackSettings { + currentSettings = entry + } else { + currentSettings = MusicPlaybackSettings.defaultSettings + } + return f(currentSettings) + }) + } +} diff --git a/TelegramUI/NativeVideoContent.swift b/TelegramUI/NativeVideoContent.swift index acf6fd851d..88ad453d49 100644 --- a/TelegramUI/NativeVideoContent.swift +++ b/TelegramUI/NativeVideoContent.swift @@ -5,25 +5,61 @@ import SwiftSignalKit import Postbox import TelegramCore +enum NativeVideoContentId: Hashable { + case message(MessageId, MediaId) + case instantPage(MediaId, MediaId) + + static func ==(lhs: NativeVideoContentId, rhs: NativeVideoContentId) -> Bool { + switch lhs { + case let .message(messageId, mediaId): + if case .message(messageId, mediaId) = rhs { + return true + } else { + return false + } + case let .instantPage(pageId, mediaId): + if case .instantPage(pageId, mediaId) = rhs { + return true + } else { + return false + } + } + } + + var hashValue: Int { + switch self { + case let .message(messageId, mediaId): + return messageId.hashValue &* 31 &+ mediaId.hashValue + case let .instantPage(pageId, mediaId): + return pageId.hashValue &* 31 &+ mediaId.hashValue + } + } +} + final class NativeVideoContent: UniversalVideoContent { let id: AnyHashable let file: TelegramMediaFile let dimensions: CGSize let duration: Int32 + let streamVideo: Bool + let enableSound: Bool - init(file: TelegramMediaFile) { - self.id = anyHashableFromMediaResourceId(file.resource.id) + init(id: NativeVideoContentId, file: TelegramMediaFile, streamVideo: Bool = false, enableSound: Bool = true) { + self.id = id self.file = file self.dimensions = file.dimensions ?? CGSize(width: 128.0, height: 128.0) self.duration = file.duration ?? 0 + self.streamVideo = streamVideo + self.enableSound = enableSound } - func makeContentNode(account: Account) -> UniversalVideoContentNode & ASDisplayNode { - return NativeVideoContentNode(account: account, audioSessionManager: account.telegramApplicationContext.mediaManager.audioSession, postbox: account.postbox, file: self.file) + func makeContentNode(postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode { + return NativeVideoContentNode(postbox: postbox, audioSessionManager: audioSession, file: self.file, streamVideo: self.streamVideo, enableSound: self.enableSound) } } private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContentNode { + private let postbox: Postbox private let file: TelegramMediaFile private let player: MediaPlayer private let imageNode: TransformImageNode @@ -36,36 +72,59 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent return self._status.get() } - init(account: Account, audioSessionManager: ManagedAudioSession, postbox: Postbox, file: TelegramMediaFile) { + private let _ready = Promise() + var ready: Signal { + return self._ready.get() + } + + private let fetchDisposable = MetaDisposable() + + init(postbox: Postbox, audioSessionManager: ManagedAudioSession, file: TelegramMediaFile, streamVideo: Bool, enableSound: Bool) { + self.postbox = postbox self.file = file self.imageNode = TransformImageNode() - self.player = MediaPlayer(audioSessionManager: audioSessionManager, postbox: postbox, resource: file.resource, streamable: false, video: true, preferSoftwareDecoding: false, playAutomatically: false, enableSound: true) + self.player = MediaPlayer(audioSessionManager: audioSessionManager, postbox: postbox, resource: file.resource, streamable: streamVideo, video: true, preferSoftwareDecoding: false, playAutomatically: false, enableSound: enableSound) var actionAtEndImpl: (() -> Void)? - self.player.actionAtEnd = .stop + if enableSound { + self.player.actionAtEnd = .action({ + actionAtEndImpl?() + }) + } else { + self.player.actionAtEnd = .loop({ + actionAtEndImpl?() + }) + } self.playerNode = MediaPlayerNode(backgroundThread: false) self.player.attachPlayerNode(self.playerNode) super.init() actionAtEndImpl = { [weak self] in - if let strongSelf = self { - for listener in strongSelf.playbackCompletedListeners.copyItems() { - listener() - } - } + self?.performActionAtEnd() } - self.imageNode.setSignal(account: account, signal: mediaGridMessageVideo(account: account, video: file)) + self.imageNode.setSignal(mediaGridMessageVideo(postbox: postbox, video: file)) self.addSubnode(self.imageNode) self.addSubnode(self.playerNode) self._status.set(self.player.status) + + self.imageNode.imageUpdated = { [weak self] in + self?._ready.set(.single(Void())) + } } deinit { self.player.pause() + self.fetchDisposable.dispose() + } + + private func performActionAtEnd() { + for listener in self.playbackCompletedListeners.copyItems() { + listener() + } } func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { @@ -98,7 +157,7 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent func setSoundEnabled(_ value: Bool) { assert(Queue.mainQueue().isCurrent()) if value { - self.player.playOnceWithSound() + self.player.playOnceWithSound(playAndRecord: true) } else { self.player.continuePlayingWithoutSound() } @@ -109,6 +168,24 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent self.player.seek(timestamp: timestamp) } + func playOnceWithSound(playAndRecord: Bool) { + assert(Queue.mainQueue().isCurrent()) + self.player.actionAtEnd = .loopDisablingSound({ [weak self] in + self?.performActionAtEnd() + }) + self.player.playOnceWithSound(playAndRecord: playAndRecord) + } + + func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) { + assert(Queue.mainQueue().isCurrent()) + self.player.setForceAudioToSpeaker(forceAudioToSpeaker) + } + + func continuePlayingWithoutSound() { + assert(Queue.mainQueue().isCurrent()) + self.player.continuePlayingWithoutSound() + } + func addPlaybackCompleted(_ f: @escaping () -> Void) -> Int { return self.playbackCompletedListeners.add(f) } @@ -116,4 +193,13 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent func removePlaybackCompleted(_ index: Int) { self.playbackCompletedListeners.remove(index) } + + func fetchControl(_ control: UniversalVideoNodeFetchControl) { + switch control { + case .fetch: + self.fetchDisposable.set(self.postbox.mediaBox.fetchedResource(self.file.resource, tag: TelegramMediaResourceFetchTag(statsCategory: MediaResourceStatsCategory.video)).start()) + case .cancel: + self.postbox.mediaBox.cancelInteractiveResourceFetch(self.file.resource) + } + } } diff --git a/TelegramUI/NavigateToChatController.swift b/TelegramUI/NavigateToChatController.swift index 060207deb5..ced2cb2cc6 100644 --- a/TelegramUI/NavigateToChatController.swift +++ b/TelegramUI/NavigateToChatController.swift @@ -3,17 +3,35 @@ import Display import TelegramCore import Postbox -public func navigateToChatController(navigationController: NavigationController, account: Account, peerId: PeerId) { +public func navigateToChatController(navigationController: NavigationController, account: Account, chatLocation: ChatLocation, messageId: MessageId? = nil, animated: Bool = true) { var found = false - for controller in navigationController.viewControllers { - if let controller = controller as? ChatController, controller.peerId == peerId { - let _ = navigationController.popToViewController(controller, animated: true) + var isFirst = true + for controller in navigationController.viewControllers.reversed() { + if let controller = controller as? ChatController, controller.chatLocation == chatLocation { + if let messageId = messageId { + controller.navigateToMessage(id: messageId, animated: isFirst, completion: { [weak navigationController, weak controller] in + if let navigationController = navigationController, let controller = controller { + let _ = navigationController.popToViewController(controller, animated: animated) + } + }) + } else { + let _ = navigationController.popToViewController(controller, animated: animated) + } found = true break } + isFirst = false } if !found { - navigationController.pushViewController(ChatController(account: account, peerId: peerId)) + navigationController.pushViewController(ChatController(account: account, chatLocation: chatLocation, messageId: messageId)) + } +} + +public func isOverlayControllerForChatNotificationOverlayPresentation(_ controller: ViewController) -> Bool { + if controller is GalleryController || controller is AvatarGalleryController || controller is ThemeGalleryController || controller is InstantPageGalleryController { + return true + } else { + return false } } diff --git a/TelegramUI/NetworkStatusTitleView.swift b/TelegramUI/NetworkStatusTitleView.swift index bc21b3b7da..bb260a5049 100644 --- a/TelegramUI/NetworkStatusTitleView.swift +++ b/TelegramUI/NetworkStatusTitleView.swift @@ -67,7 +67,7 @@ final class NetworkStatusTitleView: UIView, NavigationBarTitleTransitionNode { self.titleNode.isOpaque = false self.titleNode.isUserInteractionEnabled = false - self.activityIndicator = ActivityIndicator(type: .custom(theme.rootController.navigationBar.secondaryTextColor), speed: .slow) + self.activityIndicator = ActivityIndicator(type: .custom(theme.rootController.navigationBar.secondaryTextColor, 22.0), speed: .slow) let activityIndicatorSize = self.activityIndicator.measure(CGSize(width: 100.0, height: 100.0)) self.activityIndicator.frame = CGRect(origin: CGPoint(), size: activityIndicatorSize) diff --git a/TelegramUI/NotificationContainerController.swift b/TelegramUI/NotificationContainerController.swift index 703d2d8500..cfb6b6ff0c 100644 --- a/TelegramUI/NotificationContainerController.swift +++ b/TelegramUI/NotificationContainerController.swift @@ -1,33 +1,69 @@ import Foundation import Display import AsyncDisplayKit +import TelegramCore +import SwiftSignalKit public final class NotificationContainerController: ViewController { private var controllerNode: NotificationContainerControllerNode { return self.displayNode as! NotificationContainerControllerNode } - public init() { + private var presentationData: PresentationData + private var presentationDataDisposable: Disposable? + + public init(account: Account) { + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + super.init(navigationBarTheme: nil) self.statusBar.statusBarStyle = .Ignore + + self.presentationDataDisposable = (account.telegramApplicationContext.presentationData + |> deliverOnMainQueue).start(next: { [weak self] presentationData in + if let strongSelf = self { + let previousTheme = strongSelf.presentationData.theme + let previousStrings = strongSelf.presentationData.strings + + strongSelf.presentationData = presentationData + + if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings { + strongSelf.updateThemeAndStrings() + } + } + }) + } + + deinit { + self.presentationDataDisposable?.dispose() } required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } + private func updateThemeAndStrings() { + if self.isNodeLoaded { + self.controllerNode.updatePresentationData(self.presentationData) + } + } + override public func loadView() { super.loadView() } override public func loadDisplayNode() { - self.displayNode = NotificationContainerControllerNode() + self.displayNode = NotificationContainerControllerNode(presentationData: self.presentationData) self.displayNodeDidLoad() self.controllerNode.displayingItemsUpdated = { [weak self] value in if let strongSelf = self { strongSelf.statusBar.statusBarStyle = value ? .Hide : .Ignore + if value { + strongSelf.deferScreenEdgeGestures = [.top] + } else { + strongSelf.deferScreenEdgeGestures = [] + } } } } diff --git a/TelegramUI/NotificationContainerControllerNode.swift b/TelegramUI/NotificationContainerControllerNode.swift index 4a93cbd872..c71e52817c 100644 --- a/TelegramUI/NotificationContainerControllerNode.swift +++ b/TelegramUI/NotificationContainerControllerNode.swift @@ -19,7 +19,11 @@ final class NotificationContainerControllerNode: ASDisplayNode { private var timeoutTimer: SwiftSignalKit.Timer? - override init() { + private var presentationData: PresentationData + + init(presentationData: PresentationData) { + self.presentationData = presentationData + super.init() self.setViewBlock({ @@ -30,6 +34,10 @@ final class NotificationContainerControllerNode: ASDisplayNode { self.isOpaque = false } + func updatePresentationData(_ presentationData: PresentationData) { + self.presentationData = presentationData + } + override func didLoad() { super.didLoad() @@ -49,6 +57,7 @@ final class NotificationContainerControllerNode: ASDisplayNode { self.validLayout = layout if let (_, topItemNode) = self.topItemAndNode { + transition.updateFrame(node: topItemNode, frame: CGRect(origin: CGPoint(), size: layout.size)) topItemNode.updateLayout(layout: layout, transition: transition) } } @@ -83,13 +92,14 @@ final class NotificationContainerControllerNode: ASDisplayNode { } let itemNode = item.node() - let containerNode = NotificationItemContainerNode() + let containerNode = NotificationItemContainerNode(theme: self.presentationData.theme) containerNode.item = item containerNode.contentNode = itemNode containerNode.dismissed = { [weak self] item in if let strongSelf = self { if let (topItem, topItemNode) = strongSelf.topItemAndNode, topItem.groupingKey != nil && topItem.groupingKey == item.groupingKey { topItemNode.removeFromSupernode() + strongSelf.topItemAndNode = nil if let strongSelf = self, strongSelf.topItemAndNode == nil { strongSelf.displayingItemsUpdated?(false) @@ -97,11 +107,27 @@ final class NotificationContainerControllerNode: ASDisplayNode { } } } + containerNode.cancelTimeout = { [weak self] item in + if let strongSelf = self { + if let (topItem, topItemNode) = strongSelf.topItemAndNode, topItem.groupingKey != nil && topItem.groupingKey == item.groupingKey { + strongSelf.timeoutTimer?.invalidate() + strongSelf.timeoutTimer = nil + } + } + } + containerNode.resumeTimeout = { [weak self] item in + if let strongSelf = self { + if let (topItem, _) = strongSelf.topItemAndNode, topItem.groupingKey != nil && topItem.groupingKey == item.groupingKey { + strongSelf.resetTimeoutTimer() + } + } + } self.topItemAndNode = (item, containerNode) self.addSubnode(containerNode) if let validLayout = self.validLayout { containerNode.updateLayout(layout: validLayout, transition: .immediate) + containerNode.frame = CGRect(origin: CGPoint(), size: validLayout.size) containerNode.animateIn() } @@ -109,7 +135,17 @@ final class NotificationContainerControllerNode: ASDisplayNode { self.displayingItemsUpdated?(true) } + self.resetTimeoutTimer() + } + + private func resetTimeoutTimer() { self.timeoutTimer?.invalidate() + let timeout: Double + #if DEBUG + timeout = 6.0 + #else + timeout = 5.0 + #endif let timeoutTimer = SwiftSignalKit.Timer(timeout: 5.0, repeat: false, completion: { [weak self] in if let strongSelf = self { if let (_, topItemNode) = strongSelf.topItemAndNode { diff --git a/TelegramUI/NotificationItem.swift b/TelegramUI/NotificationItem.swift index 4aab094f47..4b6e007a36 100644 --- a/TelegramUI/NotificationItem.swift +++ b/TelegramUI/NotificationItem.swift @@ -6,7 +6,9 @@ public protocol NotificationItem { var groupingKey: AnyHashable? { get } func node() -> NotificationItemNode - func tapped() + func tapped(_ take: @escaping () -> (ASDisplayNode?, () -> Void)) + func canBeExpanded() -> Bool + func expand(_ take: @escaping () -> (ASDisplayNode?, () -> Void)) } public class NotificationItemNode: ASDisplayNode { diff --git a/TelegramUI/NotificationItemContainerNode.swift b/TelegramUI/NotificationItemContainerNode.swift index 910c6abdb7..069ab5f2f8 100644 --- a/TelegramUI/NotificationItemContainerNode.swift +++ b/TelegramUI/NotificationItemContainerNode.swift @@ -2,13 +2,6 @@ import Foundation import AsyncDisplayKit import Display -private let backgroundImageWithShadow = generateImage(CGSize(width: 30.0 + 8.0 * 2.0, height: 30.0 + 8.0 + 20.0), rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setShadow(offset: CGSize(width: 0.0, height: -4.0), blur: 40.0, color: UIColor(white: 0.0, alpha: 0.3).cgColor) - context.setFillColor(UIColor.white.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(x: 8.0, y: 8.0), size: CGSize(width: 30.0, height: 30.0))) -})?.stretchableImage(withLeftCapWidth: 8 + 15, topCapHeight: 8 + 15) - final class NotificationItemContainerNode: ASDisplayNode { private let backgroundNode: ASImageNode @@ -16,6 +9,18 @@ final class NotificationItemContainerNode: ASDisplayNode { var item: NotificationItem? + private var hapticFeedback: HapticFeedback? + private var willBeExpanded = false { + didSet { + if self.willBeExpanded != oldValue { + if self.hapticFeedback == nil { + self.hapticFeedback = HapticFeedback() + } + self.hapticFeedback?.impact() + } + } + } + var contentNode: NotificationItemNode? { didSet { if self.contentNode !== oldValue { @@ -33,12 +38,16 @@ final class NotificationItemContainerNode: ASDisplayNode { } var dismissed: ((NotificationItem) -> Void)? + var cancelTimeout: ((NotificationItem) -> Void)? + var resumeTimeout: ((NotificationItem) -> Void)? - override init() { + var cancelledTimeout = false + + init(theme: PresentationTheme) { self.backgroundNode = ASImageNode() self.backgroundNode.displayWithoutProcessing = true self.backgroundNode.displaysAsynchronously = false - self.backgroundNode.image = backgroundImageWithShadow + self.backgroundNode.image = PresentationResourcesRootController.inAppNotificationBackground(theme) super.init() @@ -56,24 +65,33 @@ final class NotificationItemContainerNode: ASDisplayNode { } func animateIn() { - self.layer.animatePosition(from: CGPoint(x: 0.0, y: -100.0), to: CGPoint(), duration: 0.4, additive: true) + if let _ = self.validLayout { + self.layer.animatePosition(from: CGPoint(x: 0.0, y: -self.backgroundNode.bounds.size.height), to: CGPoint(), duration: 0.4, additive: true) + } } func animateOut(completion: @escaping () -> Void) { - self.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -100.0), duration: 0.4, removeOnCompletion: false, additive: true, completion: { _ in + if let _ = self.validLayout { + self.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -self.backgroundNode.bounds.size.height), duration: 0.4, removeOnCompletion: false, additive: true, completion: { _ in + completion() + }) + } else { completion() - }) + } } func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { self.validLayout = layout if let contentNode = self.contentNode { - let contentInsets = UIEdgeInsets(top: 8.0, left: 8.0, bottom: 8.0, right: 8.0) + var contentInsets = UIEdgeInsets(top: 8.0, left: 8.0 + layout.safeInsets.left, bottom: 8.0, right: 8.0 + layout.safeInsets.right) + if let statusBarHeight = layout.statusBarHeight, CGFloat(44.0).isLessThanOrEqualTo(statusBarHeight) { + contentInsets.top += 34.0 + } let contentWidth = layout.size.width - contentInsets.left - contentInsets.right let contentHeight = contentNode.updateLayout(width: contentWidth, transition: transition) - transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: 8.0 + contentHeight + 20.0))) + transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left, y: contentInsets.top - 8.0), size: CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right, height: 8.0 + contentHeight + 20.0))) transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(x: contentInsets.left, y: contentInsets.top), size: CGSize(width: contentWidth, height: contentHeight))) } @@ -89,7 +107,17 @@ final class NotificationItemContainerNode: ASDisplayNode { @objc func tapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { if let item = self.item { - item.tapped() + item.tapped({ [weak self] in + if let strongSelf = self, let contentNode = strongSelf.contentNode, let _ = strongSelf.item { + return (contentNode, { + if let strongSelf = self, let item = strongSelf.item { + strongSelf.dismissed?(item) + } + }) + } else { + return (nil, {}) + } + }) } } } @@ -97,19 +125,87 @@ final class NotificationItemContainerNode: ASDisplayNode { @objc func panGesture(_ recognizer: UIPanGestureRecognizer) { switch recognizer.state { case .began: - break + self.cancelledTimeout = false case .changed: let translation = recognizer.translation(in: self.view) var bounds = self.bounds - bounds.origin.y = max(0.0, -translation.y) - self.bounds = bounds - case .ended: - self.animateOut(completion: { [weak self] in - if let strongSelf = self, let item = strongSelf.item { - strongSelf.dismissed?(item) + bounds.origin.y = -translation.y + if bounds.origin.y < 0.0 { + let delta = -bounds.origin.y + bounds.origin.y = -((1.0 - (1.0 / (((delta) * 0.55 / (50.0)) + 1.0))) * 50.0) + } + if abs(translation.y) > 1.0 { + if self.hapticFeedback == nil { + self.hapticFeedback = HapticFeedback() } - }) + self.hapticFeedback?.prepareImpact() + } + self.bounds = bounds + var expand = false + if let item = self.item { + if !self.cancelledTimeout && abs(translation.y) > 4.0 { + self.cancelledTimeout = true + self.cancelTimeout?(item) + } + expand = item.canBeExpanded() && bounds.minY < -24.0 + } + if self.willBeExpanded != expand { + self.willBeExpanded = expand + } + case .ended: + let translation = recognizer.translation(in: self.view) + var bounds = self.bounds + bounds.origin.y = -translation.y + if bounds.origin.y < 0.0 { + let delta = -bounds.origin.y + bounds.origin.y = -((1.0 - (1.0 / (((delta) * 0.55 / (50.0)) + 1.0))) * 50.0) + } + + let velocity = recognizer.velocity(in: self.view) + + if (bounds.minY < -20.0 || velocity.y > 300.0) { + if let item = self.item { + if !self.cancelledTimeout { + self.cancelledTimeout = true + self.cancelTimeout?(item) + } + + item.expand({ [weak self] in + if let strongSelf = self, let contentNode = strongSelf.contentNode, let _ = strongSelf.item { + return (contentNode, { + if let strongSelf = self, let item = strongSelf.item { + strongSelf.dismissed?(item) + } + }) + } else { + return (nil, {}) + } + }) + } + } else if bounds.minY > 5.0 || velocity.y < -200.0 { + self.animateOut(completion: { [weak self] in + if let strongSelf = self, let item = strongSelf.item { + strongSelf.dismissed?(item) + } + }) + } else { + if let item = self.item, self.cancelledTimeout { + self.cancelledTimeout = false + self.resumeTimeout?(item) + } + + self.cancelledTimeout = false + let previousBounds = self.bounds + var bounds = self.bounds + bounds.origin.y = 0.0 + self.bounds = bounds + self.layer.animateBounds(from: previousBounds, to: self.bounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionEaseInEaseOut) + + self.willBeExpanded = false + } case .cancelled: + self.willBeExpanded = false + self.cancelledTimeout = false let previousBounds = self.bounds var bounds = self.bounds bounds.origin.y = 0.0 diff --git a/TelegramUI/NotificationSoundSelection.swift b/TelegramUI/NotificationSoundSelection.swift index cd7f969000..92be87ef38 100644 --- a/TelegramUI/NotificationSoundSelection.swift +++ b/TelegramUI/NotificationSoundSelection.swift @@ -215,7 +215,7 @@ private func playSound(account: Account, sound: PeerMessageSound, defaultSound: return Signal { subscriber in var currentPlayer: AudioPlayerWrapper? var deactivateImpl: (() -> Void)? - let session = account.telegramApplicationContext.mediaManager.audioSession.push(audioSessionType: .play, activate: { + let session = account.telegramApplicationContext.mediaManager.audioSession.push(audioSessionType: .play, activate: { _ in if let url = Bundle.main.url(forResource: fileNameForNotificationSound(sound, defaultSound: defaultSound), withExtension: "m4a") { currentPlayer = AudioPlayerWrapper(url: url, completed: { deactivateImpl?() diff --git a/TelegramUI/NumericFormat.swift b/TelegramUI/NumericFormat.swift index da511ff77f..3181608f0d 100644 --- a/TelegramUI/NumericFormat.swift +++ b/TelegramUI/NumericFormat.swift @@ -31,10 +31,8 @@ func timeIntervalString(strings: PresentationStrings, value: Int32) -> String { return strings.MessageTimer_Days(max(1, value / (60 * 60 * 24))) } else if value < 60 * 60 * 24 * 30 { return strings.MessageTimer_Weeks(max(1, value / (60 * 60 * 24 * 7))) - } else if value < 60 * 60 * 24 * 360 { - return strings.MessageTimer_Months(max(1, value / (60 * 60 * 24 * 30))) } else { - return strings.MessageTimer_Years(max(1, value / (60 * 60 * 24 * 365))) + return strings.MessageTimer_Months(max(1, value / (60 * 60 * 24 * 30))) } } @@ -53,14 +51,10 @@ func shortTimeIntervalString(strings: PresentationStrings, value: Int32) -> Stri } func muteForIntervalString(strings: PresentationStrings, value: Int32) -> String { - if value < 60 * 60 { - return strings.MuteFor_Minutes(max(1, value / 60)) - } else if value < 60 * 60 * 24 { + if value < 60 * 60 * 24 { return strings.MuteFor_Hours(max(1, value / (60 * 60))) - } else if value < 60 * 60 * 24 * 7 { - return strings.MuteFor_Days(max(1, value / (60 * 60 * 24))) } else { - return strings.MuteFor_Weeks(max(1, value / (60 * 60 * 24 * 7))) + return strings.MuteFor_Days(max(1, value / (60 * 60 * 24))) } } diff --git a/TelegramUI/OngoingCallContext.swift b/TelegramUI/OngoingCallContext.swift index ec237aa156..f3fde3c844 100644 --- a/TelegramUI/OngoingCallContext.swift +++ b/TelegramUI/OngoingCallContext.swift @@ -9,6 +9,15 @@ private func callConnectionDescription(_ connection: CallSessionConnection) -> O return OngoingCallConnectionDescription(connectionId: connection.id, ip: connection.ip, ipv6: connection.ipv6, port: connection.port, peerTag: connection.peerTag) } +private let setupLogs: Bool = { + OngoingCallThreadLocalContext.setupLoggingFunction({ value in + if let value = value { + Logger.shared.log("TGVOIP", value) + } + }) + return true +}() + final class OngoingCallContext { let internalId: CallSessionInternalId @@ -22,6 +31,8 @@ final class OngoingCallContext { private let audioSessionDisposable = MetaDisposable() init(callSessionManager: CallSessionManager, internalId: CallSessionInternalId) { + let _ = setupLogs + self.internalId = internalId self.callSessionManager = callSessionManager diff --git a/TelegramUI/OngoingCallThreadLocalContext.h b/TelegramUI/OngoingCallThreadLocalContext.h index 61e4570211..0c5b407be2 100644 --- a/TelegramUI/OngoingCallThreadLocalContext.h +++ b/TelegramUI/OngoingCallThreadLocalContext.h @@ -23,6 +23,8 @@ typedef NS_ENUM(int32_t, OngoingCallState) { @interface OngoingCallThreadLocalContext : NSObject ++ (void)setupLoggingFunction:(void (*)(NSString *))loggingFunction; + @property (nonatomic, copy) void (^stateChanged)(OngoingCallState); - (instancetype _Nonnull)init; diff --git a/TelegramUI/OngoingCallThreadLocalContext.mm b/TelegramUI/OngoingCallThreadLocalContext.mm index 293a3bfde7..c99ffa9fde 100644 --- a/TelegramUI/OngoingCallThreadLocalContext.mm +++ b/TelegramUI/OngoingCallThreadLocalContext.mm @@ -1,6 +1,7 @@ #import "OngoingCallThreadLocalContext.h" #import "../submodules/libtgvoip/VoIPController.h" +#import "../submodules/libtgvoip/os/darwin/TGLogWrapper.h" #import @@ -20,12 +21,21 @@ static void TGCallSha256(uint8_t *msg, size_t length, uint8_t *output) { MTRawSha256(msg, length, output); } -static void TGCallRandomBytes(uint8_t *buffer, size_t length) { - arc4random_buf(buffer, length); +static void TGCallAesCtrEncrypt(uint8_t *inOut, size_t length, uint8_t *key, uint8_t *iv, uint8_t *ecount, uint32_t *num) { + uint8_t *outData = (uint8_t *)malloc(length); + MTAesCtr *aesCtr = [[MTAesCtr alloc] initWithKey:key keyLength:32 iv:iv ecount:ecount num:*num]; + [aesCtr encryptIn:inOut out:outData len:length]; + memcpy(inOut, outData, length); + free(outData); + + [aesCtr getIv:iv]; + + memcpy(ecount, [aesCtr ecount], 16); + *num = [aesCtr num]; } -static void TGCallLoggingFunction(const char *msg) { - NSLog(@"%s", msg); +static void TGCallRandomBytes(uint8_t *buffer, size_t length) { + arc4random_buf(buffer, length); } @implementation OngoingCallConnectionDescription @@ -68,6 +78,10 @@ static void controllerStateCallback(tgvoip::VoIPController *controller, int stat @implementation OngoingCallThreadLocalContext ++ (void)setupLoggingFunction:(void (*)(NSString *))loggingFunction { + TGVoipLoggingFunction = loggingFunction; +} + - (instancetype)init { self = [super init]; if (self != nil) { @@ -87,6 +101,7 @@ static void controllerStateCallback(tgvoip::VoIPController *controller, int stat tgvoip::VoIPController::crypto.rand_bytes = &TGCallRandomBytes; tgvoip::VoIPController::crypto.aes_ige_encrypt = &TGCallAesIgeEncrypt; tgvoip::VoIPController::crypto.aes_ige_decrypt = &TGCallAesIgeDecrypt; + tgvoip::VoIPController::crypto.aes_ctr_encrypt = &TGCallAesCtrEncrypt; _state = OngoingCallStateInitializing; } diff --git a/TelegramUI/OpenChatMessage.swift b/TelegramUI/OpenChatMessage.swift new file mode 100644 index 0000000000..8b1d57bac2 --- /dev/null +++ b/TelegramUI/OpenChatMessage.swift @@ -0,0 +1,251 @@ +import Foundation +import Display +import AsyncDisplayKit +import Postbox +import TelegramCore +import SwiftSignalKit +import PassKit + +func openChatMessage(account: Account, message: Message, reverseMessageGalleryOrder: Bool, navigationController: NavigationController?, dismissInput: @escaping () -> Void, present: @escaping (ViewController, Any?) -> Void, transitionNode: @escaping (MessageId, Media) -> ASDisplayNode?, addToTransitionSurface: @escaping (UIView) -> Void, openUrl: (String) -> Void, openPeer: @escaping (Peer, ChatControllerInteractionNavigateToPeer) -> Void, callPeer: @escaping (PeerId) -> Void, sendSticker: @escaping (TelegramMediaFile) -> Void, setupTemporaryHiddenMedia: @escaping (Signal, Int, Media) -> Void) -> Bool { + var galleryMedia: Media? + var otherMedia: Media? + var instantPageMedia: [InstantPageGalleryEntry]? + for media in message.media { + if let file = media as? TelegramMediaFile { + galleryMedia = file + } else if let image = media as? TelegramMediaImage { + galleryMedia = image + } else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { + if content.embedUrl != nil && !webEmbedVideoContentSupportsWebpage(content) { + openUrl(content.url) + return true + } else { + if let file = content.file { + galleryMedia = file + } else if let image = content.image { + galleryMedia = image + } + if let instantPage = content.instantPage, let galleryMedia = galleryMedia { + switch websiteType(of: content) { + case .instagram, .twitter: + let medias = instantPageGalleryMedia(webpageId: webpage.webpageId, page: instantPage, galleryMedia: galleryMedia) + if medias.count > 1 { + instantPageMedia = medias + } + case .generic: + break + } + } + } + } else if let mapMedia = media as? TelegramMediaMap { + galleryMedia = mapMedia + } else if let contactMedia = media as? TelegramMediaContact { + otherMedia = contactMedia + } + } + + if let instantPageMedia = instantPageMedia, let galleryMedia = galleryMedia { + var centralIndex: Int = 0 + for i in 0 ..< instantPageMedia.count { + if instantPageMedia[i].media.media.id == galleryMedia.id { + centralIndex = i + break + } + } + + let gallery = InstantPageGalleryController(account: account, entries: instantPageMedia, centralIndex: centralIndex, replaceRootController: { [weak navigationController] controller, ready in + if let navigationController = navigationController { + navigationController.replaceTopController(controller, animated: false, ready: ready) + } + }) + setupTemporaryHiddenMedia(gallery.hiddenMedia, centralIndex, galleryMedia) + + dismissInput() + present(gallery, InstantPageGalleryControllerPresentationArguments(transitionArguments: { entry in + var selectedTransitionNode: ASDisplayNode? + if entry.index == centralIndex { + selectedTransitionNode = transitionNode(message.id, galleryMedia) + } + if let selectedTransitionNode = selectedTransitionNode { + return GalleryTransitionArguments(transitionNode: selectedTransitionNode, addToTransitionSurface: addToTransitionSurface) + } + return nil + })) + return true + } else if let galleryMedia = galleryMedia { + if let mapMedia = galleryMedia as? TelegramMediaMap { + dismissInput() + present(legacyLocationController(message: message, mapMedia: mapMedia, account: account, openPeer: { peer in + openPeer(peer, .info) + }), nil) + } else if let file = galleryMedia as? TelegramMediaFile, file.isSticker { + for attribute in file.attributes { + if case let .Sticker(_, reference, _) = attribute { + if let reference = reference { + let controller = StickerPackPreviewController(account: account, stickerPack: reference) + controller.sendSticker = sendSticker + dismissInput() + present(controller, nil) + } + break + } + } + } else if let file = galleryMedia as? TelegramMediaFile, file.isMusic || file.isVoice || file.isInstantVideo { + let location: PeerMessagesPlaylistLocation + let playerType: MediaManagerPlayerType + if (file.isVoice || file.isInstantVideo) && message.tags.contains(.voiceOrInstantVideo) { + location = .messages(peerId: message.id.peerId, tagMask: .voiceOrInstantVideo, at: message.id) + playerType = .voice + } else if file.isMusic && message.tags.contains(.music) { + location = .messages(peerId: message.id.peerId, tagMask: .music, at: message.id) + playerType = .music + } else { + location = .singleMessage(message.id) + playerType = (file.isVoice || file.isInstantVideo) ? .voice : .music + } + account.telegramApplicationContext.mediaManager.setPlaylist(PeerMessagesMediaPlaylist(postbox: account.postbox, network: account.network, location: location), type: playerType) + } else if let file = galleryMedia as? TelegramMediaFile, file.mimeType == "application/vnd.apple.pkpass" || (file.fileName != nil && file.fileName!.lowercased().hasSuffix(".pkpass")) { + let _ = (account.postbox.mediaBox.resourceData(file.resource, option: .complete(waitUntilFetchStatus: true)) + |> take(1) + |> deliverOnMainQueue).start(next: { data in + guard let navigationController = navigationController else { + return + } + if data.complete, let content = try? Data(contentsOf: URL(fileURLWithPath: data.path)) { + var error: NSError? + let pass = PKPass(data: content, error: &error) + if error == nil { + let controller = PKAddPassesViewController(pass: pass) + if let window = navigationController.view.window { + window.rootViewController?.present(controller, animated: true) + } + } + } + }) + /* + NSData *passData = [[NSData alloc] initWithContentsOfFile:[_companion fileUrlForDocumentMedia:documentAttachment].path]; + NSError *error; + PKPass *pass = [[PKPass alloc] initWithData:passData error:&error]; + + if (error == nil) + { + [self presentViewController:[[PKAddPassesViewController alloc] initWithPass:pass] animated:true completion:nil]; + return nil; + } + */ + } else { + let gallery = GalleryController(account: account, messageId: message.id, invertItemOrder: reverseMessageGalleryOrder, replaceRootController: { [weak navigationController] controller, ready in + navigationController?.replaceTopController(controller, animated: false, ready: ready) + }, baseNavigationController: navigationController) + + dismissInput() + present(gallery, GalleryControllerPresentationArguments(transitionArguments: { messageId, media in + let selectedTransitionNode = transitionNode(messageId, media) + if let selectedTransitionNode = selectedTransitionNode { + return GalleryTransitionArguments(transitionNode: selectedTransitionNode, addToTransitionSurface: addToTransitionSurface) + } + return nil + })) + } + return true + } else if let contact = otherMedia as? TelegramMediaContact { + let _ = (account.postbox.modify { modifier -> (Peer?, Bool?) in + if let peerId = contact.peerId { + return (modifier.getPeer(peerId), modifier.isPeerContact(peerId: peerId)) + } else { + return (nil, nil) + } + } |> deliverOnMainQueue).start(next: { peer, isContact in + guard let peer = peer else { + return + } + + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + let controller = ActionSheetController(presentationTheme: presentationData.theme) + let dismissAction: () -> Void = { [weak controller] in + controller?.dismissAnimated() + } + var items: [ActionSheetItem] = [] + + if let peerId = contact.peerId { + items.append(ActionSheetButtonItem(title: presentationData.strings.Conversation_SendMessage, action: { + dismissAction() + + openPeer(peer, .chat(textInputState: nil)) + })) + if let isContact = isContact, !isContact { + items.append(ActionSheetButtonItem(title: presentationData.strings.Conversation_AddContact, action: { + dismissAction() + let _ = addContactPeerInteractively(account: account, peerId: peerId, phone: contact.phoneNumber).start() + })) + } + items.append(ActionSheetButtonItem(title: presentationData.strings.UserInfo_TelegramCall, action: { + dismissAction() + callPeer(peerId) + })) + } + items.append(ActionSheetButtonItem(title: presentationData.strings.UserInfo_PhoneCall, action: { + dismissAction() + account.telegramApplicationContext.applicationBindings.openUrl("tel:\(formatPhoneNumber(contact.phoneNumber).replacingOccurrences(of: " ", with: ""))") + })) + controller.setItemGroups([ + ActionSheetItemGroup(items: items), + ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) + ]) + dismissInput() + present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + }) + return true + } + return false +} + +func openChatInstantPage(account: Account, message: Message, navigationController: NavigationController) { + for media in message.media { + if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { + if let _ = content.instantPage { + var textUrl: String? + if let pageUrl = URL(string: content.url) { + inner: for attribute in message.attributes { + if let attribute = attribute as? TextEntitiesMessageAttribute { + for entity in attribute.entities { + switch entity.type { + case let .TextUrl(url): + if let parsedUrl = URL(string: url) { + if pageUrl.scheme == parsedUrl.scheme && pageUrl.host == parsedUrl.host && pageUrl.path == parsedUrl.path { + textUrl = url + } + } + case .Url: + let nsText = message.text as NSString + var entityRange = NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound) + if entityRange.location + entityRange.length > nsText.length { + entityRange.location = max(0, nsText.length - entityRange.length) + entityRange.length = nsText.length - entityRange.location + } + let url = nsText.substring(with: entityRange) + if let parsedUrl = URL(string: url) { + if pageUrl.scheme == parsedUrl.scheme && pageUrl.host == parsedUrl.host && pageUrl.path == parsedUrl.path { + textUrl = url + } + } + default: + break + } + } + break inner + } + } + } + var anchor: String? + if let textUrl = textUrl, let anchorRange = textUrl.range(of: "#") { + anchor = String(textUrl[anchorRange.upperBound...]) + } + + let pageController = InstantPageController(account: account, webPage: webpage, anchor: anchor) + navigationController.pushViewController(pageController) + } + break + } + } +} diff --git a/TelegramUI/OpenUrl.swift b/TelegramUI/OpenUrl.swift new file mode 100644 index 0000000000..279b98917d --- /dev/null +++ b/TelegramUI/OpenUrl.swift @@ -0,0 +1,62 @@ +import Foundation +import Display +import SafariServices + +func openExternalUrl(url: String, presentationData: PresentationData, applicationContext: TelegramApplicationContext, navigationController: NavigationController?) { + if url.lowercased().hasPrefix("tel:") { + applicationContext.applicationBindings.openUrl(url) + return + } + + var parsedUrlValue: URL? + if let parsed = URL(string: url) { + parsedUrlValue = parsed + } + if let parsed = parsedUrlValue, parsed.scheme == nil { + parsedUrlValue = URL(string: "https://" + parsed.absoluteString) + } + + guard let parsedUrl = parsedUrlValue else { + return + } + + if parsedUrl.scheme == "mailto" { + applicationContext.applicationBindings.openUrl(url) + return + } + + if let host = parsedUrl.host?.lowercased() { + if host == "itunes.apple.com" { + if applicationContext.applicationBindings.canOpenUrl(parsedUrl.absoluteString) { + applicationContext.applicationBindings.openUrl(url) + return + } + } + if host == "twitter.com" || host == "mobile.twitter.com" { + if applicationContext.applicationBindings.canOpenUrl("twitter://status") { + applicationContext.applicationBindings.openUrl(url) + return + } + } else if host == "instagram.com" { + if applicationContext.applicationBindings.canOpenUrl("instagram://photo") { + applicationContext.applicationBindings.openUrl(url) + return + } + } + } + + if #available(iOSApplicationExtension 9.0, *) { + if let window = navigationController?.view.window { + let controller = SFSafariViewController(url: parsedUrl) + if #available(iOSApplicationExtension 10.0, *) { + controller.preferredBarTintColor = presentationData.theme.rootController.navigationBar.backgroundColor + controller.preferredControlTintColor = presentationData.theme.rootController.navigationBar.accentTextColor + } + window.rootViewController?.present(controller, animated: true) + } else { + applicationContext.applicationBindings.openUrl(parsedUrl.absoluteString) + } + } else { + applicationContext.applicationBindings.openUrl(url) + } +} diff --git a/TelegramUI/OverlayInstantVideoDecoration.swift b/TelegramUI/OverlayInstantVideoDecoration.swift new file mode 100644 index 0000000000..c4e3ebafb8 --- /dev/null +++ b/TelegramUI/OverlayInstantVideoDecoration.swift @@ -0,0 +1,112 @@ +import Foundation +import AsyncDisplayKit +import Display +import SwiftSignalKit + +private let backgroundImage = UIImage(bundleImageName: "Chat/Message/OverlayInstantVideoShadow")?.precomposed() + +final class OverlayInstantVideoDecoration: UniversalVideoDecoration { + private let tapped: () -> Void + + let backgroundNode: ASDisplayNode? + let contentContainerNode: ASDisplayNode + let foregroundNode: ASDisplayNode? + + private let shadowNode: ASImageNode + private let foregroundContainerNode: ASDisplayNode + + private var contentNode: (ASDisplayNode & UniversalVideoContentNode)? + private var contentNodeSnapshot: UIView? + + private var validLayoutSize: CGSize? + + init(tapped: @escaping () -> Void) { + self.tapped = tapped + + self.shadowNode = ASImageNode() + self.shadowNode.image = backgroundImage + self.backgroundNode = self.shadowNode + + self.contentContainerNode = ASDisplayNode() + self.contentContainerNode.backgroundColor = .white + self.contentContainerNode.cornerRadius = 60.0 + self.contentContainerNode.clipsToBounds = true + + self.foregroundContainerNode = ASDisplayNode() + //self.foregroundContainerNode.addSubnode(self.controlsNode) + self.foregroundNode = self.foregroundContainerNode + + //self.controlsNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(controlsNodeTapGesture(_:)))) + } + + func updateContentNode(_ contentNode: (UniversalVideoContentNode & ASDisplayNode)?) { + if self.contentNode !== contentNode { + let previous = self.contentNode + self.contentNode = contentNode + + if let previous = previous { + if previous.supernode === self.contentContainerNode { + previous.removeFromSupernode() + } + } + + if let contentNode = contentNode { + if contentNode.supernode !== self.contentContainerNode { + self.contentContainerNode.addSubnode(contentNode) + if let validLayoutSize = self.validLayoutSize { + contentNode.frame = CGRect(origin: CGPoint(), size: validLayoutSize) + contentNode.updateLayout(size: validLayoutSize, transition: .immediate) + } + } + } + } + } + + func updateContentNodeSnapshot(_ snapshot: UIView?) { + if self.contentNodeSnapshot !== snapshot { + self.contentNodeSnapshot?.removeFromSuperview() + self.contentNodeSnapshot = snapshot + + if let snapshot = snapshot { + self.contentContainerNode.view.addSubview(snapshot) + if let validLayoutSize = self.validLayoutSize { + snapshot.frame = CGRect(origin: CGPoint(), size: validLayoutSize) + } + } + } + } + + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + self.validLayoutSize = size + + let shadowInsets = UIEdgeInsets(top: 2.0, left: 3.0, bottom: 4.0, right: 3.0) + transition.updateFrame(node: self.shadowNode, frame: CGRect(origin: CGPoint(x: -shadowInsets.left, y: -shadowInsets.top), size: CGSize(width: size.width + shadowInsets.left + shadowInsets.right, height: size.height + shadowInsets.top + shadowInsets.bottom))) + + transition.updateFrame(node: self.foregroundContainerNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height))) + + transition.updateFrame(node: self.contentContainerNode, frame: CGRect(origin: CGPoint(), size: size)) + if let contentNode = self.contentNode { + transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(), size: size)) + contentNode.updateLayout(size: size, transition: transition) + } + + if let contentNodeSnapshot = self.contentNodeSnapshot { + transition.updateFrame(layer: contentNodeSnapshot.layer, frame: CGRect(origin: CGPoint(), size: size)) + } + } + + func tap() { + self.tapped() + } + + func setStatus(_ status: Signal) { + /*self.controlsNode.status = status |> map { value -> MediaPlayerStatus in + if let value = value { + return value + } else { + return MediaPlayerStatus(generationTimestamp: CACurrentMediaTime(), duration: 0.0, timestamp: 0.0, seekId: 0, status: .paused) + } + }*/ + } +} + diff --git a/TelegramUI/OverlayInstantVideoNode.swift b/TelegramUI/OverlayInstantVideoNode.swift new file mode 100644 index 0000000000..0ad5a7f72e --- /dev/null +++ b/TelegramUI/OverlayInstantVideoNode.swift @@ -0,0 +1,131 @@ +import Foundation +import AsyncDisplayKit +import SwiftSignalKit +import Display +import TelegramCore +import Postbox + +final class OverlayInstantVideoNode: OverlayMediaItemNode { + private let content: UniversalVideoContent + private let videoNode: UniversalVideoNode + private let decoration: OverlayInstantVideoDecoration + + private let close: () -> Void + + private var validLayoutSize: CGSize? + + override var group: OverlayMediaItemNodeGroup? { + return OverlayMediaItemNodeGroup(rawValue: 1) + } + + override var isMinimizeable: Bool { + return false + } + + var canAttachContent: Bool = true { + didSet { + self.videoNode.canAttachContent = self.canAttachContent + } + } + + var status: Signal { + return self.videoNode.status + } + + var playbackEnded: (() -> Void)? + + init(postbox: Postbox, audioSession: ManagedAudioSession, manager: UniversalVideoContentManager, content: UniversalVideoContent, close: @escaping () -> Void) { + self.close = close + self.content = content + var togglePlayPauseImpl: (() -> Void)? + let decoration = OverlayInstantVideoDecoration(tapped: { + togglePlayPauseImpl?() + }) + self.videoNode = UniversalVideoNode(postbox: postbox, audioSession: audioSession, manager: manager, decoration: decoration, content: content, priority: .secondaryOverlay, snapshotContentWhenGone: true) + self.decoration = decoration + + super.init() + + togglePlayPauseImpl = { [weak self] in + self?.videoNode.togglePlayPause() + } + + self.addSubnode(self.videoNode) + self.videoNode.ownsContentNodeUpdated = { [weak self] value in + if let strongSelf = self { + let previous = strongSelf.hasAttachedContext + strongSelf.hasAttachedContext = value + if previous != value { + strongSelf.hasAttachedContextUpdated?(value) + } + } + } + + self.videoNode.playbackCompleted = { [weak self] in + self?.playbackEnded?() + } + + self.videoNode.canAttachContent = true + } + + override func didLoad() { + super.didLoad() + } + + override func layout() { + self.updateLayout(self.bounds.size) + } + + override func preferredSizeForOverlayDisplay() -> CGSize { + return CGSize(width: 120.0, height: 120.0) + } + + override func dismiss() { + self.close() + } + + override func updateLayout(_ size: CGSize) { + if size != self.validLayoutSize { + self.updateLayoutImpl(size) + } + } + + private func updateLayoutImpl(_ size: CGSize) { + self.validLayoutSize = size + + self.videoNode.frame = CGRect(origin: CGPoint(), size: size) + self.videoNode.updateLayout(size: size, transition: .immediate) + } + + func play() { + self.videoNode.play() + } + + func playOnceWithSound(playAndRecord: Bool) { + self.videoNode.playOnceWithSound(playAndRecord: playAndRecord) + } + + func pause() { + self.videoNode.pause() + } + + func togglePlayPause() { + self.videoNode.togglePlayPause() + } + + func seek(_ timestamp: Double) { + self.videoNode.seek(timestamp) + } + + func setSoundEnabled(_ soundEnabled: Bool) { + if soundEnabled { + self.videoNode.playOnceWithSound(playAndRecord: true) + } else { + self.videoNode.continuePlayingWithoutSound() + } + } + + func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) { + self.videoNode.setForceAudioToSpeaker(forceAudioToSpeaker) + } +} diff --git a/TelegramUI/OverlayMediaController.swift b/TelegramUI/OverlayMediaController.swift index 8794b5c13a..a0612d4e2b 100644 --- a/TelegramUI/OverlayMediaController.swift +++ b/TelegramUI/OverlayMediaController.swift @@ -35,7 +35,7 @@ public final class OverlayMediaController: ViewController { override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - let updatedLayout = ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: UIEdgeInsets(top: 20.0 + 44.0, left: layout.intrinsicInsets.left, bottom: layout.intrinsicInsets.bottom, right: layout.intrinsicInsets.right), statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight) + let updatedLayout = ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: UIEdgeInsets(top: (layout.statusBarHeight ?? 0.0) + 44.0, left: layout.intrinsicInsets.left, bottom: layout.intrinsicInsets.bottom, right: layout.intrinsicInsets.right), safeInsets: layout.safeInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging) self.controllerNode.containerLayoutUpdated(updatedLayout, transition: transition) } } diff --git a/TelegramUI/OverlayMediaControllerNode.swift b/TelegramUI/OverlayMediaControllerNode.swift index 57c705aff6..b28ed1b0c1 100644 --- a/TelegramUI/OverlayMediaControllerNode.swift +++ b/TelegramUI/OverlayMediaControllerNode.swift @@ -16,11 +16,13 @@ private final class OverlayMediaVideoNodeData { var node: OverlayMediaItemNode var location: CGPoint var isMinimized: Bool + var currentSize: CGSize - init(node: OverlayMediaItemNode, location: CGPoint, isMinimized: Bool) { + init(node: OverlayMediaItemNode, location: CGPoint, isMinimized: Bool, currentSize: CGSize) { self.node = node self.location = location self.isMinimized = isMinimized + self.currentSize = currentSize } } @@ -33,6 +35,9 @@ final class OverlayMediaControllerNode: ASDisplayNode, UIGestureRecognizerDelega private weak var draggingNode: OverlayMediaItemNode? private var draggingStartPosition = CGPoint() + private var pinchingNode: OverlayMediaItemNode? + private var pinchingNodeInitialSize: CGSize? + override init() { super.init() @@ -48,12 +53,20 @@ final class OverlayMediaControllerNode: ASDisplayNode, UIGestureRecognizerDelega panRecognizer.cancelsTouchesInView = false panRecognizer.delegate = self self.view.addGestureRecognizer(panRecognizer) + + let pinchRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(self.pinchGesture(_:))) + pinchRecognizer.cancelsTouchesInView = false + pinchRecognizer.delegate = self + self.view.addGestureRecognizer(pinchRecognizer) } deinit { } func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + if gestureRecognizer is UIPinchGestureRecognizer { + return false + } return true } @@ -74,8 +87,8 @@ final class OverlayMediaControllerNode: ASDisplayNode, UIGestureRecognizerDelega self.validLayout = layout for item in self.videoNodes { - let nodeSize = item.node.preferredSizeForOverlayDisplay() - transition.updateFrame(node: item.node, frame: CGRect(origin: self.nodePosition(layout: layout, size: nodeSize, location: item.location, hidden: !item.node.hasAttachedContext, isMinimized: item.isMinimized, tempExtendedTopInset: item.node.tempExtendedTopInset), size: nodeSize)) + let nodeSize = item.currentSize + transition.updateFrame(node: item.node, frame: CGRect(origin: self.nodePosition(layout: layout, size: nodeSize, location: item.location, hidden: !item.node.customTransition && !item.node.hasAttachedContext, isMinimized: item.isMinimized, tempExtendedTopInset: item.node.tempExtendedTopInset), size: nodeSize)) item.node.updateLayout(nodeSize) } } @@ -86,7 +99,7 @@ final class OverlayMediaControllerNode: ASDisplayNode, UIGestureRecognizerDelega if tempExtendedTopInset { layoutInsets.top += 38.0 } - let inset: CGFloat = 4.0 + let inset: CGFloat = 4.0 + layout.safeInsets.left var result = CGPoint() if location.x.isZero { if isMinimized { @@ -228,15 +241,17 @@ final class OverlayMediaControllerNode: ASDisplayNode, UIGestureRecognizerDelega func addNode(_ node: OverlayMediaItemNode, customTransition: Bool) { var location = CGPoint(x: 1.0, y: 0.0) + node.customTransition = customTransition if let group = node.group { if let groupLocation = self.locationByGroup[group] { location = groupLocation } } - self.videoNodes.append(OverlayMediaVideoNodeData(node: node, location: location, isMinimized: false)) + let nodeData = OverlayMediaVideoNodeData(node: node, location: location, isMinimized: false, currentSize: node.preferredSizeForOverlayDisplay()) + self.videoNodes.append(nodeData) self.addSubnode(node) if let validLayout = self.validLayout { - let nodeSize = node.preferredSizeForOverlayDisplay() + let nodeSize = nodeData.currentSize if self.draggingNode !== node { if customTransition { node.frame = CGRect(origin: self.nodePosition(layout: validLayout, size: nodeSize, location: location, hidden: false, isMinimized: false, tempExtendedTopInset: node.tempExtendedTopInset), size: nodeSize) @@ -273,7 +288,7 @@ final class OverlayMediaControllerNode: ASDisplayNode, UIGestureRecognizerDelega if customTransition { node.removeFromSupernode() } else { - let nodeSize = node.preferredSizeForOverlayDisplay() + let nodeSize = self.videoNodes[index].currentSize node.layer.animateFrame(from: node.layer.frame, to: CGRect(origin: self.nodePosition(layout: validLayout, size: nodeSize, location: self.videoNodes[index].location, hidden: true, isMinimized: self.videoNodes[index].isMinimized, tempExtendedTopInset: node.tempExtendedTopInset), size: nodeSize), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { [weak node] _ in node?.removeFromSupernode() }) @@ -291,9 +306,9 @@ final class OverlayMediaControllerNode: ASDisplayNode, UIGestureRecognizerDelega switch recognizer.state { case .began: if let draggingNode = self.draggingNode, let validLayout = self.validLayout, let index = self.videoNodes.index(where: { $0.node === draggingNode }){ - let nodeSize = draggingNode.preferredSizeForOverlayDisplay() + let nodeSize = self.videoNodes[index].currentSize let previousFrame = draggingNode.frame - draggingNode.frame = CGRect(origin: self.nodePosition(layout: validLayout, size: nodeSize, location: self.videoNodes[index].location, hidden: !draggingNode.hasAttachedContext, isMinimized: self.videoNodes[index].isMinimized, tempExtendedTopInset: draggingNode.tempExtendedTopInset), size: nodeSize) + draggingNode.frame = CGRect(origin: self.nodePosition(layout: validLayout, size: nodeSize, location: self.videoNodes[index].location, hidden: !draggingNode.customTransition && !draggingNode.hasAttachedContext, isMinimized: self.videoNodes[index].isMinimized, tempExtendedTopInset: draggingNode.tempExtendedTopInset), size: nodeSize) draggingNode.layer.animateFrame(from: previousFrame, to: draggingNode.frame, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) self.draggingNode = nil } @@ -320,7 +335,7 @@ final class OverlayMediaControllerNode: ASDisplayNode, UIGestureRecognizerDelega } case .ended, .cancelled: if let draggingNode = self.draggingNode, let validLayout = self.validLayout, let index = self.videoNodes.index(where: { $0.node === draggingNode }){ - let nodeSize = draggingNode.preferredSizeForOverlayDisplay() + let nodeSize = self.videoNodes[index].currentSize let previousFrame = draggingNode.frame let (updatedLocation, shouldDismiss) = self.nodeLocationForPosition(layout: validLayout, position: CGPoint(x: previousFrame.midX, y: previousFrame.midY), velocity: recognizer.velocity(in: self.view), size: nodeSize, tempExtendedTopInset: draggingNode.tempExtendedTopInset) @@ -350,4 +365,46 @@ final class OverlayMediaControllerNode: ASDisplayNode, UIGestureRecognizerDelega break } } + + @objc func pinchGesture(_ recognizer: UIPinchGestureRecognizer) { + switch recognizer.state { + case .began: + let location = recognizer.location(in: self.view) + loop: for videoNode in self.videoNodes { + if videoNode.node.frame.contains(location) { + if videoNode.node.isMinimizeable { + self.pinchingNode = videoNode.node + self.pinchingNodeInitialSize = videoNode.currentSize + } + break loop + } + } + case .changed: + if let validLayout = self.validLayout, let pinchingNode = self.pinchingNode, let initialSize = self.pinchingNodeInitialSize { + let minSize = CGSize(width: 180.0, height: 90.0) + let maxSize = CGSize(width: validLayout.size.width - validLayout.safeInsets.left - validLayout.safeInsets.right - 14.0, height: 500.0) + + let scale = recognizer.scale + var updatedSize = CGSize(width: floor(initialSize.width * scale), height: floor(initialSize.height * scale)) + updatedSize = updatedSize.fitted(maxSize) + if updatedSize.width < minSize.width { + updatedSize = updatedSize.aspectFitted(CGSize(width: minSize.width, height: 1000.0)) + } + + loop: for videoNode in self.videoNodes { + if videoNode.node === pinchingNode { + videoNode.currentSize = updatedSize + break loop + } + } + + self.containerLayoutUpdated(validLayout, transition: .immediate) + } + case .ended, .cancelled: + self.pinchingNode = nil + self.pinchingNodeInitialSize = nil + default: + break + } + } } diff --git a/TelegramUI/OverlayMediaItemNode.swift b/TelegramUI/OverlayMediaItemNode.swift index 151c09853e..82df2c48dc 100644 --- a/TelegramUI/OverlayMediaItemNode.swift +++ b/TelegramUI/OverlayMediaItemNode.swift @@ -36,6 +36,8 @@ class OverlayMediaItemNode: ASDisplayNode { return false } + var customTransition: Bool = false + func setShouldAcquireContext(_ value: Bool) { } diff --git a/TelegramUI/OverlayPlayerController.swift b/TelegramUI/OverlayPlayerController.swift new file mode 100644 index 0000000000..6ff435d47f --- /dev/null +++ b/TelegramUI/OverlayPlayerController.swift @@ -0,0 +1,91 @@ +import Foundation +import TelegramCore +import Postbox +import Display +import SwiftSignalKit + +final class OverlayPlayerController: ViewController { + private let account: Account + let peerId: PeerId + let type: MediaManagerPlayerType + let initialMessageId: MessageId + let initialOrder: MusicPlaybackSettingsOrder + + private weak var parentNavigationController: NavigationController? + + private var animatedIn = false + + private var controllerNode: OverlayPlayerControllerNode { + return self.displayNode as! OverlayPlayerControllerNode + } + + init(account: Account, peerId: PeerId, type: MediaManagerPlayerType, initialMessageId: MessageId, initialOrder: MusicPlaybackSettingsOrder, parentNavigationController: NavigationController?) { + self.account = account + self.peerId = peerId + self.type = type + self.initialMessageId = initialMessageId + self.initialOrder = initialOrder + self.parentNavigationController = parentNavigationController + + super.init(navigationBarTheme: nil) + + self.statusBar.statusBarStyle = .Ignore + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func loadDisplayNode() { + self.displayNode = OverlayPlayerControllerNode(account: self.account, peerId: self.peerId, type: self.type, initialMessageId: self.initialMessageId, initialOrder: self.initialOrder, requestDismiss: { [weak self] in + self?.dismiss() + }, requestShare: { [weak self] messageId in + if let strongSelf = self { + let _ = (strongSelf.account.postbox.modify { modifier -> Message? in + return modifier.getMessage(messageId) + } |> deliverOnMainQueue).start(next: { message in + if let strongSelf = self, let message = message { + let shareController = ShareController(account: strongSelf.account, subject: .messages([message]), showInChat: { message in + if let strongSelf = self, let navigationController = strongSelf.parentNavigationController { + navigateToChatController(navigationController: navigationController, account: strongSelf.account, chatLocation: .peer(message.id.peerId), messageId: messageId, animated: true) + strongSelf.dismiss() + } + }, externalShare: false) + strongSelf.controllerNode.view.endEditing(true) + strongSelf.present(shareController, in: .window(.root)) + } + }) + } + }) + + self.displayNodeDidLoad() + } + + override public func loadView() { + super.loadView() + + self.statusBar.removeFromSupernode() + } + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if !self.animatedIn { + self.animatedIn = true + self.controllerNode.animateIn() + } + } + + override public func dismiss(completion: (() -> Void)? = nil) { + self.controllerNode.animateOut(completion: { [weak self] in + self?.presentingViewController?.dismiss(animated: false, completion: nil) + completion?() + }) + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.controllerNode.containerLayoutUpdated(layout, transition: transition) + } +} diff --git a/TelegramUI/OverlayPlayerControllerNode.swift b/TelegramUI/OverlayPlayerControllerNode.swift new file mode 100644 index 0000000000..2964918f2c --- /dev/null +++ b/TelegramUI/OverlayPlayerControllerNode.swift @@ -0,0 +1,490 @@ +import Foundation +import Display +import AsyncDisplayKit +import SwiftSignalKit +import Postbox +import TelegramCore + +final class OverlayPlayerControllerNode: ViewControllerTracingNode, UIGestureRecognizerDelegate { + private let account: Account + private let peerId: PeerId + private let presentationData: PresentationData + private let type: MediaManagerPlayerType + private let requestDismiss: () -> Void + private let requestShare: (MessageId) -> Void + + private let controllerInteraction: ChatControllerInteraction + + private var currentIsReversed: Bool + + private let dimNode: ASDisplayNode + private let contentNode: ASDisplayNode + private let controlsNode: OverlayPlayerControlsNode + private let historyBackgroundNode: ASDisplayNode + private let historyBackgroundContentNode: ASDisplayNode + private var floatingHeaderOffset: CGFloat? + private var historyNode: ChatHistoryListNode + private var replacementHistoryNode: ChatHistoryListNode? + private var replacementHistoryNodeFloatingOffset: CGFloat? + + private var validLayout: ContainerViewLayout? + + private let replacementHistoryNodeReadyDisposable = MetaDisposable() + + init(account: Account, peerId: PeerId, type: MediaManagerPlayerType, initialMessageId: MessageId, initialOrder: MusicPlaybackSettingsOrder, requestDismiss: @escaping () -> Void, requestShare: @escaping (MessageId) -> Void) { + self.account = account + self.peerId = peerId + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + self.type = type + self.requestDismiss = requestDismiss + self.requestShare = requestShare + + if case .reversed = initialOrder { + self.currentIsReversed = true + } else { + self.currentIsReversed = false + } + + var openMessageImpl: ((MessageId) -> Bool)? + self.controllerInteraction = ChatControllerInteraction(openMessage: { id in + if let openMessageImpl = openMessageImpl { + return openMessageImpl(id) + } else { + return false + } + }, openSecretMessagePreview: { _ in }, closeSecretMessagePreview: { }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _ in }, navigateToMessage: { _, _ in }, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendMessage: { _ in }, sendSticker: { _ in }, sendGif: { _ in }, requestMessageActionCallback: { _, _, _ in }, openUrl: { _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, openMessageShareMenu: { _ in + }, presentController: { _, _ in }, callPeer: { _ in }, longTap: { _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in + }, canSetupReply: { + return false + }, automaticMediaDownloadSettings: .none) + + self.dimNode = ASDisplayNode() + self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) + + self.contentNode = ASDisplayNode() + + self.controlsNode = OverlayPlayerControlsNode(postbox: account.postbox, theme: self.presentationData.theme, status: account.telegramApplicationContext.mediaManager.musicMediaPlayerState) + + self.historyBackgroundNode = ASDisplayNode() + self.historyBackgroundNode.isLayerBacked = true + + self.historyBackgroundContentNode = ASDisplayNode() + self.historyBackgroundContentNode.isLayerBacked = true + self.historyBackgroundContentNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor + + self.historyBackgroundNode.addSubnode(self.historyBackgroundContentNode) + + let tagMask: MessageTags + switch type { + case .music: + tagMask = .music + case .voice: + tagMask = .voiceOrInstantVideo + } + + self.historyNode = ChatHistoryListNode(account: account, chatLocation: .peer(peerId), tagMask: tagMask, messageId: initialMessageId, controllerInteraction: self.controllerInteraction, selectedMessages: .single(nil), mode: .list(search: false, reversed: currentIsReversed)) + + super.init() + + self.backgroundColor = nil + self.isOpaque = false + + self.historyNode.preloadPages = true + self.historyNode.stackFromBottom = true + self.historyNode.updateFloatingHeaderOffset = { [weak self] offset, transition in + if let strongSelf = self { + strongSelf.updateFloatingHeaderOffset(offset: offset, transition: transition) + } + } + + self.controlsNode.updateIsExpanded = { [weak self] in + if let strongSelf = self, let validLayout = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(validLayout, transition: .animated(duration: 0.3, curve: .spring)) + } + } + + self.controlsNode.requestCollapse = { [weak self] in + self?.requestDismiss() + } + + self.controlsNode.requestShare = { [weak self] messageId in + self?.requestShare(messageId) + } + + self.controlsNode.updateOrder = { [weak self] order in + if let strongSelf = self { + var reversed = false + if case .reversed = order { + reversed = true + } + if reversed != strongSelf.currentIsReversed { + strongSelf.currentIsReversed = reversed + if let itemId = strongSelf.controlsNode.currentItemId as? PeerMessagesMediaPlaylistItemId { + strongSelf.transitionToUpdatedHistoryNode(atMessage: itemId.messageId) + } + } + } + } + + self.controlsNode.control = { [weak self] action in + if let strongSelf = self { + strongSelf.account.telegramApplicationContext.mediaManager.playlistControl(action, type: strongSelf.type) + } + } + + self.addSubnode(self.dimNode) + self.addSubnode(self.contentNode) + self.contentNode.addSubnode(self.historyBackgroundNode) + self.contentNode.addSubnode(self.historyNode) + self.contentNode.addSubnode(self.controlsNode) + + self.historyNode.beganInteractiveDragging = { [weak self] in + self?.controlsNode.collapse() + } + + openMessageImpl = { [weak self] id in + if let strongSelf = self, strongSelf.isNodeLoaded, let message = strongSelf.historyNode.messageInCurrentHistoryView(id) { + return openChatMessage(account: strongSelf.account, message: message, reverseMessageGalleryOrder: false, navigationController: nil, dismissInput: { }, present: { _, _ in }, transitionNode: { _, _ in return nil }, addToTransitionSurface: { _ in }, openUrl: { _ in }, openPeer: { _, _ in }, callPeer: { _ in }, sendSticker: { _ in }, setupTemporaryHiddenMedia: { _, _, _ in }) + } + return false + } + } + + deinit { + self.replacementHistoryNodeReadyDisposable.dispose() + } + + override func didLoad() { + super.didLoad() + + self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) + + let panRecognizer = DirectionalPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))) + panRecognizer.delegate = self + panRecognizer.delaysTouchesBegan = false + panRecognizer.cancelsTouchesInView = true + self.view.addGestureRecognizer(panRecognizer) + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + self.validLayout = layout + + transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + transition.updateFrame(node: self.contentNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + + let layoutTopInset: CGFloat = max(layout.statusBarHeight ?? 0.0, layout.safeInsets.top) + + var insets = UIEdgeInsets() + insets.left = layout.safeInsets.left + insets.right = layout.safeInsets.right + insets.bottom = layout.intrinsicInsets.bottom + + let maxHeight = layout.size.height - layoutTopInset - floor(56.0 * 0.5) + + let controlsHeight = OverlayPlayerControlsNode.heightForLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, maxHeight: maxHeight, isExpanded: self.controlsNode.isExpanded) + + let listTopInset = layoutTopInset + controlsHeight + + let listNodeSize = CGSize(width: layout.size.width, height: layout.size.height - listTopInset) + + insets.top = max(0.0, listNodeSize.height - floor(56.0 * 3.5)) + + transition.updateFrame(node: self.historyNode, frame: CGRect(origin: CGPoint(x: 0.0, y: listTopInset), size: listNodeSize)) + + var duration: Double = 0.0 + var curve: UInt = 0 + switch transition { + case .immediate: + break + case let .animated(animationDuration, animationCurve): + duration = animationDuration + switch animationCurve { + case .easeInOut: + break + case .spring: + curve = 7 + } + } + + let listViewCurve: ListViewAnimationCurve + if curve == 7 { + listViewCurve = .Spring(duration: duration) + } else { + listViewCurve = .Default + } + + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: listNodeSize, insets: insets, duration: duration, curve: listViewCurve) + self.historyNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets) + if let replacementHistoryNode = replacementHistoryNode { + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: listNodeSize, insets: insets, duration: 0.0, curve: .Default) + replacementHistoryNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets) + } + } + + func animateIn() { + self.layer.animateBoundsOriginYAdditive(from: -self.bounds.size.height, to: 0.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.dimNode.layer.animatePosition(from: CGPoint(x: 0.0, y: -self.bounds.size.height), to: CGPoint(), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: true, additive: true) + } + + func animateOut(completion: (() -> Void)?) { + self.layer.animateBoundsOriginYAdditive(from: self.bounds.origin.y, to: -self.bounds.size.height, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in + completion?() + }) + self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + self.dimNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -self.bounds.size.height), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if !self.bounds.contains(point) { + return nil + } + if point.y < self.controlsNode.frame.minY { + return self.dimNode.view + } + let result = super.hitTest(point, with: event) + if self.controlsNode.frame.contains(point) { + if result == self.historyNode.view { + return self.view + } + } + return result + } + + @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.requestDismiss() + } + } + + override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + if let recognizer = gestureRecognizer as? UIPanGestureRecognizer { + let location = recognizer.location(in: self.view) + /*if let view = super.hitTest(location, with: nil) { + if view != self.view && view.gestureRecognizers != nil { + return false + } + }*/ + } + return true + } + + @objc func panGesture(_ recognizer: UIPanGestureRecognizer) { + switch recognizer.state { + case .began: + break + case .changed: + let translation = recognizer.translation(in: self.contentNode.view) + var bounds = self.contentNode.bounds + bounds.origin.y = -translation.y + bounds.origin.y = min(0.0, bounds.origin.y) + if bounds.origin.y < 0.0 { + //let delta = -bounds.origin.y + //bounds.origin.y = -((1.0 - (1.0 / (((delta) * 0.55 / (50.0)) + 1.0))) * 50.0) + } + + self.contentNode.bounds = bounds + case .ended: + let translation = recognizer.translation(in: self.contentNode.view) + var bounds = self.contentNode.bounds + bounds.origin.y = -translation.y + if bounds.origin.y < 0.0 { + //let delta = -bounds.origin.y + //bounds.origin.y = -((1.0 - (1.0 / (((delta) * 0.55 / (50.0)) + 1.0))) * 50.0) + } + + let velocity = recognizer.velocity(in: self.contentNode.view) + + if (bounds.minY < -60.0 || velocity.y > 300.0) { + self.requestDismiss() + } else { + let previousBounds = self.bounds + var bounds = self.bounds + bounds.origin.y = 0.0 + self.contentNode.bounds = bounds + self.contentNode.layer.animateBounds(from: previousBounds, to: self.contentNode.bounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionEaseInEaseOut) + } + case .cancelled: + let previousBounds = self.contentNode.bounds + var bounds = self.contentNode.bounds + bounds.origin.y = 0.0 + self.contentNode.bounds = bounds + self.contentNode.layer.animateBounds(from: previousBounds, to: self.contentNode.bounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionEaseInEaseOut) + default: + break + } + } + + private func updateFloatingHeaderOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { + guard let validLayout = self.validLayout else { + return + } + + self.floatingHeaderOffset = offset + + let layoutTopInset: CGFloat = max(validLayout.statusBarHeight ?? 0.0, validLayout.safeInsets.top) + + let maxHeight = validLayout.size.height - layoutTopInset - floor(56.0 * 0.5) + + let controlsHeight = self.controlsNode.updateLayout(width: validLayout.size.width, leftInset: validLayout.safeInsets.left, rightInset: validLayout.safeInsets.right, maxHeight: maxHeight, transition: transition) + + let listTopInset = layoutTopInset + controlsHeight + + let rawControlsOffset = offset + listTopInset - controlsHeight + let controlsOffset = max(layoutTopInset, rawControlsOffset) + let isOverscrolling = rawControlsOffset <= layoutTopInset + let controlsFrame = CGRect(origin: CGPoint(x: 0.0, y: controlsOffset), size: CGSize(width: validLayout.size.width, height: controlsHeight)) + + let previousFrame = self.controlsNode.frame + + if !controlsFrame.equalTo(previousFrame) { + self.controlsNode.frame = controlsFrame + + let positionDelta = CGPoint(x: controlsFrame.minX - previousFrame.minX, y: controlsFrame.minY - previousFrame.minY) + + transition.animateOffsetAdditive(node: self.controlsNode, offset: positionDelta.y) + } + + transition.updateAlpha(node: self.controlsNode.separatorNode, alpha: isOverscrolling ? 1.0 : 0.0) + + let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: controlsFrame.maxY), size: CGSize(width: validLayout.size.width, height: validLayout.size.height)) + + let previousBackgroundFrame = self.historyBackgroundNode.frame + + if !backgroundFrame.equalTo(previousBackgroundFrame) { + self.historyBackgroundNode.frame = backgroundFrame + self.historyBackgroundContentNode.frame = CGRect(origin: CGPoint(), size: backgroundFrame.size) + + let positionDelta = CGPoint(x: backgroundFrame.minX - previousBackgroundFrame.minX, y: backgroundFrame.minY - previousBackgroundFrame.minY) + + transition.animateOffsetAdditive(node: self.historyBackgroundNode, offset: positionDelta.y) + } + } + + private func transitionToUpdatedHistoryNode(atMessage messageId: MessageId) { + let tagMask: MessageTags + switch self.type { + case .music: + tagMask = .music + case .voice: + tagMask = .voiceOrInstantVideo + } + + let historyNode = ChatHistoryListNode(account: self.account, chatLocation: .peer(self.peerId), tagMask: tagMask, messageId: messageId, controllerInteraction: self.controllerInteraction, selectedMessages: .single(nil), mode: .list(search: false, reversed: self.currentIsReversed)) + historyNode.preloadPages = true + historyNode.stackFromBottom = true + historyNode.updateFloatingHeaderOffset = { [weak self] offset, _ in + self?.replacementHistoryNodeFloatingOffset = offset + } + self.replacementHistoryNode = historyNode + if let layout = self.validLayout { + let layoutTopInset: CGFloat = max(layout.statusBarHeight ?? 0.0, layout.safeInsets.top) + + var insets = UIEdgeInsets() + insets.left = layout.safeInsets.left + insets.right = layout.safeInsets.right + insets.bottom = layout.intrinsicInsets.bottom + + let maxHeight = layout.size.height - layoutTopInset - floor(56.0 * 0.5) + + let controlsHeight = OverlayPlayerControlsNode.heightForLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, maxHeight: maxHeight, isExpanded: self.controlsNode.isExpanded) + + let listTopInset = layoutTopInset + controlsHeight + + let listNodeSize = CGSize(width: layout.size.width, height: layout.size.height - listTopInset) + + insets.top = max(0.0, listNodeSize.height - floor(56.0 * 3.5)) + + historyNode.frame = CGRect(origin: CGPoint(x: 0.0, y: listTopInset), size: listNodeSize) + + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: listNodeSize, insets: insets, duration: 0.0, curve: .Default) + historyNode.updateLayout(transition: .immediate, updateSizeAndInsets: updateSizeAndInsets) + } + self.replacementHistoryNodeReadyDisposable.set((historyNode.historyState.get() |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in + if let strongSelf = self { + strongSelf.replaceWithReadyUpdatedHistoryNode() + } + })) + } + + private func replaceWithReadyUpdatedHistoryNode() { + if let replacementHistoryNode = self.replacementHistoryNode { + self.replacementHistoryNode = nil + + let previousHistoryNode = self.historyNode + previousHistoryNode.disconnect() + self.contentNode.insertSubnode(replacementHistoryNode, belowSubnode: self.historyNode) + self.historyNode = replacementHistoryNode + + if let validLayout = self.validLayout, let offset = self.replacementHistoryNodeFloatingOffset, let previousOffset = self.floatingHeaderOffset { + let offsetDelta = offset - previousOffset + + let layoutTopInset: CGFloat = max(validLayout.statusBarHeight ?? 0.0, validLayout.safeInsets.top) + + let maxHeight = validLayout.size.height - layoutTopInset - floor(56.0 * 0.5) + + let controlsHeight = OverlayPlayerControlsNode.heightForLayout(width: validLayout.size.width, leftInset: validLayout.safeInsets.left, rightInset: validLayout.safeInsets.right, maxHeight: maxHeight, isExpanded: self.controlsNode.isExpanded) + + let listTopInset = layoutTopInset + controlsHeight + + let controlsBottomOffset = max(layoutTopInset, offset + listTopInset) + + let previousBackgroundNode = ASDisplayNode() + previousBackgroundNode.isLayerBacked = true + previousBackgroundNode.backgroundColor = self.historyBackgroundContentNode.backgroundColor + self.contentNode.insertSubnode(previousBackgroundNode, belowSubnode: previousHistoryNode) + previousBackgroundNode.frame = self.historyBackgroundNode.frame + + previousBackgroundNode.layer.animateFrame(from: previousBackgroundNode.frame, to: CGRect(origin: CGPoint(x: 0.0, y: controlsBottomOffset), size: validLayout.size), duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + + self.updateFloatingHeaderOffset(offset: offset, transition: .animated(duration: 0.4, curve: .spring)) + previousHistoryNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previousHistoryNode] _ in + previousHistoryNode?.removeFromSupernode() + }) + previousHistoryNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: offsetDelta), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: true, additive: true) + previousBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previousBackgroundNode] _ in + previousBackgroundNode?.removeFromSupernode() + }) + self.historyNode.layer.animatePosition(from: CGPoint(x: 0.0, y: -offsetDelta), to: CGPoint(), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: true, additive: true) + } else { + previousHistoryNode.removeFromSupernode() + } + + self.historyNode.updateFloatingHeaderOffset = { [weak self] offset, transition in + if let strongSelf = self { + strongSelf.updateFloatingHeaderOffset(offset: offset, transition: transition) + } + } + + self.historyNode.beganInteractiveDragging = { [weak self] in + self?.controlsNode.collapse() + } + + if let layout = self.validLayout { + let layoutTopInset: CGFloat = max(layout.statusBarHeight ?? 0.0, layout.safeInsets.top) + + var insets = UIEdgeInsets() + insets.left = layout.safeInsets.left + insets.right = layout.safeInsets.right + insets.bottom = layout.intrinsicInsets.bottom + + let maxHeight = layout.size.height - layoutTopInset - floor(56.0 * 0.5) + + let controlsHeight = OverlayPlayerControlsNode.heightForLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, maxHeight: maxHeight, isExpanded: self.controlsNode.isExpanded) + + let listTopInset = layoutTopInset + controlsHeight + + let listNodeSize = CGSize(width: layout.size.width, height: layout.size.height - listTopInset) + + insets.top = max(0.0, listNodeSize.height - floor(56.0 * 3.5)) + + self.historyNode.frame = CGRect(origin: CGPoint(x: 0.0, y: listTopInset), size: listNodeSize) + + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: listNodeSize, insets: insets, duration: 0.0, curve: .Default) + self.historyNode.updateLayout(transition: .immediate, updateSizeAndInsets: updateSizeAndInsets) + + self.historyNode.recursivelyEnsureDisplaySynchronously(true) + } + } + } +} diff --git a/TelegramUI/OverlayPlayerControlsNode.swift b/TelegramUI/OverlayPlayerControlsNode.swift new file mode 100644 index 0000000000..b9447596d3 --- /dev/null +++ b/TelegramUI/OverlayPlayerControlsNode.swift @@ -0,0 +1,534 @@ +import Foundation +import AsyncDisplayKit +import Display +import Postbox +import SwiftSignalKit + +private func generateBackground(theme: PresentationTheme) -> UIImage? { + return generateImage(CGSize(width: 20.0, height: 10.0 + 8.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setShadow(offset: CGSize(width: 0.0, height: -4.0), blur: 20.0, color: UIColor(white: 0.0, alpha: 0.3).cgColor) + context.setFillColor(theme.list.plainBackgroundColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: 8.0), size: CGSize(width: 20.0, height: 20.0))) + })?.stretchableImage(withLeftCapWidth: 10, topCapHeight: 10 + 8) +} + +private func generateShareIcon(theme: PresentationTheme) -> UIImage? { + return generateImage(CGSize(width: 19.0, height: 5.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(theme.list.itemAccentColor.cgColor) + for i in 0 ..< 3 { + context.fillEllipse(in: CGRect(origin: CGPoint(x: CGFloat(i) * (5.0 + 2.0), y: 0.0), size: CGSize(width: 5.0, height: 5.0))) + } + }) +} + +private let titleFont = Font.medium(16.0) +private let descriptionFont = Font.regular(12.0) + +private func stringsForDisplayData(_ data: SharedMediaPlaybackDisplayData?, theme: PresentationTheme) -> (NSAttributedString?, NSAttributedString?) { + var titleString: NSAttributedString? + var descriptionString: NSAttributedString? + + if let data = data { + let titleText: String + let subtitleText: String + switch data { + case let .music(title, performer, _): + titleText = title ?? "Unknown Track" + subtitleText = performer ?? "Unknown Artist" + case .voice, .instantVideo: + titleText = "" + subtitleText = "" + } + + titleString = NSAttributedString(string: titleText, font: titleFont, textColor: theme.list.itemPrimaryTextColor) + descriptionString = NSAttributedString(string: subtitleText, font: descriptionFont, textColor: theme.list.itemSecondaryTextColor) + } + + return (titleString, descriptionString) +} + +final class OverlayPlayerControlsNode: ASDisplayNode { + private let postbox: Postbox + private let theme: PresentationTheme + + private let backgroundNode: ASImageNode + + private let collapseNode: HighlightableButtonNode + + private let albumArtNode: TransformImageNode + private var largeAlbumArtNode: TransformImageNode? + private let titleNode: TextNode + private let descriptionNode: TextNode + private let shareNode: HighlightableButtonNode + + private let scrubberNode: MediaPlayerScrubbingNode + private let leftDurationLabel: MediaPlayerTimeTextNode + private let rightDurationLabel: MediaPlayerTimeTextNode + + private let backwardButton: IconButtonNode + private let forwardButton: IconButtonNode + + private var currentIsPaused: Bool? + private let playPauseButton: IconButtonNode + + private var currentOrder: MusicPlaybackSettingsOrder? + private let orderButton: IconButtonNode + + private var currentLooping: MusicPlaybackSettingsLooping? + private let loopingButton: IconButtonNode + + let separatorNode: ASDisplayNode + + private(set) var isExpanded = false + var updateIsExpanded: (() -> Void)? + + var requestCollapse: (() -> Void)? + var requestShare: ((MessageId) -> Void)? + + var updateOrder: ((MusicPlaybackSettingsOrder) -> Void)? + var control: ((SharedMediaPlayerControlAction) -> Void)? + + private(set) var currentItemId: SharedMediaPlaylistItemId? + private var displayData: SharedMediaPlaybackDisplayData? + private var currentAlbumArtInitialized = false + private var currentAlbumArt: SharedMediaPlaybackAlbumArt? + private var statusDisposable: Disposable? + + private var validLayout: (width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat)? + + init(postbox: Postbox, theme: PresentationTheme, status: Signal) { + self.postbox = postbox + self.theme = theme + + self.backgroundNode = ASImageNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.displayWithoutProcessing = true + self.backgroundNode.displaysAsynchronously = false + self.backgroundNode.image = generateBackground(theme: theme) + + self.collapseNode = HighlightableButtonNode() + self.collapseNode.displaysAsynchronously = false + self.collapseNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/CollapseArrow"), color: theme.list.controlSecondaryColor), for: []) + + self.albumArtNode = TransformImageNode() + + self.titleNode = TextNode() + self.titleNode.isLayerBacked = true + self.titleNode.displaysAsynchronously = false + + self.descriptionNode = TextNode() + self.descriptionNode.isLayerBacked = true + self.descriptionNode.displaysAsynchronously = false + + self.shareNode = HighlightableButtonNode() + self.shareNode.setImage(generateShareIcon(theme: theme), for: []) + + self.scrubberNode = MediaPlayerScrubbingNode(content: .standard(lineHeight: 3.0, lineCap: .round, scrubberHandle: .circle, backgroundColor: theme.list.controlSecondaryColor, foregroundColor: theme.list.itemAccentColor)) + self.leftDurationLabel = MediaPlayerTimeTextNode(textColor: theme.list.itemSecondaryTextColor) + self.rightDurationLabel = MediaPlayerTimeTextNode(textColor: theme.list.itemSecondaryTextColor) + self.rightDurationLabel.mode = .reversed + self.rightDurationLabel.alignment = .right + + self.backwardButton = IconButtonNode() + self.backwardButton.displaysAsynchronously = false + + self.forwardButton = IconButtonNode() + self.forwardButton.displaysAsynchronously = false + + self.orderButton = IconButtonNode() + self.orderButton.displaysAsynchronously = false + + self.loopingButton = IconButtonNode() + self.loopingButton.displaysAsynchronously = false + + self.playPauseButton = IconButtonNode() + self.playPauseButton.displaysAsynchronously = false + + self.backwardButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Previous"), color: theme.list.itemPrimaryTextColor) + self.forwardButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Next"), color: theme.list.itemPrimaryTextColor) + + self.separatorNode = ASDisplayNode() + self.separatorNode.isLayerBacked = true + self.separatorNode.backgroundColor = theme.list.itemPlainSeparatorColor + + super.init() + + self.addSubnode(self.backgroundNode) + + self.addSubnode(self.collapseNode) + + self.addSubnode(self.albumArtNode) + self.addSubnode(self.titleNode) + self.addSubnode(self.descriptionNode) + self.addSubnode(self.shareNode) + + self.addSubnode(self.scrubberNode) + self.addSubnode(self.leftDurationLabel) + self.addSubnode(self.rightDurationLabel) + + self.addSubnode(self.orderButton) + self.addSubnode(self.loopingButton) + self.addSubnode(self.backwardButton) + self.addSubnode(self.forwardButton) + self.addSubnode(self.playPauseButton) + + self.addSubnode(self.separatorNode) + + let mappedStatus = status |> map { value -> MediaPlayerStatus in + if let value = value { + return value.status + } else { + return MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, timestamp: 0.0, seekId: 0, status: .paused) + } + } + self.scrubberNode.status = mappedStatus + self.leftDurationLabel.status = mappedStatus + self.rightDurationLabel.status = mappedStatus + + self.statusDisposable = (status |> deliverOnMainQueue).start(next: { [weak self] value in + if let strongSelf = self { + if !areSharedMediaPlaylistItemIdsEqual(value?.item.id, strongSelf.currentItemId) { + strongSelf.currentItemId = value?.item.id + strongSelf.scrubberNode.ignoreSeekId = nil + } + var displayData: SharedMediaPlaybackDisplayData? + if let value = value { + let isPaused: Bool + switch value.status.status { + case .playing: + isPaused = false + case .paused: + isPaused = true + case let .buffering(_, whilePlaying): + isPaused = !whilePlaying + } + if strongSelf.currentIsPaused != isPaused { + strongSelf.currentIsPaused = isPaused + + if isPaused { + strongSelf.playPauseButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Play"), color: strongSelf.theme.list.itemPrimaryTextColor) + } else { + strongSelf.playPauseButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Pause"), color: strongSelf.theme.list.itemPrimaryTextColor) + } + } + + strongSelf.playPauseButton.isEnabled = true + strongSelf.backwardButton.isEnabled = true + strongSelf.forwardButton.isEnabled = true + + displayData = value.item.displayData + + if value.order != strongSelf.currentOrder { + strongSelf.updateOrder?(value.order) + strongSelf.currentOrder = value.order + switch value.order { + case .regular: + strongSelf.orderButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/OrderReverse"), color: strongSelf.theme.list.itemPrimaryTextColor) + case .reversed: + strongSelf.orderButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/OrderReverse"), color: strongSelf.theme.list.itemAccentColor) + case .random: + strongSelf.orderButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/OrderRandom"), color: strongSelf.theme.list.itemPrimaryTextColor) + } + } + if value.looping != strongSelf.currentLooping { + strongSelf.currentLooping = value.looping + + switch value.looping { + case .none: + strongSelf.loopingButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Repeat"), color: strongSelf.theme.list.itemPrimaryTextColor) + case .item: + strongSelf.loopingButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/RepeatOne"), color: strongSelf.theme.list.itemPrimaryTextColor) + case .all: + strongSelf.loopingButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Repeat"), color: strongSelf.theme.list.itemAccentColor) + } + } + } else { + strongSelf.playPauseButton.isEnabled = false + strongSelf.backwardButton.isEnabled = false + strongSelf.forwardButton.isEnabled = false + + displayData = nil + } + + if strongSelf.displayData != displayData { + strongSelf.displayData = displayData + strongSelf.updateLabels(transition: .immediate) + } + } + }) + + self.scrubberNode.seek = { [weak self] value in + self?.control?(.seek(value)) + } + + self.collapseNode.addTarget(self, action: #selector(self.collapsePressed), forControlEvents: .touchUpInside) + self.shareNode.addTarget(self, action: #selector(self.sharePressed), forControlEvents: .touchUpInside) + self.orderButton.addTarget(self, action: #selector(self.orderPressed), forControlEvents: .touchUpInside) + self.loopingButton.addTarget(self, action: #selector(self.loopingPressed), forControlEvents: .touchUpInside) + self.backwardButton.addTarget(self, action: #selector(self.backwardPressed), forControlEvents: .touchUpInside) + self.forwardButton.addTarget(self, action: #selector(self.forwardPressed), forControlEvents: .touchUpInside) + self.playPauseButton.addTarget(self, action: #selector(self.playPausePressed), forControlEvents: .touchUpInside) + } + + deinit { + self.statusDisposable?.dispose() + } + + override func didLoad() { + super.didLoad() + + self.albumArtNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.albumArtTap(_:)))) + } + + private func updateLabels(transition: ContainedViewLayoutTransition) { + guard let (width, leftInset, rightInset, maxHeight) = self.validLayout else { + return + } + + let panelHeight = OverlayPlayerControlsNode.heightForLayout(width: width, leftInset: leftInset, rightInset: rightInset, maxHeight: maxHeight, isExpanded: self.isExpanded) + + let sideInset: CGFloat = 20.0 + + let infoLabelsLeftInset: CGFloat = 64.0 + let infoLabelsRightInset: CGFloat = 32.0 + + let infoVerticalOrigin: CGFloat = panelHeight - OverlayPlayerControlsNode.basePanelHeight + 36.0 + + let (titleString, descriptionString) = stringsForDisplayData(self.displayData, theme: self.theme) + let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - sideInset * 2.0 - infoLabelsLeftInset - infoLabelsRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .left, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) + let makeDescriptionLayout = TextNode.asyncLayout(self.descriptionNode) + let (descriptionLayout, descriptionApply) = makeDescriptionLayout(TextNodeLayoutArguments(attributedString: descriptionString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - sideInset * 2.0 - infoLabelsLeftInset - infoLabelsRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .left, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) + + transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: self.isExpanded ? floor((width - titleLayout.size.width) / 2.0) : (sideInset + infoLabelsLeftInset), y: infoVerticalOrigin + 1.0), size: titleLayout.size)) + let _ = titleApply() + + transition.updateFrame(node: self.descriptionNode, frame: CGRect(origin: CGPoint(x: self.isExpanded ? floor((width - descriptionLayout.size.width) / 2.0) : (sideInset + infoLabelsLeftInset), y: infoVerticalOrigin + 27.0), size: descriptionLayout.size)) + let _ = descriptionApply() + + var albumArt: SharedMediaPlaybackAlbumArt? + if let displayData = self.displayData { + switch displayData { + case let .music(_, _, value): + albumArt = value + default: + break + } + } + if self.currentAlbumArt != albumArt || !self.currentAlbumArtInitialized { + self.currentAlbumArtInitialized = true + self.currentAlbumArt = albumArt + self.albumArtNode.setSignal(playerAlbumArt(postbox: self.postbox, albumArt: albumArt, thumbnail: true)) + if let largeAlbumArtNode = self.largeAlbumArtNode { + largeAlbumArtNode.setSignal(playerAlbumArt(postbox: self.postbox, albumArt: albumArt, thumbnail: false)) + } + } + } + + static let basePanelHeight: CGFloat = 220.0 + + static func heightForLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, isExpanded: Bool) -> CGFloat { + var panelHeight: CGFloat = OverlayPlayerControlsNode.basePanelHeight + if isExpanded { + let sideInset: CGFloat = 20.0 + panelHeight += width - leftInset - rightInset - sideInset * 2.0 + 24.0 + } + return min(panelHeight, maxHeight) + } + + func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { + self.validLayout = (width, leftInset, rightInset, maxHeight) + + let panelHeight = OverlayPlayerControlsNode.heightForLayout(width: width, leftInset: leftInset, rightInset: rightInset, maxHeight: maxHeight, isExpanded: self.isExpanded) + + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelHeight), size: CGSize(width: width, height: UIScreenPixel))) + + transition.updateFrame(node: self.collapseNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 2.0), size: CGSize(width: width, height: 30.0))) + + let sideInset: CGFloat = 20.0 + let sideButtonsInset: CGFloat = sideInset + 30.0 + + let infoVerticalOrigin: CGFloat = panelHeight - OverlayPlayerControlsNode.basePanelHeight + 36.0 + + self.updateLabels(transition: transition) + + transition.updateFrame(node: self.shareNode, frame: CGRect(origin: CGPoint(x: width - rightInset - sideInset - 32.0, y: infoVerticalOrigin + 2.0), size: CGSize(width: 42.0, height: 42.0))) + + let albumArtSize = CGSize(width: 48.0, height: 48.0) + let makeAlbumArtLayout = self.albumArtNode.asyncLayout() + let applyAlbumArt = makeAlbumArtLayout(TransformImageArguments(corners: ImageCorners(radius: 4.0), imageSize: albumArtSize, boundingSize: albumArtSize, intrinsicInsets: UIEdgeInsets())) + applyAlbumArt() + let albumArtFrame = CGRect(origin: CGPoint(x: sideInset, y: infoVerticalOrigin - 1.0), size: albumArtSize) + let previousAlbumArtNodeFrame = self.albumArtNode.frame + transition.updateFrame(node: self.albumArtNode, frame: albumArtFrame) + + if self.isExpanded { + let largeAlbumArtNode: TransformImageNode + var animateIn = false + if let current = self.largeAlbumArtNode { + largeAlbumArtNode = current + } else { + animateIn = true + largeAlbumArtNode = TransformImageNode() + if self.isNodeLoaded { + largeAlbumArtNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.albumArtTap(_:)))) + } + self.largeAlbumArtNode = largeAlbumArtNode + self.addSubnode(largeAlbumArtNode) + if self.currentAlbumArtInitialized { + largeAlbumArtNode.setSignal(playerAlbumArt(postbox: self.postbox, albumArt: self.currentAlbumArt, thumbnail: false)) + } + } + + let albumArtHeight = max(1.0, panelHeight - OverlayPlayerControlsNode.basePanelHeight - 24.0) + + let largeAlbumArtSize = CGSize(width: albumArtHeight, height: albumArtHeight) + let makeLargeAlbumArtLayout = largeAlbumArtNode.asyncLayout() + let applyLargeAlbumArt = makeLargeAlbumArtLayout(TransformImageArguments(corners: ImageCorners(radius: 4.0), imageSize: largeAlbumArtSize, boundingSize: largeAlbumArtSize, intrinsicInsets: UIEdgeInsets())) + applyLargeAlbumArt() + + let largeAlbumArtFrame = CGRect(origin: CGPoint(x: floor((width - largeAlbumArtSize.width) / 2.0), y: 34.0), size: largeAlbumArtSize) + + if animateIn && transition.isAnimated { + largeAlbumArtNode.frame = largeAlbumArtFrame + transition.animatePositionAdditive(node: largeAlbumArtNode, offset: CGPoint(x: previousAlbumArtNodeFrame.center.x - largeAlbumArtFrame.center.x, y: previousAlbumArtNodeFrame.center.y - largeAlbumArtFrame.center.y)) + //largeAlbumArtNode.layer.animatePosition(from: CGPoint(x: -50.0, y: 0.0), to: CGPoint(), duration: 0.15, timingFunction: kCAMediaTimingFunctionEaseInEaseOut, additive: true) + transition.animateTransformScale(node: largeAlbumArtNode, from: previousAlbumArtNodeFrame.size.height / largeAlbumArtFrame.size.height) + largeAlbumArtNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.12) + if let copyView = self.albumArtNode.view.snapshotContentTree() { + copyView.frame = previousAlbumArtNodeFrame + copyView.center = largeAlbumArtFrame.center + self.view.insertSubview(copyView, belowSubview: largeAlbumArtNode.view) + transition.animatePositionAdditive(layer: copyView.layer, offset: CGPoint(x: previousAlbumArtNodeFrame.center.x - largeAlbumArtFrame.center.x, y: previousAlbumArtNodeFrame.center.y - largeAlbumArtFrame.center.y), completion: { [weak copyView] in + copyView?.removeFromSuperview() + }) + //copyView.layer.animatePosition(from: CGPoint(x: -50.0, y: 0.0), to: CGPoint(), duration: 0.15, timingFunction: kCAMediaTimingFunctionEaseInEaseOut, additive: true) + copyView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.28, removeOnCompletion: false) + transition.updateTransformScale(layer: copyView.layer, scale: largeAlbumArtFrame.size.height / previousAlbumArtNodeFrame.size.height) + } + } else { + transition.updateFrame(node: largeAlbumArtNode, frame: largeAlbumArtFrame) + } + self.albumArtNode.isHidden = true + } else if let largeAlbumArtNode = self.largeAlbumArtNode { + self.largeAlbumArtNode = nil + self.albumArtNode.isHidden = false + if transition.isAnimated { + transition.animatePosition(node: self.albumArtNode, from: largeAlbumArtNode.frame.center) + transition.animateTransformScale(node: self.albumArtNode, from: largeAlbumArtNode.frame.height / self.albumArtNode.frame.height) + self.albumArtNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.12) + + transition.updatePosition(node: largeAlbumArtNode, position: self.albumArtNode.frame.center, completion: { [weak largeAlbumArtNode] _ in + largeAlbumArtNode?.removeFromSupernode() + }) + largeAlbumArtNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.28, removeOnCompletion: false) + transition.updateTransformScale(node: largeAlbumArtNode, scale: self.albumArtNode.frame.height / largeAlbumArtNode.frame.height) + } else { + largeAlbumArtNode.removeFromSupernode() + } + } + + let scrubberVerticalOrigin: CGFloat = infoVerticalOrigin + 64.0 + + transition.updateFrame(node: self.scrubberNode, frame: CGRect(origin: CGPoint(x: sideInset, y: scrubberVerticalOrigin - 8.0), size: CGSize(width: width - sideInset * 2.0, height: 10.0 + 8.0 * 2.0))) + transition.updateFrame(node: self.leftDurationLabel, frame: CGRect(origin: CGPoint(x: sideInset, y: scrubberVerticalOrigin + 12.0), size: CGSize(width: 40.0, height: 20.0))) + transition.updateFrame(node: self.rightDurationLabel, frame: CGRect(origin: CGPoint(x: width - sideInset - 40.0, y: scrubberVerticalOrigin + 12.0), size: CGSize(width: 40.0, height: 20.0))) + + transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -8.0), size: CGSize(width: width, height: panelHeight + 8.0))) + + let buttonSize = CGSize(width: 64.0, height: 64.0) + let buttonsWidth = width - leftInset - rightInset - sideButtonsInset * 2.0 + let buttonsRect = CGRect(origin: CGPoint(x: floor((width - buttonsWidth) / 2.0), y: scrubberVerticalOrigin + 36.0), size: CGSize(width: buttonsWidth, height: buttonSize.height)) + + transition.updateFrame(node: self.orderButton, frame: CGRect(origin: CGPoint(x: leftInset + sideInset - 22.0, y: buttonsRect.minY), size: buttonSize)) + transition.updateFrame(node: self.loopingButton, frame: CGRect(origin: CGPoint(x: width - rightInset - sideInset - buttonSize.width + 22.0, y: buttonsRect.minY), size: buttonSize)) + + transition.updateFrame(node: self.backwardButton, frame: CGRect(origin: buttonsRect.origin, size: buttonSize)) + transition.updateFrame(node: self.forwardButton, frame: CGRect(origin: CGPoint(x: buttonsRect.maxX - buttonSize.width, y: buttonsRect.minY), size: buttonSize)) + transition.updateFrame(node: self.playPauseButton, frame: CGRect(origin: CGPoint(x: buttonsRect.minX + floor((buttonsRect.width - buttonSize.width) / 2.0), y: buttonsRect.minY), size: buttonSize)) + + return panelHeight + } + + func collapse() { + if self.isExpanded { + self.isExpanded = false + self.updateIsExpanded?() + } + } + + @objc func collapsePressed() { + self.requestCollapse?() + } + + @objc func sharePressed() { + if let itemId = self.currentItemId as? PeerMessagesMediaPlaylistItemId { + self.requestShare?(itemId.messageId) + } + } + + @objc func orderPressed() { + if let order = self.currentOrder { + let nextOrder: MusicPlaybackSettingsOrder + switch order { + case .regular: + nextOrder = .reversed + case .reversed: + nextOrder = .random + case .random: + nextOrder = .regular + } + let _ = updateMusicPlaybackSettingsInteractively(postbox: self.postbox, { + return $0.withUpdatedOrder(nextOrder) + }).start() + self.control?(.setOrder(nextOrder)) + } + } + + @objc func loopingPressed() { + if let looping = self.currentLooping { + let nextLooping: MusicPlaybackSettingsLooping + switch looping { + case .none: + nextLooping = .item + case .item: + nextLooping = .all + case .all: + nextLooping = .none + } + let _ = updateMusicPlaybackSettingsInteractively(postbox: self.postbox, { + return $0.withUpdatedLooping(nextLooping) + }).start() + self.control?(.setLooping(nextLooping)) + } + } + + @objc func backwardPressed() { + self.control?(.previous) + } + + @objc func forwardPressed() { + self.control?(.next) + } + + @objc func playPausePressed() { + self.control?(.playback(.togglePlayPause)) + } + + @objc func albumArtTap(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.isExpanded = !self.isExpanded + self.updateIsExpanded?() + } + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let result = super.hitTest(point, with: event) + if result == self.view { + return nil + } + return result + } +} diff --git a/TelegramUI/OverlayUniversalVideoNode.swift b/TelegramUI/OverlayUniversalVideoNode.swift index cfa7961021..3dc20bd44e 100644 --- a/TelegramUI/OverlayUniversalVideoNode.swift +++ b/TelegramUI/OverlayUniversalVideoNode.swift @@ -19,7 +19,13 @@ final class OverlayUniversalVideoNode: OverlayMediaItemNode { return true } - init(account: Account, manager: UniversalVideoContentManager, content: UniversalVideoContent, expand: @escaping () -> Void, close: @escaping () -> Void) { + var canAttachContent: Bool = true { + didSet { + self.videoNode.canAttachContent = self.canAttachContent + } + } + + init(account: Account, audioSession: ManagedAudioSession, manager: UniversalVideoContentManager, content: UniversalVideoContent, expand: @escaping () -> Void, close: @escaping () -> Void) { self.content = content var unminimizeImpl: (() -> Void)? var togglePlayPauseImpl: (() -> Void)? @@ -33,7 +39,7 @@ final class OverlayUniversalVideoNode: OverlayMediaItemNode { }, close: { closeImpl?() }) - self.videoNode = UniversalVideoNode(account: account, manager: manager, decoration: decoration, content: content, priority: .overlay) + self.videoNode = UniversalVideoNode(postbox: account.postbox, audioSession: audioSession, manager: manager, decoration: decoration, content: content, priority: .overlay) self.decoration = decoration super.init() @@ -60,8 +66,16 @@ final class OverlayUniversalVideoNode: OverlayMediaItemNode { self.addSubnode(self.videoNode) self.videoNode.ownsContentNodeUpdated = { [weak self] value in if let strongSelf = self { + let previous = strongSelf.hasAttachedContext strongSelf.hasAttachedContext = value strongSelf.hasAttachedContextUpdated?(value) + + if previous != value { + if !value { + strongSelf.dismiss() + close() + } + } } } diff --git a/TelegramUI/OverlayVideoDecoration.swift b/TelegramUI/OverlayVideoDecoration.swift index 11793afc58..ad2feb0863 100644 --- a/TelegramUI/OverlayVideoDecoration.swift +++ b/TelegramUI/OverlayVideoDecoration.swift @@ -88,6 +88,9 @@ final class OverlayVideoDecoration: UniversalVideoDecoration { } } + func updateContentNodeSnapshot(_ snapshot: UIView?) { + } + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { self.validLayoutSize = size @@ -133,7 +136,7 @@ final class OverlayVideoDecoration: UniversalVideoDecoration { if let value = value { return value } else { - return MediaPlayerStatus(generationTimestamp: CACurrentMediaTime(), duration: 0.0, timestamp: 0.0, status: .paused) + return MediaPlayerStatus(generationTimestamp: CACurrentMediaTime(), duration: 0.0, timestamp: 0.0, seekId: 0, status: .paused) } } } diff --git a/TelegramUI/PasscodeOptionsController.swift b/TelegramUI/PasscodeOptionsController.swift index b7e946d370..0ce3a8c266 100644 --- a/TelegramUI/PasscodeOptionsController.swift +++ b/TelegramUI/PasscodeOptionsController.swift @@ -4,6 +4,7 @@ import SwiftSignalKit import Postbox import TelegramCore import LegacyComponents +import LocalAuthentication private final class PasscodeOptionsControllerArguments { let turnPasscodeOn: () -> Void @@ -153,23 +154,23 @@ private struct PasscodeOptionsData: Equatable { } } -private func autolockStringForTimeout(_ timeout: Int32?) -> String { +private func autolockStringForTimeout(strings: PresentationStrings, timeout: Int32?) -> String { if let timeout = timeout { if timeout == 10 { return "If away for 10 seconds" } else if timeout == 1 * 60 { - return "If away for 1 min" + return strings.PasscodeSettings_AutoLock_IfAwayFor_1minute } else if timeout == 5 * 60 { - return "If away for 5 min" + return strings.PasscodeSettings_AutoLock_IfAwayFor_5minutes } else if timeout == 1 * 60 * 60 { - return "If away for 1 hour" + return strings.PasscodeSettings_AutoLock_IfAwayFor_1hour } else if timeout == 5 * 60 * 60 { - return "If away for 5 hours" + return strings.PasscodeSettings_AutoLock_IfAwayFor_5hours } else { return "" } } else { - return "Disabled" + return strings.PasscodeSettings_AutoLock_Disabled } } @@ -184,8 +185,15 @@ private func passcodeOptionsControllerEntries(presentationData: PresentationData entries.append(.togglePasscode(presentationData.theme, presentationData.strings.PasscodeSettings_TurnPasscodeOff, true)) entries.append(.changePasscode(presentationData.theme, presentationData.strings.PasscodeSettings_ChangePasscode)) entries.append(.settingInfo(presentationData.theme, presentationData.strings.PasscodeSettings_Help)) - entries.append(.autoLock(presentationData.theme, presentationData.strings.PasscodeSettings_AutoLock, autolockStringForTimeout(passcodeOptionsData.presentationSettings.autolockTimeout))) - entries.append(.touchId(presentationData.theme, presentationData.strings.PasscodeSettings_UnlockWithTouchId, passcodeOptionsData.presentationSettings.enableBiometrics)) + entries.append(.autoLock(presentationData.theme, presentationData.strings.PasscodeSettings_AutoLock, autolockStringForTimeout(strings: presentationData.strings, timeout: passcodeOptionsData.presentationSettings.autolockTimeout))) + if let biometricAuthentication = LocalAuth.biometricAuthentication { + switch biometricAuthentication { + case .touchId: + entries.append(.touchId(presentationData.theme, presentationData.strings.PasscodeSettings_UnlockWithTouchId, passcodeOptionsData.presentationSettings.enableBiometrics)) + case .faceId: + entries.append(.touchId(presentationData.theme, presentationData.strings.PasscodeSettings_UnlockWithFaceId, passcodeOptionsData.presentationSettings.enableBiometrics)) + } + } } return entries @@ -213,16 +221,22 @@ func passcodeOptionsController(account: Account) -> ViewController { let arguments = PasscodeOptionsControllerArguments(turnPasscodeOn: { var dismissImpl: (() -> Void)? - let legacyController = LegacyController(presentation: LegacyControllerPresentation.modal(animateIn: true)) + + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + + let legacyController = LegacyController(presentation: LegacyControllerPresentation.modal(animateIn: true), theme: presentationData.theme) let controller = TGPasscodeEntryController(context: legacyController.context, style: TGPasscodeEntryControllerStyleDefault, mode: TGPasscodeEntryControllerModeSetupSimple, cancelEnabled: true, allowTouchId: false, attemptData: nil, completion: { result in if let result = result { let challenge = PostboxAccessChallengeData.numericalPassword(value: result, timeout: nil, attempts: nil) let _ = account.postbox.modify({ modifier -> Void in modifier.setAccessChallengeData(challenge) + updatePresentationPasscodeSettingsInternal(modifier: modifier, { current in + return current.withUpdatedAutolockTimeout(1 * 60 * 60) + }) }).start() let _ = (passcodeOptionsDataPromise.get() |> take(1)).start(next: { [weak passcodeOptionsDataPromise] data in - passcodeOptionsDataPromise?.set(.single(data.withUpdatedAccessChallenge(challenge))) + passcodeOptionsDataPromise?.set(.single(data.withUpdatedAccessChallenge(challenge).withUpdatedPresentationSettings(data.presentationSettings.withUpdatedAutolockTimeout(1 * 60 * 60)))) }) dismissImpl?() @@ -261,7 +275,10 @@ func passcodeOptionsController(account: Account) -> ViewController { presentControllerImpl?(actionSheet, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) }, changePasscode: { var dismissImpl: (() -> Void)? - let legacyController = LegacyController(presentation: LegacyControllerPresentation.modal(animateIn: true)) + + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + + let legacyController = LegacyController(presentation: LegacyControllerPresentation.modal(animateIn: true), theme: presentationData.theme) let controller = TGPasscodeEntryController(context: legacyController.context, style: TGPasscodeEntryControllerStyleDefault, mode: TGPasscodeEntryControllerModeSetupSimple, cancelEnabled: true, allowTouchId: false, attemptData: nil, completion: { result in if let result = result { let _ = account.postbox.modify({ modifier -> Void in @@ -311,7 +328,7 @@ func passcodeOptionsController(account: Account) -> ViewController { if value != 0 { t = value } - items.append(ActionSheetButtonItem(title: autolockStringForTimeout(t), color: .accent, action: { [weak actionSheet] in + items.append(ActionSheetButtonItem(title: autolockStringForTimeout(strings: presentationData.strings, timeout: t), color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() setAction(t) @@ -369,7 +386,10 @@ public func passcodeOptionsAccessController(account: Account, animateIn: Bool = attemptData = TGPasscodeEntryAttemptData(numberOfInvalidAttempts: Int(attempts.count), dateOfLastInvalidAttempt: Double(attempts.timestamp)) } var dismissImpl: (() -> Void)? - let legacyController = LegacyController(presentation: LegacyControllerPresentation.modal(animateIn: true)) + + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + + let legacyController = LegacyController(presentation: LegacyControllerPresentation.modal(animateIn: true), theme: presentationData.theme) let controller = TGPasscodeEntryController(context: legacyController.context, style: TGPasscodeEntryControllerStyleDefault, mode: TGPasscodeEntryControllerModeVerifySimple, cancelEnabled: true, allowTouchId: false, attemptData: attemptData, completion: { value in if value != nil { completion(false) diff --git a/TelegramUI/PeerAvatar.swift b/TelegramUI/PeerAvatar.swift index 054b343aaf..1db5276414 100644 --- a/TelegramUI/PeerAvatar.swift +++ b/TelegramUI/PeerAvatar.swift @@ -19,14 +19,8 @@ private let roundCorners = { () -> UIImage in return image }() -func peerAvatarImage(account: Account, peer: Peer, temporaryRepresentation: TelegramMediaImageRepresentation?, displayDimensions: CGSize = CGSize(width: 60.0, height: 60.0)) -> Signal? { - var smallProfileImage: TelegramMediaImageRepresentation? - if let temporaryRepresentation = temporaryRepresentation { - smallProfileImage = temporaryRepresentation - } else { - smallProfileImage = peer.smallProfileImage - } - if let smallProfileImage = smallProfileImage { +func peerAvatarImage(account: Account, representation: TelegramMediaImageRepresentation?, displayDimensions: CGSize = CGSize(width: 60.0, height: 60.0)) -> Signal? { + if let smallProfileImage = representation { let resourceData = account.postbox.mediaBox.resourceData(smallProfileImage.resource) let imageData = resourceData |> take(1) diff --git a/TelegramUI/PeerAvatarImageGalleryItem.swift b/TelegramUI/PeerAvatarImageGalleryItem.swift index b3c53ee0cf..9f38622d81 100644 --- a/TelegramUI/PeerAvatarImageGalleryItem.swift +++ b/TelegramUI/PeerAvatarImageGalleryItem.swift @@ -47,9 +47,13 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { private let imageNode: TransformImageNode fileprivate let _ready = Promise() fileprivate let _title = Promise() + private let statusNodeContainer: HighlightableButtonNode + private let statusNode: RadialStatusNode //private let footerContentNode: ChatItemGalleryFooterContentNode - private var fetchDisposable = MetaDisposable() + private let fetchDisposable = MetaDisposable() + private let statusDisposable = MetaDisposable() + private var status: MediaResourceStatus? init(account: Account) { self.account = account @@ -57,6 +61,10 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { self.imageNode = TransformImageNode() //self.footerContentNode = ChatItemGalleryFooterContentNode(account: account) + self.statusNodeContainer = HighlightableButtonNode() + self.statusNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.5)) + self.statusNode.isHidden = true + super.init() self.imageNode.imageUpdated = { [weak self] in @@ -65,10 +73,17 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { self.imageNode.view.contentMode = .scaleAspectFill self.imageNode.clipsToBounds = true + + self.statusNodeContainer.addSubnode(self.statusNode) + self.addSubnode(self.statusNodeContainer) + + self.statusNodeContainer.addTarget(self, action: #selector(self.statusPressed), forControlEvents: .touchUpInside) + self.statusNodeContainer.isUserInteractionEnabled = false } deinit { self.fetchDisposable.dispose() + self.statusDisposable.dispose() } override func ready() -> Signal { @@ -77,6 +92,10 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) + + let statusSize = CGSize(width: 50.0, height: 50.0) + transition.updateFrame(node: self.statusNodeContainer, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - statusSize.width) / 2.0), y: floor((layout.size.height - statusSize.height) / 2.0)), size: statusSize)) + transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(), size: statusSize)) } fileprivate func setEntry(_ entry: AvatarGalleryEntry) { @@ -85,11 +104,54 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { if let largestSize = largestImageRepresentation(entry.representations) { let displaySize = largestSize.dimensions.fitted(CGSize(width: 1280.0, height: 1280.0)).dividedByScreenScale().integralFloor - self.imageNode.alphaTransitionOnFirstUpdate = false self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets()))() - self.imageNode.setSignal(account: account, signal: chatAvatarGalleryPhoto(account: account, representations: entry.representations), dispatchOnDisplayLink: false) + self.imageNode.setSignal(chatAvatarGalleryPhoto(account: account, representations: entry.representations), dispatchOnDisplayLink: false) self.zoomableContent = (largestSize.dimensions, self.imageNode) self.fetchDisposable.set(account.postbox.mediaBox.fetchedResource(largestSize.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .generic)).start()) + + self.statusDisposable.set((account.postbox.mediaBox.resourceStatus(largestSize.resource) + |> deliverOnMainQueue).start(next: { [weak self] status in + if let strongSelf = self { + let previousStatus = strongSelf.status + strongSelf.status = status + switch status { + case .Remote: + strongSelf.statusNode.isHidden = false + strongSelf.statusNodeContainer.isUserInteractionEnabled = true + strongSelf.statusNode.transitionToState(.download(.white), completion: {}) + case let .Fetching(isActive, progress): + strongSelf.statusNode.isHidden = false + strongSelf.statusNodeContainer.isUserInteractionEnabled = true + var actualProgress = progress + if isActive { + actualProgress = max(actualProgress, 0.027) + } + strongSelf.statusNode.transitionToState(.progress(color: .white, value: CGFloat(actualProgress), cancelEnabled: true), completion: {}) + case .Local: + if let previousStatus = previousStatus, case .Fetching = previousStatus { + strongSelf.statusNode.transitionToState(.progress(color: .white, value: 1.0, cancelEnabled: true), completion: { + if let strongSelf = self { + strongSelf.statusNode.alpha = 0.0 + strongSelf.statusNodeContainer.isUserInteractionEnabled = false + strongSelf.statusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { _ in + if let strongSelf = self { + strongSelf.statusNode.transitionToState(.none, animated: false, completion: {}) + } + }) + } + }) + } else if !strongSelf.statusNode.isHidden && !strongSelf.statusNode.alpha.isZero { + strongSelf.statusNode.alpha = 0.0 + strongSelf.statusNodeContainer.isUserInteractionEnabled = false + strongSelf.statusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { _ in + if let strongSelf = self { + strongSelf.statusNode.transitionToState(.none, animated: false, completion: {}) + } + }) + } + } + } + })) } else { self._ready.set(.single(Void())) } @@ -130,6 +192,10 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { self?.imageNode.clipsToBounds = false } }) + + self.statusNodeContainer.layer.animatePosition(from: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), to: self.statusNodeContainer.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) + self.statusNodeContainer.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) + self.statusNodeContainer.layer.animateScale(from: 0.5, to: 1.0, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) } override func animateOut(to node: ASDisplayNode, addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) { @@ -174,11 +240,6 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { transformedFrame.origin = CGPoint() - /*self.imageNode.layer.animateBounds(from: self.imageNode.layer.bounds, to: transformedFrame, duration: 0.25 * durationFactor, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in - boundsCompleted = true - intermediateCompletion() - })*/ - let transform = CATransform3DScale(self.imageNode.layer.transform, transformedFrame.size.width / self.imageNode.layer.bounds.size.width, transformedFrame.size.height / self.imageNode.layer.bounds.size.height, 1.0) self.imageNode.layer.animate(from: NSValue(caTransform3D: self.imageNode.layer.transform), to: NSValue(caTransform3D: transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25 * durationFactor, removeOnCompletion: false, completion: { _ in boundsCompleted = true @@ -187,25 +248,29 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { self.imageNode.clipsToBounds = true self.imageNode.layer.animate(from: 0.0 as NSNumber, to: (self.imageNode.frame.width / 2.0) as NSNumber, keyPath: "cornerRadius", timingFunction: kCAMediaTimingFunctionDefault, duration: 0.18 * durationFactor, removeOnCompletion: false) + + self.statusNodeContainer.layer.animatePosition(from: self.statusNodeContainer.position, to: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + self.statusNodeContainer.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, timingFunction: kCAMediaTimingFunctionEaseIn, removeOnCompletion: false) } override func visibilityUpdated(isVisible: Bool) { super.visibilityUpdated(isVisible: isVisible) - - /*if let (account, media) = self.accountAndEntry, let file = media as? TelegramMediaFile { - if isVisible { - self.fetchDisposable.set(account.postbox.mediaBox.fetchedResource(file.resource).start()) - } else { - self.fetchDisposable.set(nil) - } - }*/ } override func title() -> Signal { return self._title.get() } - /*override func footerContent() -> Signal { - return .single(self.footerContentNode) - }*/ + @objc func statusPressed() { + if let entry = self.entry, let resource = largestImageRepresentation(entry.representations)?.resource, let status = self.status { + switch status { + case .Fetching: + self.account.postbox.mediaBox.cancelInteractiveResourceFetch(resource) + case .Remote: + self.fetchDisposable.set(self.account.postbox.mediaBox.fetchedResource(resource, tag: TelegramMediaResourceFetchTag(statsCategory: .generic)).start()) + default: + break + } + } + } } diff --git a/TelegramUI/PeerMediaAudioPlaylist.swift b/TelegramUI/PeerMediaAudioPlaylist.swift index a122805d85..d245b22c1d 100644 --- a/TelegramUI/PeerMediaAudioPlaylist.swift +++ b/TelegramUI/PeerMediaAudioPlaylist.swift @@ -176,7 +176,7 @@ func peerMessageHistoryAudioPlaylist(account: Account, messageId: MessageId) -> break } if let tagMask = tagMask { - return account.postbox.aroundMessageHistoryViewForPeerId(item.entry.index.id.peerId, index: item.entry.index, count: 10, clipHoles: false, anchorIndex: item.entry.index, fixedCombinedReadState: nil, topTaggedMessageIdNamespaces: [], tagMask: tagMask, orderStatistics: []) + return account.postbox.aroundMessageHistoryViewForLocation(.peer(item.entry.index.id.peerId), index: .message(item.entry.index), anchorIndex: .message(item.entry.index), count: 10, clipHoles: false, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tagMask: tagMask, orderStatistics: []) |> take(1) |> map { (view, _, _) -> AudioPlaylistItem? in var index = 0 diff --git a/TelegramUI/PeerMediaCollectionController.swift b/TelegramUI/PeerMediaCollectionController.swift index 583808613f..c6a08eeff3 100644 --- a/TelegramUI/PeerMediaCollectionController.swift +++ b/TelegramUI/PeerMediaCollectionController.swift @@ -5,8 +5,9 @@ import SwiftSignalKit import Display import AsyncDisplayKit import TelegramCore +import SafariServices -public class PeerMediaCollectionController: ViewController { +public class PeerMediaCollectionController: TelegramController { private var containerLayout = ContainerViewLayout() private let account: Account @@ -43,7 +44,7 @@ public class PeerMediaCollectionController: ViewController { self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } self.interfaceState = PeerMediaCollectionInterfaceState(theme: self.presentationData.theme, strings: self.presentationData.strings) - super.init(navigationBarTheme: NavigationBarTheme(rootControllerTheme: self.presentationData.theme).withUpdatedSeparatorColor(.clear)) + super.init(account: account, navigationBarTheme: NavigationBarTheme(rootControllerTheme: self.presentationData.theme).withUpdatedSeparatorColor(self.presentationData.theme.rootController.navigationBar.backgroundColor), enableMediaAccessoryPanel: true) self.title = self.presentationData.strings.SharedMedia_TitleAll @@ -60,80 +61,65 @@ public class PeerMediaCollectionController: ViewController { } let controllerInteraction = ChatControllerInteraction(openMessage: { [weak self] id in - if let strongSelf = self, strongSelf.isNodeLoaded { - let galleryMessage = strongSelf.mediaCollectionDisplayNode.messageForGallery(id) - var galleryMedia: Media? - if let message = galleryMessage?.message { - for media in message.media { - if let file = media as? TelegramMediaFile { - galleryMedia = file - } else if let image = media as? TelegramMediaImage { - galleryMedia = image - } else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { - if let file = content.file { - galleryMedia = file - } else if let image = content.image { - galleryMedia = image - } + if let strongSelf = self, strongSelf.isNodeLoaded, let galleryMessage = strongSelf.mediaCollectionDisplayNode.messageForGallery(id) { + guard let navigationController = strongSelf.navigationController as? NavigationController else { + return false + } + strongSelf.mediaCollectionDisplayNode.view.endEditing(true) + return openChatMessage(account: account, message: galleryMessage.message, reverseMessageGalleryOrder: true, navigationController: navigationController, dismissInput: { + self?.mediaCollectionDisplayNode.view.endEditing(true) + }, present: { c, a in + self?.present(c, in: .window(.root), with: a) + }, transitionNode: { messageId, media in + if let strongSelf = self { + return strongSelf.mediaCollectionDisplayNode.transitionNodeForGallery(messageId: messageId, media: media) + } + return nil + }, addToTransitionSurface: { view in + if let strongSelf = self { + strongSelf.mediaCollectionDisplayNode.view.insertSubview(view, aboveSubview: strongSelf.mediaCollectionDisplayNode.historyNode.view) + } + }, openUrl: { url in + if let strongSelf = self { + if let applicationContext = strongSelf.account.applicationContext as? TelegramApplicationContext { + applicationContext.applicationBindings.openUrl(url) } } - } - - if let galleryMessage = galleryMessage, let galleryMedia = galleryMedia { - if let file = galleryMedia as? TelegramMediaFile, file.isVoice || file.isMusic { - - } else { - let _ = (storedMessageFromSearch(account: strongSelf.account, message: galleryMessage.message) |> deliverOnMainQueue).start(completed: { - if let strongSelf = self { - let gallery = GalleryController(account: strongSelf.account, messageId: id, replaceRootController: { controller, ready in - if let strongSelf = self { - (strongSelf.navigationController as? NavigationController)?.replaceTopController(controller, animated: false, ready: ready) - } - }, baseNavigationController: nil) - strongSelf.galleryHiddenMesageAndMediaDisposable.set(gallery.hiddenMedia.start(next: { [weak strongSelf] messageIdAndMedia in - if let strongSelf = strongSelf { - if let messageIdAndMedia = messageIdAndMedia { - strongSelf.controllerInteraction?.hiddenMedia = [messageIdAndMedia.0: [messageIdAndMedia.1]] - } else { - strongSelf.controllerInteraction?.hiddenMedia = [:] - } - strongSelf.mediaCollectionDisplayNode.updateHiddenMedia() - } - })) - - strongSelf.present(gallery, in: .window(.root), with: GalleryControllerPresentationArguments(transitionArguments: { [weak self] messageId, media in - if let strongSelf = self { - if let transitionNode = strongSelf.mediaCollectionDisplayNode.transitionNodeForGallery(messageId: messageId, media: media) { - return GalleryTransitionArguments(transitionNode: transitionNode, addToTransitionSurface: { _ in - }) - } - } - return nil - })) - } - }) - } - } + }, openPeer: { peer, navigation in + self?.controllerInteraction?.openPeer(peer.id, navigation, nil) + }, callPeer: { peerId in + self?.controllerInteraction?.callPeer(peerId) + }, sendSticker: { file in + self?.controllerInteraction?.sendSticker(file) + }, setupTemporaryHiddenMedia: { signal, centralIndex, galleryMedia in + }) } + return false }, openSecretMessagePreview: { _ in }, closeSecretMessagePreview: { }, openPeer: { [weak self] id, navigation, _ in if let strongSelf = self { if let id = id { - (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, peerId: id, messageId: nil)) + (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, chatLocation: .peer(id), messageId: nil)) } } }, openPeerMention: { _ in - }, openMessageContextMenu: { [weak self] id, node, frame in + }, openMessageContextMenu: { [weak self] id, _, _ in if let strongSelf = self, strongSelf.isNodeLoaded { - if let message = strongSelf.mediaCollectionDisplayNode.historyNode.messageInCurrentHistoryView(id) { - /*if let contextMenuController = contextMenuForChatPresentationIntefaceState(strongSelf.presentationInterfaceState, account: strongSelf.account, message: message, interfaceInteraction: strongSelf.interfaceInteraction) { - strongSelf.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak strongSelf, weak node] in - if let node = node { - return (node, frame) - } else { - return nil - } - })) - }*/ + if let message = strongSelf.mediaCollectionDisplayNode.messageForGallery(id)?.message { + let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) + actionSheet.setItemGroups([ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: strongSelf.presentationData.strings.SharedMedia_ViewInChat, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + if let strongSelf = self, let navigationController = strongSelf.navigationController as? NavigationController { + navigateToChatController(navigationController: navigationController, account: strongSelf.account, chatLocation: .peer(strongSelf.peerId), messageId: message.id) + } + }) + ]), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + strongSelf.mediaCollectionDisplayNode.view.endEditing(true) + strongSelf.present(actionSheet, in: .window(.root)) } } }, navigateToMessage: { [weak self] fromId, id in @@ -157,34 +143,79 @@ public class PeerMediaCollectionController: ViewController { } }*/ } else { - (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, peerId: id.peerId, messageId: id)) + (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, chatLocation: .peer(id.peerId), messageId: id)) } } }, clickThroughMessage: { [weak self] in self?.view.endEditing(true) - }, toggleMessageSelection: { [weak self] id in + }, toggleMessagesSelection: { [weak self] ids, value in if let strongSelf = self, strongSelf.isNodeLoaded { - strongSelf.updateInterfaceState(animated: true, { $0.withToggledSelectedMessage(id) }) + strongSelf.updateInterfaceState(animated: true, { $0.withToggledSelectedMessages(ids, value: value) }) } }, sendMessage: { _ in },sendSticker: { _ in }, sendGif: { _ in }, requestMessageActionCallback: { _, _, _ in - }, openUrl: { _ in + }, openUrl: { [weak self] url in + if let strongSelf = self { + if let applicationContext = strongSelf.account.applicationContext as? TelegramApplicationContext { + applicationContext.applicationBindings.openUrl(url) + } + } }, shareCurrentLocation: { }, shareAccountContact: { }, sendBotCommand: { _, _ in - }, openInstantPage: { _ in + }, openInstantPage: { [weak self] messageId in + if let strongSelf = self, strongSelf.isNodeLoaded, let navigationController = strongSelf.navigationController as? NavigationController, let message = strongSelf.mediaCollectionDisplayNode.messageForGallery(messageId)?.message { + openChatInstantPage(account: strongSelf.account, message: message, navigationController: navigationController) + } }, openHashtag: { _, _ in }, updateInputState: { _ in }, openMessageShareMenu: { _ in }, presentController: { _, _ in }, callPeer: { _ in - }, longTap: { _ in + }, longTap: { [weak self] content in + if let strongSelf = self { + switch content { + case let .url(url): + let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) + actionSheet.setItemGroups([ActionSheetItemGroup(items: [ + ActionSheetTextItem(title: url), + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + if let strongSelf = self { + if let applicationContext = strongSelf.account.applicationContext as? TelegramApplicationContext { + applicationContext.applicationBindings.openUrl(url) + } + } + }), + ActionSheetButtonItem(title: strongSelf.presentationData.strings.ShareMenu_CopyShareLink, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + UIPasteboard.general.string = url + }), + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_AddToReadingList, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + if let link = URL(string: url) { + let _ = try? SSReadingList.default()?.addItem(with: link, title: nil, previewText: nil) + } + }) + ]), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + strongSelf.present(actionSheet, in: .window(.root)) + default: + break + } + } }, openCheckoutOrReceipt: { _ in }, openSearch: { [weak self] in self?.activateSearch() - }, automaticMediaDownloadSettings: .none) + }, setupReply: { _ in + }, canSetupReply: { + return false + }, automaticMediaDownloadSettings: .none) self.controllerInteraction = controllerInteraction @@ -194,7 +225,7 @@ public class PeerMediaCollectionController: ViewController { }, deleteSelectedMessages: { [weak self] in if let strongSelf = self { if let messageIds = strongSelf.interfaceState.selectionState?.selectedIds, !messageIds.isEmpty { - strongSelf.messageContextDisposable.set((combineLatest(chatDeleteMessagesOptions(account: strongSelf.account, messageIds: messageIds), strongSelf.peer.get() |> take(1)) |> deliverOnMainQueue).start(next: { options, peer in + strongSelf.messageContextDisposable.set((combineLatest(chatDeleteMessagesOptions(postbox: strongSelf.account.postbox, accountPeerId: strongSelf.account.peerId, messageIds: messageIds), strongSelf.peer.get() |> take(1)) |> deliverOnMainQueue).start(next: { options, peer in if let strongSelf = self, let peer = peer, !options.isEmpty { let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) var items: [ActionSheetItem] = [] @@ -209,11 +240,11 @@ public class PeerMediaCollectionController: ViewController { if options.contains(.globally) { let globalTitle: String if isChannel { - globalTitle = "Delete" + globalTitle = strongSelf.presentationData.strings.Conversation_DeleteMessagesForMe } else if let personalPeerName = personalPeerName { - globalTitle = "Delete for me and \(personalPeerName)" + globalTitle = strongSelf.presentationData.strings.Conversation_DeleteMessagesFor(personalPeerName).0 } else { - globalTitle = "Delete for everyone" + globalTitle = strongSelf.presentationData.strings.Conversation_DeleteMessagesForEveryone } items.append(ActionSheetButtonItem(title: globalTitle, color: .destructive, action: { [weak actionSheet] in actionSheet?.dismissAnimated() @@ -224,7 +255,7 @@ public class PeerMediaCollectionController: ViewController { })) } if options.contains(.locally) { - items.append(ActionSheetButtonItem(title: "Delete for me", color: .destructive, action: { [weak actionSheet] in + items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_DeleteMessagesForMe, color: .destructive, action: { [weak actionSheet] in actionSheet?.dismissAnimated() if let strongSelf = self { strongSelf.updateInterfaceState(animated: true, { $0.withoutSelectionState() }) @@ -233,7 +264,7 @@ public class PeerMediaCollectionController: ViewController { })) } actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: "Cancel", color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) @@ -270,7 +301,7 @@ public class PeerMediaCollectionController: ViewController { } })) - (strongSelf.navigationController as? NavigationController)?.replaceTopController(ChatController(account: strongSelf.account, peerId: peerId), animated: false, ready: ready) + (strongSelf.navigationController as? NavigationController)?.replaceTopController(ChatController(account: strongSelf.account, chatLocation: .peer(peerId)), animated: false, ready: ready) } }) } @@ -278,14 +309,38 @@ public class PeerMediaCollectionController: ViewController { strongSelf.present(controller, in: .window(.root)) } } + }, shareSelectedMessages: { [weak self] in + if let strongSelf = self, let selectedIds = strongSelf.interfaceState.selectionState?.selectedIds, !selectedIds.isEmpty { + let _ = (strongSelf.account.postbox.modify { modifier -> [Message] in + var messages: [Message] = [] + for id in selectedIds { + if let message = modifier.getMessage(id) { + messages.append(message) + } + } + return messages + } |> deliverOnMainQueue).start(next: { messages in + if let strongSelf = self, !messages.isEmpty { + strongSelf.updateInterfaceState(animated: true, { + $0.withoutSelectionState() + }) + + let shareController = ShareController(account: strongSelf.account, subject: .messages(messages.sorted(by: { lhs, rhs in + return MessageIndex(lhs) < MessageIndex(rhs) + })), externalShare: true, immediateExternalShare: true) + strongSelf.present(shareController, in: .window(.root)) + } + }) + } }, updateTextInputState: { _ in }, updateInputModeAndDismissedButtonKeyboardMessageId: { _ in - }, editMessage: { _, _ in - }, beginMessageSearch: { + }, editMessage: { + }, beginMessageSearch: { _ in }, dismissMessageSearch: { }, updateMessageSearch: { _ in }, navigateMessageSearch: { _ in }, openCalendarSearch: { + }, toggleMembersSearch: { _ in }, navigateToMessage: { _ in }, openPeerInfo: { }, togglePeerNotifications: { @@ -297,6 +352,8 @@ public class PeerMediaCollectionController: ViewController { }, finishMediaRecording: { _ in }, stopMediaRecording: { }, lockMediaRecording: { + }, deleteRecordedMedia: { + }, sendRecordedMedia: { }, switchMediaRecordingMode: { }, setupMessageAutoremoveTimeout: { }, sendSticker: { _ in @@ -308,7 +365,8 @@ public class PeerMediaCollectionController: ViewController { }, deleteChat: { }, beginCall: { }, toggleMessageStickerStarred: { _ in - }, presentController: { _ in + }, presentController: { _, _ in + }, navigateFeed: { }, statuses: nil) self.updateInterfaceState(animated: false, { return $0 }) @@ -349,6 +407,30 @@ public class PeerMediaCollectionController: ViewController { self?.deactivateSearch() }) + self.galleryHiddenMesageAndMediaDisposable.set(self.account.telegramApplicationContext.mediaManager.galleryHiddenMediaManager.hiddenIds().start(next: { [weak self] ids in + if let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction { + var messageIdAndMedia: [MessageId: [Media]] = [:] + + for id in ids { + if case let .chat(messageId, media) = id { + messageIdAndMedia[messageId] = [media] + } + } + + //if controllerInteraction.hiddenMedia != messageIdAndMedia { + controllerInteraction.hiddenMedia = messageIdAndMedia + + strongSelf.mediaCollectionDisplayNode.historyNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? GridMessageItemNode { + itemNode.updateHiddenMedia() + } else if let itemNode = itemNode as? ListMessageNode { + itemNode.updateHiddenMedia() + } + } + //} + } + })) + self.ready.set(combineLatest(self.mediaCollectionDisplayNode.historyNode.historyState.get(), self._peerReady.get()) |> map { $1 }) self.mediaCollectionDisplayNode.requestLayout = { [weak self] transition in @@ -391,7 +473,9 @@ public class PeerMediaCollectionController: ViewController { self.interfaceState = updatedInterfaceState if let button = rightNavigationButtonForPeerMediaCollectionInterfaceState(updatedInterfaceState, currentButton: self.rightNavigationButton, target: self, selector: #selector(self.rightNavigationButtonAction)) { - self.navigationItem.setRightBarButton(button.buttonItem, animated: true) + if self.rightNavigationButton != button { + self.navigationItem.setRightBarButton(button.buttonItem, animated: true) + } self.rightNavigationButton = button } else if let _ = self.rightNavigationButton { self.navigationItem.setRightBarButton(nil, animated: true) @@ -409,6 +493,8 @@ public class PeerMediaCollectionController: ViewController { itemNode.updateSelectionState(animated: animated) } } + + self.mediaCollectionDisplayNode.selectedMessages = updatedInterfaceState.selectionState?.selectedIds } } } diff --git a/TelegramUI/PeerMediaCollectionControllerNode.swift b/TelegramUI/PeerMediaCollectionControllerNode.swift index 434545feb0..f4f3ab5146 100644 --- a/TelegramUI/PeerMediaCollectionControllerNode.swift +++ b/TelegramUI/PeerMediaCollectionControllerNode.swift @@ -10,20 +10,20 @@ struct PeerMediaCollectionMessageForGallery { let fromSearchResults: Bool } -private func historyNodeImplForMode(_ mode: PeerMediaCollectionMode, account: Account, peerId: PeerId, messageId: MessageId?, controllerInteraction: ChatControllerInteraction) -> ChatHistoryNode & ASDisplayNode { +private func historyNodeImplForMode(_ mode: PeerMediaCollectionMode, account: Account, peerId: PeerId, messageId: MessageId?, controllerInteraction: ChatControllerInteraction, selectedMessages: Signal?, NoError>) -> ChatHistoryNode & ASDisplayNode { switch mode { case .photoOrVideo: return ChatHistoryGridNode(account: account, peerId: peerId, messageId: messageId, tagMask: .photoOrVideo, controllerInteraction: controllerInteraction) case .file: - let node = ChatHistoryListNode(account: account, peerId: peerId, tagMask: .file, messageId: messageId, controllerInteraction: controllerInteraction, mode: .list) + let node = ChatHistoryListNode(account: account, chatLocation: .peer(peerId), tagMask: .file, messageId: messageId, controllerInteraction: controllerInteraction, selectedMessages: selectedMessages, mode: .list(search: true, reversed: false)) node.preloadPages = true return node case .music: - let node = ChatHistoryListNode(account: account, peerId: peerId, tagMask: .music, messageId: messageId, controllerInteraction: controllerInteraction, mode: .list) + let node = ChatHistoryListNode(account: account, chatLocation: .peer(peerId), tagMask: .music, messageId: messageId, controllerInteraction: controllerInteraction, selectedMessages: selectedMessages, mode: .list(search: true, reversed: false)) node.preloadPages = true return node case .webpage: - let node = ChatHistoryListNode(account: account, peerId: peerId, tagMask: .webPage, messageId: messageId, controllerInteraction: controllerInteraction, mode: .list) + let node = ChatHistoryListNode(account: account, chatLocation: .peer(peerId), tagMask: .webPage, messageId: messageId, controllerInteraction: controllerInteraction, selectedMessages: selectedMessages, mode: .list(search: true, reversed: false)) node.preloadPages = true return node } @@ -86,8 +86,17 @@ class PeerMediaCollectionControllerNode: ASDisplayNode { private var mediaCollectionInterfaceState: PeerMediaCollectionInterfaceState - private var modeSelectionNode: PeerMediaCollectionModeSelectionNode? + private let selectedMessagesPromise = Promise?>(nil) + var selectedMessages: Set? { + didSet { + if self.selectedMessages != oldValue { + self.selectedMessagesPromise.set(.single(self.selectedMessages)) + } + } + } private var selectionPanel: ChatMessageSelectionInputPanelNode? + private var selectionPanelSeparatorNode: ASDisplayNode? + private var selectionPanelBackgroundNode: ASDisplayNode? private var chatPresentationInterfaceState: ChatPresentationInterfaceState @@ -107,11 +116,11 @@ class PeerMediaCollectionControllerNode: ASDisplayNode { self.sectionsNode = PeerMediaCollectionSectionsNode(theme: self.presentationData.theme, strings: self.presentationData.strings) - self.historyNode = historyNodeImplForMode(self.mediaCollectionInterfaceState.mode, account: account, peerId: peerId, messageId: messageId, controllerInteraction: controllerInteraction) + self.historyNode = historyNodeImplForMode(self.mediaCollectionInterfaceState.mode, account: account, peerId: peerId, messageId: messageId, controllerInteraction: controllerInteraction, selectedMessages: self.selectedMessagesPromise.get()) self.historyEmptyNode = PeerMediaCollectionEmptyNode(mode: self.mediaCollectionInterfaceState.mode, theme: self.presentationData.theme, strings: self.presentationData.strings) self.historyEmptyNode.isHidden = true - self.chatPresentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: self.presentationData.chatWallpaper, theme: self.presentationData.theme, strings: self.presentationData.strings, accountPeerId: account.peerId) + self.chatPresentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: self.presentationData.chatWallpaper, theme: self.presentationData.theme, strings: self.presentationData.strings, fontSize: self.presentationData.fontSize, accountPeerId: account.peerId, mode: .standard, chatLocation: .peer(self.peerId)) super.init() @@ -127,7 +136,11 @@ class PeerMediaCollectionControllerNode: ASDisplayNode { if let navigationBar = navigationBar { self.addSubnode(navigationBar) } - self.addSubnode(self.sectionsNode) + if let navigationBar = self.navigationBar { + self.insertSubnode(self.sectionsNode, aboveSubnode: navigationBar) + } else { + self.addSubnode(self.sectionsNode) + } self.sectionsNode.indexUpdated = { [weak self] index in if let strongSelf = self { @@ -166,6 +179,13 @@ class PeerMediaCollectionControllerNode: ASDisplayNode { var vanillaInsets = layout.insets(options: []) vanillaInsets.top += navigationBarHeight + var additionalInset: CGFloat = 0.0 + + if (navigationBarHeight - (layout.statusBarHeight ?? 0.0)).isLessThanOrEqualTo(44.0) { + } else { + additionalInset += 10.0 + } + if let searchDisplayController = self.searchDisplayController { searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) if !searchDisplayController.isDeactivating { @@ -173,7 +193,7 @@ class PeerMediaCollectionControllerNode: ASDisplayNode { } } - let sectionsHeight = self.sectionsNode.updateLayout(width: layout.size.width, transition: transition) + let sectionsHeight = self.sectionsNode.updateLayout(width: layout.size.width, additionalInset: additionalInset, transition: transition) var sectionOffset: CGFloat = 0.0 if navigationBarHeight.isZero { sectionOffset = -sectionsHeight @@ -193,25 +213,59 @@ class PeerMediaCollectionControllerNode: ASDisplayNode { let interfaceState = self.chatPresentationInterfaceState.updatedPeer({ _ in self.mediaCollectionInterfaceState.peer }) if let selectionPanel = self.selectionPanel { - selectionPanel.selectedMessageCount = selectionState.selectedIds.count - let panelHeight = selectionPanel.updateLayout(width: layout.size.width, maxHeight: 0.0, transition: transition, interfaceState: interfaceState) + selectionPanel.selectedMessages = selectionState.selectedIds + let panelHeight = selectionPanel.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, maxHeight: 0.0, transition: transition, interfaceState: interfaceState) transition.updateFrame(node: selectionPanel, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - insets.bottom - panelHeight), size: CGSize(width: layout.size.width, height: panelHeight))) + if let selectionPanelSeparatorNode = self.selectionPanelSeparatorNode { + transition.updateFrame(node: selectionPanelSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - insets.bottom - panelHeight), size: CGSize(width: layout.size.width, height: UIScreenPixel))) + } + if let selectionPanelBackgroundNode = self.selectionPanelBackgroundNode { + transition.updateFrame(node: selectionPanelBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - insets.bottom - panelHeight), size: CGSize(width: layout.size.width, height: insets.bottom + panelHeight))) + } } else { + let selectionPanelBackgroundNode = ASDisplayNode() + selectionPanelBackgroundNode.isLayerBacked = true + selectionPanelBackgroundNode.backgroundColor = self.presentationData.theme.chat.inputPanel.panelBackgroundColor + self.addSubnode(selectionPanelBackgroundNode) + self.selectionPanelBackgroundNode = selectionPanelBackgroundNode + let selectionPanel = ChatMessageSelectionInputPanelNode(theme: self.chatPresentationInterfaceState.theme) - selectionPanel.interfaceInteraction = self.interfaceInteraction - selectionPanel.selectedMessageCount = selectionState.selectedIds.count + selectionPanel.account = self.account selectionPanel.backgroundColor = self.presentationData.theme.chat.inputPanel.panelBackgroundColor - let panelHeight = selectionPanel.updateLayout(width: layout.size.width, maxHeight: 0.0, transition: .immediate, interfaceState: interfaceState) + selectionPanel.interfaceInteraction = self.interfaceInteraction + selectionPanel.selectedMessages = selectionState.selectedIds + let panelHeight = selectionPanel.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, maxHeight: 0.0, transition: .immediate, interfaceState: interfaceState) self.selectionPanel = selectionPanel self.addSubnode(selectionPanel) - selectionPanel.frame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - insets.bottom), size: CGSize(width: layout.size.width, height: panelHeight)) + + let selectionPanelSeparatorNode = ASDisplayNode() + selectionPanelSeparatorNode.isLayerBacked = true + selectionPanelSeparatorNode.backgroundColor = self.presentationData.theme.chat.inputPanel.panelStrokeColor + self.addSubnode(selectionPanelSeparatorNode) + self.selectionPanelSeparatorNode = selectionPanelSeparatorNode + + selectionPanel.frame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height), size: CGSize(width: layout.size.width, height: panelHeight)) + selectionPanelBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height), size: CGSize(width: layout.size.width, height: 0.0)) + selectionPanelSeparatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height), size: CGSize(width: layout.size.width, height: UIScreenPixel)) transition.updateFrame(node: selectionPanel, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - insets.bottom - panelHeight), size: CGSize(width: layout.size.width, height: panelHeight))) + transition.updateFrame(node: selectionPanelBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - insets.bottom - panelHeight), size: CGSize(width: layout.size.width, height: insets.bottom + panelHeight))) + transition.updateFrame(node: selectionPanelSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - insets.bottom - panelHeight), size: CGSize(width: layout.size.width, height: UIScreenPixel))) } } else if let selectionPanel = self.selectionPanel { self.selectionPanel = nil - transition.updateFrame(node: selectionPanel, frame: selectionPanel.frame.offsetBy(dx: 0.0, dy: selectionPanel.bounds.size.height), completion: { [weak selectionPanel] _ in + transition.updateFrame(node: selectionPanel, frame: selectionPanel.frame.offsetBy(dx: 0.0, dy: selectionPanel.bounds.size.height + insets.bottom), completion: { [weak selectionPanel] _ in selectionPanel?.removeFromSupernode() }) + if let selectionPanelSeparatorNode = self.selectionPanelSeparatorNode { + transition.updateFrame(node: selectionPanelSeparatorNode, frame: selectionPanelSeparatorNode.frame.offsetBy(dx: 0.0, dy: selectionPanel.bounds.size.height + insets.bottom), completion: { [weak selectionPanelSeparatorNode] _ in + selectionPanelSeparatorNode?.removeFromSupernode() + }) + } + if let selectionPanelBackgroundNode = self.selectionPanelBackgroundNode { + transition.updateFrame(node: selectionPanelBackgroundNode, frame: selectionPanelBackgroundNode.frame.offsetBy(dx: 0.0, dy: selectionPanel.bounds.size.height + insets.bottom), completion: { [weak selectionPanelSeparatorNode] _ in + selectionPanelSeparatorNode?.removeFromSupernode() + }) + } } var duration: Double = 0.0 @@ -249,7 +303,7 @@ class PeerMediaCollectionControllerNode: ASDisplayNode { } listViewTransaction(ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: insets.top, left: - insets.right, bottom: insets.bottom + additionalBottomInset, right: insets.left), duration: duration, curve: listViewCurve)) + insets.right + layout.safeInsets.right, bottom: insets.bottom + additionalBottomInset, right: insets.left + layout.safeInsets.right), duration: duration, curve: listViewCurve)) if let (candidateHistoryNode, _) = self.candidateHistoryNode { let previousBounds = candidateHistoryNode.bounds @@ -257,38 +311,7 @@ class PeerMediaCollectionControllerNode: ASDisplayNode { candidateHistoryNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0) (candidateHistoryNode as! ChatHistoryNode).updateLayout(transition: transition, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: insets.top, left: - insets.right, bottom: insets.bottom + additionalBottomInset, right: insets.left), duration: duration, curve: listViewCurve)) - } - - if self.mediaCollectionInterfaceState.selectingMode { - if let modeSelectionNode = self.modeSelectionNode { - modeSelectionNode.frame = CGRect(origin: CGPoint(), size: layout.size) - modeSelectionNode.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) - modeSelectionNode.mediaCollectionInterfaceState = self.mediaCollectionInterfaceState - } else { - let modeSelectionNode = PeerMediaCollectionModeSelectionNode(mediaCollectionInterfaceState: self.mediaCollectionInterfaceState) - modeSelectionNode.selectedMode = { [weak self] mode in - if let requestUpdateMediaCollectionInterfaceState = self?.requestUpdateMediaCollectionInterfaceState { - requestUpdateMediaCollectionInterfaceState(true, { $0.withToggledSelectingMode().withMode(mode) }) - } - } - modeSelectionNode.dismiss = { [weak self] in - if let requestUpdateMediaCollectionInterfaceState = self?.requestUpdateMediaCollectionInterfaceState { - requestUpdateMediaCollectionInterfaceState(true, { $0.withToggledSelectingMode() }) - } - } - modeSelectionNode.frame = CGRect(origin: CGPoint(), size: layout.size) - modeSelectionNode.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) - modeSelectionNode.mediaCollectionInterfaceState = self.mediaCollectionInterfaceState - self.insertSubnode(modeSelectionNode, aboveSubnode: self.historyNode) - modeSelectionNode.animateIn() - self.modeSelectionNode = modeSelectionNode - } - } else if let modeSelectionNode = self.modeSelectionNode { - self.modeSelectionNode = nil - modeSelectionNode.animateOut { [weak modeSelectionNode] in - modeSelectionNode?.removeFromSupernode() - } + insets.right + layout.safeInsets.right, bottom: insets.bottom + additionalBottomInset, right: insets.left + layout.safeInsets.left), duration: duration, curve: listViewCurve)) } } @@ -343,7 +366,7 @@ class PeerMediaCollectionControllerNode: ASDisplayNode { if self.mediaCollectionInterfaceState.mode != mediaCollectionInterfaceState.mode { let previousMode = self.mediaCollectionInterfaceState.mode if let containerLayout = self.containerLayout, self.candidateHistoryNode == nil || self.candidateHistoryNode!.1 != mediaCollectionInterfaceState.mode { - let node = historyNodeImplForMode(mediaCollectionInterfaceState.mode, account: self.account, peerId: self.peerId, messageId: nil, controllerInteraction: self.controllerInteraction) + let node = historyNodeImplForMode(mediaCollectionInterfaceState.mode, account: self.account, peerId: self.peerId, messageId: nil, controllerInteraction: self.controllerInteraction, selectedMessages: self.selectedMessagesPromise.get()) node.backgroundColor = self.presentationData.theme.list.plainBackgroundColor self.candidateHistoryNode = (node, mediaCollectionInterfaceState.mode) @@ -375,7 +398,7 @@ class PeerMediaCollectionControllerNode: ASDisplayNode { additionalBottomInset = selectionPanel.bounds.size.height } - node.updateLayout(transition: .immediate, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: containerLayout.0.size, insets: UIEdgeInsets(top: insets.top, left: insets.right, bottom: insets.bottom + additionalBottomInset, right: insets.left), duration: 0.0, curve: .Default)) + node.updateLayout(transition: .immediate, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: containerLayout.0.size, insets: UIEdgeInsets(top: insets.top, left: insets.right + containerLayout.0.safeInsets.right, bottom: insets.bottom + additionalBottomInset, right: insets.left + containerLayout.0.safeInsets.left), duration: 0.0, curve: .Default)) let historyEmptyNode = PeerMediaCollectionEmptyNode(mode: mediaCollectionInterfaceState.mode, theme: self.presentationData.theme, strings: self.presentationData.strings) historyEmptyNode.isHidden = true @@ -422,10 +445,6 @@ class PeerMediaCollectionControllerNode: ASDisplayNode { self.mediaCollectionInterfaceState = mediaCollectionInterfaceState - if let modeSelectionNode = self.modeSelectionNode { - modeSelectionNode.mediaCollectionInterfaceState = mediaCollectionInterfaceState - } - self.requestLayout(animated ? .animated(duration: 0.4, curve: .spring) : .immediate) } } diff --git a/TelegramUI/PeerMediaCollectionEmptyNode.swift b/TelegramUI/PeerMediaCollectionEmptyNode.swift index a61e440684..9515df28df 100644 --- a/TelegramUI/PeerMediaCollectionEmptyNode.swift +++ b/TelegramUI/PeerMediaCollectionEmptyNode.swift @@ -43,7 +43,7 @@ final class PeerMediaCollectionEmptyNode: ASDisplayNode { self.textNode.isLayerBacked = true self.textNode.displaysAsynchronously = false - self.activityIndicator = ActivityIndicator(type: .custom(theme.list.itemSecondaryTextColor), speed: .regular) + self.activityIndicator = ActivityIndicator(type: .custom(theme.list.itemSecondaryTextColor, 22.0), speed: .regular) let icon: UIImage? let text: NSAttributedString diff --git a/TelegramUI/PeerMediaCollectionInterfaceState.swift b/TelegramUI/PeerMediaCollectionInterfaceState.swift index 8d17b2fc7a..7c428e0e81 100644 --- a/TelegramUI/PeerMediaCollectionInterfaceState.swift +++ b/TelegramUI/PeerMediaCollectionInterfaceState.swift @@ -8,24 +8,10 @@ enum PeerMediaCollectionMode: Int32 { case music } -func titleForPeerMediaCollectionMode(_ mode: PeerMediaCollectionMode, strings: PresentationStrings) -> String { - switch mode { - case .photoOrVideo: - return strings.SharedMedia_TitleAll - case .file: - return strings.SharedMedia_TitleFile - case .music: - return strings.SharedMedia_TitleAudio - case .webpage: - return strings.SharedMedia_TitleLink - } -} - struct PeerMediaCollectionInterfaceState: Equatable { let peer: Peer? let selectionState: ChatInterfaceSelectionState? let mode: PeerMediaCollectionMode - let selectingMode: Bool let theme: PresentationTheme let strings: PresentationStrings @@ -35,14 +21,12 @@ struct PeerMediaCollectionInterfaceState: Equatable { self.peer = nil self.selectionState = nil self.mode = .photoOrVideo - self.selectingMode = false } - init(peer: Peer?, selectionState: ChatInterfaceSelectionState?, mode: PeerMediaCollectionMode, selectingMode: Bool, theme: PresentationTheme, strings: PresentationStrings) { + init(peer: Peer?, selectionState: ChatInterfaceSelectionState?, mode: PeerMediaCollectionMode, theme: PresentationTheme, strings: PresentationStrings) { self.peer = peer self.selectionState = selectionState self.mode = mode - self.selectingMode = selectingMode self.theme = theme self.strings = strings } @@ -64,10 +48,6 @@ struct PeerMediaCollectionInterfaceState: Equatable { return false } - if lhs.selectingMode != rhs.selectingMode { - return false - } - if lhs.theme !== rhs.theme { return false } @@ -79,45 +59,45 @@ struct PeerMediaCollectionInterfaceState: Equatable { return true } - func withUpdatedSelectedMessage(_ messageId: MessageId) -> PeerMediaCollectionInterfaceState { + func withUpdatedSelectedMessages(_ messageIds: [MessageId]) -> PeerMediaCollectionInterfaceState { var selectedIds = Set() if let selectionState = self.selectionState { selectedIds.formUnion(selectionState.selectedIds) } - selectedIds.insert(messageId) - return PeerMediaCollectionInterfaceState(peer: self.peer, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds), mode: self.mode, selectingMode: self.selectingMode, theme: self.theme, strings: self.strings) - } - - func withToggledSelectedMessage(_ messageId: MessageId) -> PeerMediaCollectionInterfaceState { - var selectedIds = Set() - if let selectionState = self.selectionState { - selectedIds.formUnion(selectionState.selectedIds) - } - if selectedIds.contains(messageId) { - let _ = selectedIds.remove(messageId) - } else { + for messageId in messageIds { selectedIds.insert(messageId) } - return PeerMediaCollectionInterfaceState(peer: self.peer, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds), mode: self.mode, selectingMode: self.selectingMode, theme: self.theme, strings: self.strings) + return PeerMediaCollectionInterfaceState(peer: self.peer, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds), mode: self.mode, theme: self.theme, strings: self.strings) + } + + func withToggledSelectedMessages(_ messageIds: [MessageId], value: Bool) -> PeerMediaCollectionInterfaceState { + var selectedIds = Set() + if let selectionState = self.selectionState { + selectedIds.formUnion(selectionState.selectedIds) + } + for messageId in messageIds { + if value { + selectedIds.insert(messageId) + } else { + selectedIds.remove(messageId) + } + } + return PeerMediaCollectionInterfaceState(peer: self.peer, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds), mode: self.mode, theme: self.theme, strings: self.strings) } func withSelectionState() -> PeerMediaCollectionInterfaceState { - return PeerMediaCollectionInterfaceState(peer: self.peer, selectionState: self.selectionState ?? ChatInterfaceSelectionState(selectedIds: Set()), mode: self.mode, selectingMode: self.selectingMode, theme: self.theme, strings: self.strings) + return PeerMediaCollectionInterfaceState(peer: self.peer, selectionState: self.selectionState ?? ChatInterfaceSelectionState(selectedIds: Set()), mode: self.mode, theme: self.theme, strings: self.strings) } func withoutSelectionState() -> PeerMediaCollectionInterfaceState { - return PeerMediaCollectionInterfaceState(peer: self.peer, selectionState: nil, mode: self.mode, selectingMode: self.selectingMode, theme: self.theme, strings: self.strings) + return PeerMediaCollectionInterfaceState(peer: self.peer, selectionState: nil, mode: self.mode, theme: self.theme, strings: self.strings) } func withUpdatedPeer(_ peer: Peer?) -> PeerMediaCollectionInterfaceState { - return PeerMediaCollectionInterfaceState(peer: peer, selectionState: self.selectionState, mode: self.mode, selectingMode: self.selectingMode, theme: self.theme, strings: self.strings) - } - - func withToggledSelectingMode() -> PeerMediaCollectionInterfaceState { - return PeerMediaCollectionInterfaceState(peer: self.peer, selectionState: self.selectionState, mode: self.mode, selectingMode: !self.selectingMode, theme: self.theme, strings: self.strings) + return PeerMediaCollectionInterfaceState(peer: peer, selectionState: self.selectionState, mode: self.mode, theme: self.theme, strings: self.strings) } func withMode(_ mode: PeerMediaCollectionMode) -> PeerMediaCollectionInterfaceState { - return PeerMediaCollectionInterfaceState(peer: self.peer, selectionState: self.selectionState, mode: mode, selectingMode: self.selectingMode, theme: self.theme, strings: self.strings) + return PeerMediaCollectionInterfaceState(peer: self.peer, selectionState: self.selectionState, mode: mode, theme: self.theme, strings: self.strings) } } diff --git a/TelegramUI/PeerMediaCollectionInterfaceStateButtons.swift b/TelegramUI/PeerMediaCollectionInterfaceStateButtons.swift index de65a082c0..c2256b0dd8 100644 --- a/TelegramUI/PeerMediaCollectionInterfaceStateButtons.swift +++ b/TelegramUI/PeerMediaCollectionInterfaceStateButtons.swift @@ -20,13 +20,13 @@ func rightNavigationButtonForPeerMediaCollectionInterfaceState(_ interfaceState: if let currentButton = currentButton, currentButton.action == .cancelMessageSelection { return currentButton } else { - return PeerMediaCollectionNavigationButton(action: .cancelMessageSelection, buttonItem: UIBarButtonItem(title: "Cancel", style: .plain, target: target, action: selector)) + return PeerMediaCollectionNavigationButton(action: .cancelMessageSelection, buttonItem: UIBarButtonItem(title: interfaceState.strings.Common_Cancel, style: .plain, target: target, action: selector)) } } else { if let currentButton = currentButton, currentButton.action == .beginMessageSelection { return currentButton } else { - return PeerMediaCollectionNavigationButton(action: .beginMessageSelection, buttonItem: UIBarButtonItem(title: "Select", style: .plain, target: target, action: selector)) + return PeerMediaCollectionNavigationButton(action: .beginMessageSelection, buttonItem: UIBarButtonItem(title: interfaceState.strings.Common_Select, style: .plain, target: target, action: selector)) } } } diff --git a/TelegramUI/PeerMediaCollectionModeSelectionNode.swift b/TelegramUI/PeerMediaCollectionModeSelectionNode.swift deleted file mode 100644 index 7383cf7e09..0000000000 --- a/TelegramUI/PeerMediaCollectionModeSelectionNode.swift +++ /dev/null @@ -1,192 +0,0 @@ -import Foundation -import AsyncDisplayKit -import Display - -private final class PeerMediaCollectionModeSelectionCaseNode: ASDisplayNode { - private let theme: PresentationTheme - private let strings: PresentationStrings - fileprivate let mode: PeerMediaCollectionMode - private let selected: () -> Void - - private let button: HighlightTrackingButton - private let selectionBackgroundNode: ASDisplayNode - private let separatorNode: ASDisplayNode - private let titleNode: ASTextNode - private let checkmarkView: UIImageView - - var isSelected = false { - didSet { - if self.isSelected != oldValue { - self.titleNode.attributedText = NSAttributedString(string: titleForPeerMediaCollectionMode(self.mode, strings: self.strings), font: Font.regular(17.0), textColor: isSelected ? self.theme.list.itemAccentColor : self.theme.list.itemPrimaryTextColor) - self.checkmarkView.isHidden = !self.isSelected - } - } - } - - init(theme: PresentationTheme, strings: PresentationStrings, mode: PeerMediaCollectionMode, selected: @escaping () -> Void) { - self.theme = theme - self.strings = strings - self.mode = mode - self.selected = selected - - self.button = HighlightTrackingButton() - - self.selectionBackgroundNode = ASDisplayNode() - self.selectionBackgroundNode.backgroundColor = self.theme.list.itemHighlightedBackgroundColor - - self.separatorNode = ASDisplayNode() - self.separatorNode.backgroundColor = self.theme.list.itemSeparatorColor - - self.titleNode = ASTextNode() - self.titleNode.displaysAsynchronously = false - self.titleNode.maximumNumberOfLines = 1 - self.titleNode.truncationMode = .byTruncatingTail - self.titleNode.isOpaque = false - - self.checkmarkView = UIImageView(image: PresentationResourcesItemList.checkIconImage(self.theme)) - - super.init() - - self.addSubnode(self.separatorNode) - - self.selectionBackgroundNode.alpha = 0.0 - self.addSubnode(self.selectionBackgroundNode) - - self.titleNode.attributedText = NSAttributedString(string: titleForPeerMediaCollectionMode(mode, strings: self.strings), font: Font.regular(17.0), textColor: self.theme.list.itemPrimaryTextColor) - self.addSubnode(self.titleNode) - - self.checkmarkView.isHidden = true - self.view.addSubview(self.checkmarkView) - - self.button.highligthedChanged = { [weak self] highlighted in - if let strongSelf = self { - if highlighted { - strongSelf.selectionBackgroundNode.layer.removeAnimation(forKey: "opacity") - strongSelf.selectionBackgroundNode.alpha = 1.0 - } else { - strongSelf.selectionBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) - strongSelf.selectionBackgroundNode.layer.opacity = 0.0 - } - } - } - self.button.addTarget(self, action: #selector(self.buttonPressed), for: [.touchUpInside]) - self.view.addSubview(self.button) - } - - func updateFrames(size: CGSize, transition: ContainedViewLayoutTransition) { - transition.updateFrame(layer: self.button.layer, frame: CGRect(origin: CGPoint(), size: size)) - - let leftInset: CGFloat = 15.0 - - transition.updateFrame(node: self.selectionBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: size.width, height: size.height + UIScreenPixel))) - transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: leftInset, y: -UIScreenPixel), size: CGSize(width: size.width - leftInset, height: UIScreenPixel))) - - let titleSize = self.titleNode.measure(CGSize(width: size.width - leftInset - 44.0, height: size.height)) - transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset, y: floor((size.height - titleSize.height) / 2.0)), size: titleSize)) - - let checkmarkSize = self.checkmarkView.bounds.size - transition.updateFrame(layer: self.checkmarkView.layer, frame: CGRect(origin: CGPoint(x: size.width - checkmarkSize.width - 14.0, y: floor((size.height - checkmarkSize.height) / 2.0)), size: checkmarkSize)) - } - - @objc func buttonPressed() { - self.selected() - } -} - -final class PeerMediaCollectionModeSelectionNode: ASDisplayNode { - private let dimNode: ASDisplayNode - private let backgroundNode: ASDisplayNode - - private var caseNodes: [PeerMediaCollectionModeSelectionCaseNode] = [] - - var selectedMode: ((PeerMediaCollectionMode) -> Void)? - var dismiss: (() -> Void)? - - var mediaCollectionInterfaceState: PeerMediaCollectionInterfaceState { - didSet { - for caseNode in self.caseNodes { - caseNode.isSelected = self.mediaCollectionInterfaceState.mode == caseNode.mode - } - } - } - - init(mediaCollectionInterfaceState: PeerMediaCollectionInterfaceState) { - self.mediaCollectionInterfaceState = mediaCollectionInterfaceState - - self.dimNode = ASDisplayNode() - self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.4) - - self.backgroundNode = ASDisplayNode() - self.backgroundNode.backgroundColor = self.mediaCollectionInterfaceState.theme.list.itemBackgroundColor - - super.init() - - let modes: [PeerMediaCollectionMode] = [.photoOrVideo, .file, .webpage, .music] - let selected: (PeerMediaCollectionMode) -> Void = { [weak self] mode in - if let selectedMode = self?.selectedMode { - selectedMode(mode) - } - } - self.caseNodes = modes.map { mode in - return PeerMediaCollectionModeSelectionCaseNode(theme: self.mediaCollectionInterfaceState.theme, strings: self.mediaCollectionInterfaceState.strings, mode: mode, selected: { - selected(mode) - }) - } - - self.addSubnode(self.dimNode) - self.addSubnode(self.backgroundNode) - - for caseNode in self.caseNodes { - self.addSubnode(caseNode) - } - - self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimNodeTap(_:)))) - } - - func animateIn() { - self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - - let backgrounNodePosition = self.backgroundNode.layer.position - self.backgroundNode.layer.animatePosition(from: CGPoint(x: backgrounNodePosition.x, y: backgrounNodePosition.y - self.backgroundNode.bounds.size.height), to: backgrounNodePosition, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) - - for caseNode in self.caseNodes { - let caseNodePosition = caseNode.layer.position - caseNode.layer.animatePosition(from: CGPoint(x: caseNodePosition.x, y: caseNodePosition.y - self.backgroundNode.bounds.size.height), to: caseNodePosition, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) - } - } - - func animateOut(completion: @escaping () -> Void) { - self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) - - let backgrounNodePosition = self.backgroundNode.layer.position - self.backgroundNode.layer.animatePosition(from: backgrounNodePosition, to: CGPoint(x: backgrounNodePosition.x, y: backgrounNodePosition.y - self.backgroundNode.bounds.size.height), duration: 0.2, removeOnCompletion: false, completion: { _ in - completion() - }) - - for caseNode in self.caseNodes { - let caseNodePosition = caseNode.layer.position - caseNode.layer.animatePosition(from: caseNodePosition, to: CGPoint(x: caseNodePosition.x, y: caseNodePosition.y - self.backgroundNode.bounds.size.height), duration: 0.2, removeOnCompletion: false) - } - } - - func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { - transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight), size: CGSize(width: layout.size.width, height: layout.size.height - navigationBarHeight))) - - var nextCaseNodeOrigin = CGPoint(x: 0.0, y: navigationBarHeight) - for caseNode in self.caseNodes { - transition.updateFrame(node: caseNode, frame: CGRect(origin: nextCaseNodeOrigin, size: CGSize(width: layout.size.width, height: 44.0))) - caseNode.updateFrames(size: CGSize(width: layout.size.width, height: 44.0), transition: transition) - nextCaseNodeOrigin.y += 44.0 - } - - transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight), size: CGSize(width: layout.size.width, height: CGFloat(self.caseNodes.count) * 44.0))) - } - - @objc func dimNodeTap(_ recognizer: UITapGestureRecognizer) { - if case .ended = recognizer.state { - if let dismiss = self.dismiss { - dismiss() - } - } - } -} diff --git a/TelegramUI/PeerMediaCollectionSectionsNode.swift b/TelegramUI/PeerMediaCollectionSectionsNode.swift index 4928346d2a..ea5f646307 100644 --- a/TelegramUI/PeerMediaCollectionSectionsNode.swift +++ b/TelegramUI/PeerMediaCollectionSectionsNode.swift @@ -40,8 +40,8 @@ final class PeerMediaCollectionSectionsNode: ASDisplayNode { self.segmentedControl.addTarget(self, action: #selector(indexChanged), for: .valueChanged) } - func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { - let panelHeight: CGFloat = 39.0 + func updateLayout(width: CGFloat, additionalInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { + let panelHeight: CGFloat = 39.0 + additionalInset let controlHeight: CGFloat = 29.0 let sideInset: CGFloat = 8.0 diff --git a/TelegramUI/PeerMessagesMediaPlaylist.swift b/TelegramUI/PeerMessagesMediaPlaylist.swift index 07046ff1f5..0860abc40b 100644 --- a/TelegramUI/PeerMessagesMediaPlaylist.swift +++ b/TelegramUI/PeerMessagesMediaPlaylist.swift @@ -8,27 +8,49 @@ private enum PeerMessagesMediaPlaylistLoadAnchor { case index(MessageIndex) } -struct MessageMediaPlaylistItemId: Hashable { +private enum PeerMessagesMediaPlaylistNavigation { + case earlier + case later + case random +} + +struct MessageMediaPlaylistItemStableId: Hashable { let stableId: UInt32 var hashValue: Int { return self.stableId.hashValue } - static func ==(lhs: MessageMediaPlaylistItemId, rhs: MessageMediaPlaylistItemId) -> Bool { + static func ==(lhs: MessageMediaPlaylistItemStableId, rhs: MessageMediaPlaylistItemStableId) -> Bool { return lhs.stableId == rhs.stableId } } +struct PeerMessagesMediaPlaylistItemId: SharedMediaPlaylistItemId { + let messageId: MessageId + + func isEqual(to: SharedMediaPlaylistItemId) -> Bool { + if let to = to as? PeerMessagesMediaPlaylistItemId { + if self.messageId != to.messageId { + return false + } + return true + } + return false + } +} + final class MessageMediaPlaylistItem: SharedMediaPlaylistItem { + let id: SharedMediaPlaylistItemId let message: Message init(message: Message) { + self.id = PeerMessagesMediaPlaylistItemId(messageId: message.id) self.message = message } var stableId: AnyHashable { - return MessageMediaPlaylistItemId(stableId: message.stableId) + return MessageMediaPlaylistItemStableId(stableId: message.stableId) } var playbackData: SharedMediaPlaybackData? { @@ -66,7 +88,7 @@ final class MessageMediaPlaylistItem: SharedMediaPlaylistItem { if isVoice { return SharedMediaPlaybackDisplayData.voice(author: self.message.author, peer: self.message.peers[self.message.id.peerId]) } else { - return SharedMediaPlaybackDisplayData.music(title: title, performer: performer) + return SharedMediaPlaybackDisplayData.music(title: title, performer: performer, albumArt: SharedMediaPlaybackAlbumArt(thumbnailResource: ExternalMusicAlbumArtResource(title: title ?? "", performer: performer ?? "", isThumbnail: true), fullSizeResource: ExternalMusicAlbumArtResource(title: title ?? "", performer: performer ?? "", isThumbnail: false))) } case let .Video(_, _, flags): if flags.contains(.instantRoundVideo) { @@ -84,85 +106,177 @@ final class MessageMediaPlaylistItem: SharedMediaPlaylistItem { } } -private func navigatedMessageFromView(_ view: MessageHistoryView, anchorIndex: MessageIndex, next: Bool) -> Message? { +private enum NavigatedMessageFromViewPosition { + case later + case earlier + case exact +} + +private func navigatedMessageFromView(_ view: MessageHistoryView, anchorIndex: MessageIndex, position: NavigatedMessageFromViewPosition) -> (message: Message, exact: Bool)? { var index = 0 for entry in view.entries { if entry.index.id == anchorIndex.id { - if next { - if index + 1 < view.entries.count { - switch view.entries[index + 1] { + switch position { + case .exact: + switch entry { case let .MessageEntry(message, _, _, _): - return message + return (message, true) default: return nil } - } else { - return nil - } - } else { - if index != 0 { - switch view.entries[index - 1] { - case let .MessageEntry(message, _, _, _): - return message - default: - return nil + case .later: + if index + 1 < view.entries.count { + switch view.entries[index + 1] { + case let .MessageEntry(message, _, _, _): + return (message, true) + default: + return nil + } + } else { + return nil } - } else { - switch view.entries[0] { - case let .MessageEntry(message, _, _, _): - return message - default: - return nil + case .earlier: + if index != 0 { + switch view.entries[index - 1] { + case let .MessageEntry(message, _, _, _): + return (message, true) + default: + return nil + } + } else { + return nil } - } } } index += 1 } if !view.entries.isEmpty { - switch view.entries[0] { - case let .MessageEntry(message, _, _, _): - return message - default: - return nil + switch position { + case .later, .exact: + switch view.entries[view.entries.count - 1] { + case let .MessageEntry(message, _, _, _): + return (message, false) + default: + return nil + } + case .earlier: + switch view.entries[0] { + case let .MessageEntry(message, _, _, _): + return (message, false) + default: + return nil + } } } else { return nil } } -enum PeerMessagesMediaPlaylistLocation { +enum PeerMessagesPlaylistLocation: Equatable, SharedMediaPlaylistLocation { case messages(peerId: PeerId, tagMask: MessageTags, at: MessageId) case singleMessage(MessageId) + + var peerId: PeerId { + switch self { + case let .messages(peerId, _, _): + return peerId + case let .singleMessage(id): + return id.peerId + } + } + + func isEqual(to: SharedMediaPlaylistLocation) -> Bool { + if let to = to as? PeerMessagesPlaylistLocation { + return self == to + } else { + return false + } + } + + static func ==(lhs: PeerMessagesPlaylistLocation, rhs: PeerMessagesPlaylistLocation) -> Bool { + switch lhs { + case let .messages(peerId, tagMask, at): + if case .messages(peerId, tagMask, at) = rhs { + return true + } else { + return false + } + case let .singleMessage(messageId): + if case .singleMessage(messageId) = rhs { + return true + } else { + return false + } + } + } +} + +struct PeerMessagesMediaPlaylistId: SharedMediaPlaylistId { + let peerId: PeerId + + func isEqual(to: SharedMediaPlaylistId) -> Bool { + if let to = to as? PeerMessagesMediaPlaylistId { + return self.peerId == to.peerId + } + return false + } +} + +func peerMessageMediaPlayerType(_ message: Message) -> MediaManagerPlayerType? { + for media in message.media { + if let file = media as? TelegramMediaFile { + if file.isVoice || file.isInstantVideo { + return .voice + } else if file.isMusic { + return .music + } + } + } + return nil +} + +func peerMessagesMediaPlaylistAndItemId(_ message: Message) -> (SharedMediaPlaylistId, SharedMediaPlaylistItemId)? { + return (PeerMessagesMediaPlaylistId(peerId: message.id.peerId), PeerMessagesMediaPlaylistItemId(messageId: message.id)) } final class PeerMessagesMediaPlaylist: SharedMediaPlaylist { private let postbox: Postbox private let network: Network - private let location: PeerMessagesMediaPlaylistLocation + private let messagesLocation: PeerMessagesPlaylistLocation + + var location: SharedMediaPlaylistLocation { + return self.messagesLocation + } private let navigationDisposable = MetaDisposable() private var currentItem: Message? private var loadingItem: Bool = false + private var playedToEnd: Bool = false + private var order: MusicPlaybackSettingsOrder = .regular + private(set) var looping: MusicPlaybackSettingsLooping = .none + + let id: SharedMediaPlaylistId private let stateValue = Promise() var state: Signal { return self.stateValue.get() } - init(postbox: Postbox, network: Network, location: PeerMessagesMediaPlaylistLocation) { + init(postbox: Postbox, network: Network, location: PeerMessagesPlaylistLocation) { assert(Queue.mainQueue().isCurrent()) + self.id = PeerMessagesMediaPlaylistId(peerId: location.peerId) + self.postbox = postbox self.network = network - self.location = location + self.messagesLocation = location - switch self.location { + switch self.messagesLocation { case let .messages(_, _, messageId): - self.loadItem(anchor: .messageId(messageId), lookForward: true) + self.loadItem(anchor: .messageId(messageId), navigation: .later) case let .singleMessage(messageId): - self.loadItem(anchor: .messageId(messageId), lookForward: true) + self.loadItem(anchor: .messageId(messageId), navigation: .later) } } @@ -177,23 +291,48 @@ final class PeerMessagesMediaPlaylist: SharedMediaPlaylist { case .next, .previous: if !self.loadingItem { if let currentItem = self.currentItem { - let lookForward: Bool - if case .next = action { - lookForward = true - } else { - lookForward = false + let navigation: PeerMessagesMediaPlaylistNavigation + switch self.order { + case .regular: + if case .next = action { + navigation = .earlier + } else { + navigation = .later + } + case .reversed: + if case .next = action { + navigation = .later + } else { + navigation = .earlier + } + case .random: + navigation = .random } - self.loadItem(anchor: .index(MessageIndex(currentItem)), lookForward: lookForward) + self.loadItem(anchor: .index(MessageIndex(currentItem)), navigation: navigation) } } } } - private func updateState() { - self.stateValue.set(.single(SharedMediaPlaylistState(loading: self.loadingItem, item: self.currentItem.flatMap(MessageMediaPlaylistItem.init)))) + func setOrder(_ order: MusicPlaybackSettingsOrder) { + if self.order != order { + self.order = order + self.updateState() + } } - private func loadItem(anchor: PeerMessagesMediaPlaylistLoadAnchor, lookForward: Bool) { + func setLooping(_ looping: MusicPlaybackSettingsLooping) { + if self.looping != looping { + self.looping = looping + self.updateState() + } + } + + private func updateState() { + self.stateValue.set(.single(SharedMediaPlaylistState(loading: self.loadingItem, playedToEnd: self.playedToEnd, item: self.currentItem.flatMap(MessageMediaPlaylistItem.init), order: self.order, looping: self.looping))) + } + + private func loadItem(anchor: PeerMessagesMediaPlaylistLoadAnchor, navigation: PeerMessagesMediaPlaylistNavigation) { self.loadingItem = true self.updateState() switch anchor { @@ -208,14 +347,81 @@ final class PeerMessagesMediaPlaylist: SharedMediaPlaylist { } })) case let .index(index): - switch self.location { + switch self.messagesLocation { case let .messages(peerId, tagMask, _): - self.navigationDisposable.set((self.postbox.aroundMessageHistoryViewForPeerId(peerId, index: index, count: 10, clipHoles: false, anchorIndex: index, fixedCombinedReadState: nil, topTaggedMessageIdNamespaces: [], tagMask: tagMask, orderStatistics: []) |> take(1) |> deliverOnMainQueue).start(next: { [weak self] view in + let inputIndex: Signal + let looping = self.looping + switch self.order { + case .regular, .reversed: + inputIndex = .single(index) + case .random: + inputIndex = self.postbox.modify { modifier -> MessageIndex in + + return modifier.findRandomMessage(peerId: peerId, tagMask: tagMask, ignoreId: index.id) ?? index + } + } + let historySignal = inputIndex |> mapToSignal { inputIndex -> Signal in + return self.postbox.aroundMessageHistoryViewForLocation(.peer(peerId), index: .message(inputIndex), anchorIndex: .message(inputIndex), count: 10, clipHoles: false, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tagMask: tagMask, orderStatistics: []) + |> mapToSignal { view -> Signal in + let position: NavigatedMessageFromViewPosition + switch navigation { + case .later: + position = .later + case .earlier: + position = .earlier + case .random: + position = .exact + } + + if let (message, exact) = navigatedMessageFromView(view.0, anchorIndex: inputIndex, position: position) { + switch navigation { + case .random: + return .single(message) + default: + if exact { + return .single(message) + } + } + } + + if case .all = looping { + let viewIndex: MessageHistoryAnchorIndex + if case .earlier = navigation { + viewIndex = .upperBound + } else { + viewIndex = .lowerBound + } + return self.postbox.aroundMessageHistoryViewForLocation(.peer(peerId), index: viewIndex, anchorIndex: viewIndex, count: 10, clipHoles: false, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tagMask: tagMask, orderStatistics: []) + |> mapToSignal { view -> Signal in + let position: NavigatedMessageFromViewPosition + switch navigation { + case .later, .random: + position = .earlier + case .earlier: + position = .later + } + if let (message, _) = navigatedMessageFromView(view.0, anchorIndex: MessageIndex.absoluteLowerBound(), position: position) { + return .single(message) + } else { + return .single(nil) + } + } + } else { + return .single(nil) + } + } + } |> take(1) |> deliverOnMainQueue + self.navigationDisposable.set(historySignal.start(next: { [weak self] message in if let strongSelf = self { assert(strongSelf.loadingItem) strongSelf.loadingItem = false - strongSelf.currentItem = navigatedMessageFromView(view.0, anchorIndex: index, next: lookForward) + if let message = message { + strongSelf.currentItem = message + strongSelf.playedToEnd = false + } else { + strongSelf.playedToEnd = true + } strongSelf.updateState() } })) @@ -232,4 +438,10 @@ final class PeerMessagesMediaPlaylist: SharedMediaPlaylist { } } } + + func onItemPlaybackStarted(_ item: SharedMediaPlaylistItem) { + if let item = item as? MessageMediaPlaylistItem { + let _ = markMessageContentAsConsumedInteractively(postbox: self.postbox, messageId: item.message.id).start() + } + } } diff --git a/TelegramUI/PeerSelectionController.swift b/TelegramUI/PeerSelectionController.swift index 9a01652b71..df9b01d067 100644 --- a/TelegramUI/PeerSelectionController.swift +++ b/TelegramUI/PeerSelectionController.swift @@ -7,6 +7,9 @@ import Postbox public final class PeerSelectionController: ViewController { private let account: Account + private var presentationData: PresentationData + private var presentationDataDisposable: Disposable? + var peerSelected: ((PeerId) -> Void)? private var peerSelectionNode: PeerSelectionControllerNode { @@ -15,18 +18,26 @@ public final class PeerSelectionController: ViewController { let openMessageFromSearchDisposable: MetaDisposable = MetaDisposable() + private let _ready = Promise() + override public var ready: Promise { + return self._ready + } + public init(account: Account) { self.account = account - super.init(navigationBarTheme: NavigationBarTheme(rootControllerTheme: (account.telegramApplicationContext.currentPresentationData.with { $0 }).theme)) + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } - self.title = "Forward" + super.init(navigationBarTheme: NavigationBarTheme(rootControllerTheme: self.presentationData.theme)) + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Cancel", style: .plain, target: self, action: #selector(self.cancelPressed)) + self.title = self.presentationData.strings.Conversation_ForwardTitle + + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) self.scrollToTop = { [weak self] in if let strongSelf = self { - strongSelf.peerSelectionNode.chatListNode.scrollToLatest() + strongSelf.peerSelectionNode.scrollToTop() } } } @@ -51,33 +62,16 @@ public final class PeerSelectionController: ViewController { self?.deactivateSearch() } - self.peerSelectionNode.chatListNode.activateSearch = { [weak self] in + self.peerSelectionNode.requestActivateSearch = { [weak self] in self?.activateSearch() } - self.peerSelectionNode.chatListNode.peerSelected = { [weak self] peerId in + self.peerSelectionNode.requestOpenPeer = { [weak self] peerId in if let strongSelf = self, let peerSelected = strongSelf.peerSelected { peerSelected(peerId) } } - /*self.peerSelectionNode.requestOpenMessageFromSearch = { [weak self] peer, messageId in - if let strongSelf = self { - let storedPeer = strongSelf.account.postbox.modify { modifier -> Void in - if modifier.getPeer(peer.id) == nil { - updatePeers(modifier: modifier, peers: [peer], update: { previousPeer, updatedPeer in - return updatedPeer - }) - } - } - strongSelf.openMessageFromSearchDisposable.set((storedPeer |> deliverOnMainQueue).start(completed: { [weak strongSelf] in - if let strongSelf = strongSelf { - (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, peerId: messageId.peerId, messageId: messageId)) - } - })) - } - }*/ - self.peerSelectionNode.requestOpenPeerFromSearch = { [weak self] peer in if let strongSelf = self { let storedPeer = strongSelf.account.postbox.modify { modifier -> Void in @@ -96,6 +90,8 @@ public final class PeerSelectionController: ViewController { } self.displayNodeDidLoad() + + self._ready.set(self.peerSelectionNode.ready) } override public func viewWillAppear(_ animated: Bool) { @@ -140,6 +136,7 @@ public final class PeerSelectionController: ViewController { } override open func dismiss(completion: (() -> Void)? = nil) { + self.peerSelectionNode.view.endEditing(true) self.peerSelectionNode.animateOut(completion: completion) } } diff --git a/TelegramUI/PeerSelectionControllerNode.swift b/TelegramUI/PeerSelectionControllerNode.swift index f6a0a34c1d..b236800d80 100644 --- a/TelegramUI/PeerSelectionControllerNode.swift +++ b/TelegramUI/PeerSelectionControllerNode.swift @@ -9,27 +9,52 @@ final class PeerSelectionControllerNode: ASDisplayNode { private let account: Account private let dismiss: () -> Void - let chatListNode: ChatListNode var navigationBar: NavigationBar? + private let toolbarBackgroundNode: ASDisplayNode + private let toolbarSeparatorNode: ASDisplayNode + private let segmentedControl: UISegmentedControl + + private var contactListNode: ContactListNode? + private let chatListNode: ChatListNode + + private var contactListActive = false + private var searchDisplayController: SearchDisplayController? private var containerLayout: (ContainerViewLayout, CGFloat)? + var requestActivateSearch: (() -> Void)? var requestDeactivateSearch: (() -> Void)? + var requestOpenPeer: ((PeerId) -> Void)? var requestOpenPeerFromSearch: ((Peer) -> Void)? var requestOpenMessageFromSearch: ((Peer, MessageId) -> Void)? private var presentationData: PresentationData private var presentationDataDisposable: Disposable? + private var readyValue = Promise() + var ready: Signal { + return self.readyValue.get() + } + init(account: Account, dismiss: @escaping () -> Void) { self.account = account self.dismiss = dismiss self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } - self.chatListNode = ChatListNode(account: account, mode: .peers, theme: presentationData.theme, strings: presentationData.strings) + self.toolbarBackgroundNode = ASDisplayNode() + self.toolbarBackgroundNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.backgroundColor + + self.toolbarSeparatorNode = ASDisplayNode() + self.toolbarSeparatorNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor + + self.segmentedControl = UISegmentedControl(items: [self.presentationData.strings.DialogList_TabTitle, self.presentationData.strings.Contacts_TabTitle]) + self.segmentedControl.tintColor = self.presentationData.theme.rootController.navigationBar.accentTextColor + self.segmentedControl.selectedSegmentIndex = 0 + + self.chatListNode = ChatListNode(account: account, groupId: nil, controlsHistoryPreload: false, mode: .peers(onlyWriteable: true), theme: presentationData.theme, strings: presentationData.strings, timeFormat: presentationData.timeFormat) super.init() @@ -37,6 +62,14 @@ final class PeerSelectionControllerNode: ASDisplayNode { return UITracingLayerView() }) + self.chatListNode.activateSearch = { [weak self] in + self?.requestActivateSearch?() + } + + self.chatListNode.peerSelected = { [weak self] peerId in + self?.requestOpenPeer?(peerId) + } + self.addSubnode(self.chatListNode) self.presentationDataDisposable = (account.telegramApplicationContext.presentationData |> deliverOnMainQueue).start(next: { [weak self] presentationData in @@ -49,6 +82,15 @@ final class PeerSelectionControllerNode: ASDisplayNode { } } }) + + self.addSubnode(self.toolbarBackgroundNode) + self.addSubnode(self.toolbarSeparatorNode) + + self.view.addSubview(self.segmentedControl) + + self.segmentedControl.addTarget(self, action: #selector(indexChanged), for: .valueChanged) + + self.readyValue.set(self.chatListNode.ready) } deinit { @@ -57,14 +99,28 @@ final class PeerSelectionControllerNode: ASDisplayNode { private func updateThemeAndStrings() { self.searchDisplayController?.updateThemeAndStrings(theme: self.presentationData.theme, strings: self.presentationData.strings) - self.chatListNode.updateThemeAndStrings(theme: self.presentationData.theme, strings: self.presentationData.strings) + self.chatListNode.updateThemeAndStrings(theme: self.presentationData.theme, strings: self.presentationData.strings, timeFormat: self.presentationData.timeFormat) } func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { self.containerLayout = (layout, navigationBarHeight) + let cleanInsets = layout.insets(options: []) + + let toolbarHeight: CGFloat = 44.0 + cleanInsets.bottom + + transition.updateFrame(node: self.toolbarBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - toolbarHeight), size: CGSize(width: layout.size.width, height: toolbarHeight))) + transition.updateFrame(node: self.toolbarSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - toolbarHeight), size: CGSize(width: layout.size.width, height: UIScreenPixel))) + + var controlSize = self.segmentedControl.sizeThatFits(layout.size) + controlSize.width = min(layout.size.width, max(200.0, controlSize.width)) + transition.updateFrame(view: self.segmentedControl, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - controlSize.width) / 2.0), y: layout.size.height - toolbarHeight + floor((44.0 - controlSize.height) / 2.0)), size: controlSize)) + var insets = layout.insets(options: [.input]) insets.top += max(navigationBarHeight, layout.insets(options: [.statusBar]).top) + insets.bottom = max(insets.bottom, cleanInsets.bottom) + insets.left += layout.safeInsets.left + insets.right += layout.safeInsets.right self.chatListNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) self.chatListNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0) @@ -72,20 +128,19 @@ final class PeerSelectionControllerNode: ASDisplayNode { var duration: Double = 0.0 var curve: UInt = 0 switch transition { - case .immediate: - break - case let .animated(animationDuration, animationCurve): - duration = animationDuration - switch animationCurve { - case .easeInOut: + case .immediate: break - case .spring: - curve = 7 - } + case let .animated(animationDuration, animationCurve): + duration = animationDuration + switch animationCurve { + case .easeInOut: + break + case .spring: + curve = 7 + } } let listViewCurve: ListViewAnimationCurve - var speedFactor: CGFloat = 1.0 if curve == 7 { listViewCurve = .Spring(duration: duration) } else { @@ -96,6 +151,15 @@ final class PeerSelectionControllerNode: ASDisplayNode { self.chatListNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets) + if let contactListNode = self.contactListNode { + contactListNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) + contactListNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0) + + let contactsInsets = insets + + contactListNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: contactsInsets, safeInsets: layout.safeInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging), transition: transition) + } + if let searchDisplayController = self.searchDisplayController { searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) } @@ -106,41 +170,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { return } - var maybePlaceholderNode: SearchBarPlaceholderNode? - self.chatListNode.forEachItemNode { node in - if let node = node as? ChatListSearchItemNode { - maybePlaceholderNode = node.searchBarNode - } - } - - if let _ = self.searchDisplayController { - return - } - - if let placeholderNode = maybePlaceholderNode { - self.searchDisplayController = SearchDisplayController(theme: self.presentationData.theme, strings: self.presentationData.strings, contentNode: ChatListSearchContainerNode(account: self.account, openPeer: { [weak self] peer in - if let requestOpenPeerFromSearch = self?.requestOpenPeerFromSearch { - requestOpenPeerFromSearch(peer) - } - }, openMessage: { [weak self] peer, messageId in - if let requestOpenMessageFromSearch = self?.requestOpenMessageFromSearch { - requestOpenMessageFromSearch(peer, messageId) - } - }), cancel: { [weak self] in - if let requestDeactivateSearch = self?.requestDeactivateSearch { - requestDeactivateSearch() - } - }) - - self.searchDisplayController?.containerLayoutUpdated(containerLayout, navigationBarHeight: navigationBarHeight, transition: .immediate) - self.searchDisplayController?.activate(insertSubnode: { subnode in - self.insertSubnode(subnode, belowSubnode: navigationBar) - }, placeholder: placeholderNode) - } - } - - func deactivateSearch() { - if let searchDisplayController = self.searchDisplayController { + if self.chatListNode.supernode != nil { var maybePlaceholderNode: SearchBarPlaceholderNode? self.chatListNode.forEachItemNode { node in if let node = node as? ChatListSearchItemNode { @@ -148,22 +178,158 @@ final class PeerSelectionControllerNode: ASDisplayNode { } } - searchDisplayController.deactivate(placeholder: maybePlaceholderNode) - self.searchDisplayController = nil + if let _ = self.searchDisplayController { + return + } + + if let placeholderNode = maybePlaceholderNode { + self.searchDisplayController = SearchDisplayController(theme: self.presentationData.theme, strings: self.presentationData.strings, contentNode: ChatListSearchContainerNode(account: self.account, onlyWriteable: true, groupId: nil, openPeer: { [weak self] peer in + if let requestOpenPeerFromSearch = self?.requestOpenPeerFromSearch { + requestOpenPeerFromSearch(peer) + } + }, openRecentPeerOptions: { _ in + },openMessage: { [weak self] peer, messageId in + if let requestOpenMessageFromSearch = self?.requestOpenMessageFromSearch { + requestOpenMessageFromSearch(peer, messageId) + } + }), cancel: { [weak self] in + if let requestDeactivateSearch = self?.requestDeactivateSearch { + requestDeactivateSearch() + } + }) + + self.searchDisplayController?.containerLayoutUpdated(containerLayout, navigationBarHeight: navigationBarHeight, transition: .immediate) + self.searchDisplayController?.activate(insertSubnode: { subnode in + self.insertSubnode(subnode, belowSubnode: navigationBar) + }, placeholder: placeholderNode) + } + } else if let contactListNode = self.contactListNode, contactListNode.supernode != nil { + var maybePlaceholderNode: SearchBarPlaceholderNode? + contactListNode.listNode.forEachItemNode { node in + if let node = node as? ChatListSearchItemNode { + maybePlaceholderNode = node.searchBarNode + } + } + + if let _ = self.searchDisplayController { + return + } + + if let placeholderNode = maybePlaceholderNode { + self.searchDisplayController = SearchDisplayController(theme: self.presentationData.theme, strings: self.presentationData.strings, contentNode: ContactsSearchContainerNode(account: self.account, onlyWriteable: true, openPeer: { [weak self] peerId in + if let strongSelf = self { + let _ = (strongSelf.account.postbox.modify { modifier -> Peer? in + return modifier.getPeer(peerId) + } |> deliverOnMainQueue).start(next: { peer in + if let strongSelf = self, let peer = peer { + strongSelf.requestOpenPeerFromSearch?(peer) + } + }) + } + }), cancel: { [weak self] in + if let requestDeactivateSearch = self?.requestDeactivateSearch { + requestDeactivateSearch() + } + }) + + self.searchDisplayController?.containerLayoutUpdated(containerLayout, navigationBarHeight: navigationBarHeight, transition: .immediate) + self.searchDisplayController?.activate(insertSubnode: { subnode in + self.insertSubnode(subnode, belowSubnode: navigationBar) + }, placeholder: placeholderNode) + } + } + } + + func deactivateSearch() { + if let searchDisplayController = self.searchDisplayController { + if self.chatListNode.supernode != nil { + var maybePlaceholderNode: SearchBarPlaceholderNode? + self.chatListNode.forEachItemNode { node in + if let node = node as? ChatListSearchItemNode { + maybePlaceholderNode = node.searchBarNode + } + } + + searchDisplayController.deactivate(placeholder: maybePlaceholderNode) + self.searchDisplayController = nil + } else if let contactListNode = self.contactListNode, contactListNode.supernode != nil { + var maybePlaceholderNode: SearchBarPlaceholderNode? + contactListNode.listNode.forEachItemNode { node in + if let node = node as? ChatListSearchItemNode { + maybePlaceholderNode = node.searchBarNode + } + } + + searchDisplayController.deactivate(placeholder: maybePlaceholderNode) + self.searchDisplayController = nil + } + } + } + + func scrollToTop() { + if self.chatListNode.supernode != nil { + self.chatListNode.scrollToLatest() + } else if let contactListNode = self.contactListNode, contactListNode.supernode != nil { + contactListNode.scrollToTop() } } func animateIn() { - self.layer.animatePosition(from: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), to: self.layer.position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, completion: { [weak self] _ in - }) + self.layer.animatePosition(from: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), to: self.layer.position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) } func animateOut(completion: (() -> Void)? = nil) { - self.layer.animatePosition(from: self.layer.position, to: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), duration: 0.2, timingFunction: kCAMediaTimingFunctionEaseInEaseOut, removeOnCompletion: false, completion: { [weak self] _ in + self.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: self.layer.bounds.size.height), duration: 0.2, timingFunction: kCAMediaTimingFunctionEaseInEaseOut, removeOnCompletion: false, additive: true, completion: { [weak self] _ in if let strongSelf = self { strongSelf.dismiss() } completion?() }) } + + @objc func indexChanged() { + guard let (layout, navigationHeight) = self.containerLayout else { + return + } + + let contactListActive = self.segmentedControl.selectedSegmentIndex == 1 + if contactListActive != self.contactListActive { + self.contactListActive = contactListActive + if contactListActive { + if let contactListNode = self.contactListNode { + self.insertSubnode(contactListNode, aboveSubnode: self.chatListNode) + self.chatListNode.removeFromSupernode() + self.recursivelyEnsureDisplaySynchronously(true) + contactListNode.enableUpdates = true + } else { + let contactListNode = ContactListNode(account: account, presentation: .natural(displaySearch: true, options: [])) + self.contactListNode = contactListNode + contactListNode.enableUpdates = true + contactListNode.activateSearch = { [weak self] in + self?.requestActivateSearch?() + } + contactListNode.openPeer = { [weak self] peer in + self?.requestOpenPeer?(peer.id) + } + + self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .immediate) + + let _ = (contactListNode.ready |> deliverOnMainQueue).start(next: { [weak self] _ in + if let strongSelf = self { + if let contactListNode = strongSelf.contactListNode { + strongSelf.insertSubnode(contactListNode, aboveSubnode: strongSelf.chatListNode) + } + strongSelf.chatListNode.removeFromSupernode() + strongSelf.recursivelyEnsureDisplaySynchronously(true) + } + }) + } + } else if let contactListNode = self.contactListNode { + contactListNode.enableUpdates = false + + self.insertSubnode(chatListNode, aboveSubnode: contactListNode) + contactListNode.removeFromSupernode() + } + } + } } diff --git a/TelegramUI/PhoneInputNode.swift b/TelegramUI/PhoneInputNode.swift index 7f304ee35a..3386c2c26b 100644 --- a/TelegramUI/PhoneInputNode.swift +++ b/TelegramUI/PhoneInputNode.swift @@ -177,7 +177,8 @@ final class PhoneInputNode: ASDisplayNode, UITextFieldDelegate { } if self.previousCountryCodeText != realRegionPrefix { self.previousCountryCodeText = realRegionPrefix - self.countryCodeUpdated?(removePlus(realRegionPrefix).trimmingCharacters(in: CharacterSet.whitespaces)) + let code = removePlus(realRegionPrefix).trimmingCharacters(in: CharacterSet.whitespaces) + self.countryCodeUpdated?(code) } if numberText != self.numberField.textField.text { diff --git a/TelegramUI/PhotoResources.swift b/TelegramUI/PhotoResources.swift index e4c38bcd9b..aae58a04ad 100644 --- a/TelegramUI/PhotoResources.swift +++ b/TelegramUI/PhotoResources.swift @@ -16,21 +16,21 @@ func largestRepresentationForPhoto(_ photo: TelegramMediaImage) -> TelegramMedia return photo.representationForDisplayAtSize(CGSize(width: 1280.0, height: 1280.0)) } -private func chatMessagePhotoDatas(account: Account, photo: TelegramMediaImage, fullRepresentationSize: CGSize = CGSize(width: 1280.0, height: 1280.0), autoFetchFullSize: Bool = false) -> Signal<(Data?, Data?, Bool), NoError> { +private func chatMessagePhotoDatas(postbox: Postbox, photo: TelegramMediaImage, fullRepresentationSize: CGSize = CGSize(width: 1280.0, height: 1280.0), autoFetchFullSize: Bool = false) -> Signal<(Data?, Data?, Bool), NoError> { if let smallestRepresentation = smallestImageRepresentation(photo.representations), let largestRepresentation = photo.representationForDisplayAtSize(fullRepresentationSize) { - let maybeFullSize = account.postbox.mediaBox.resourceData(largestRepresentation.resource) + let maybeFullSize = postbox.mediaBox.resourceData(largestRepresentation.resource) let signal = maybeFullSize |> take(1) |> mapToSignal { maybeData -> Signal<(Data?, Data?, Bool), NoError> in if maybeData.complete { let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []) return .single((nil, loadedData, true)) } else { - let fetchedThumbnail = account.postbox.mediaBox.fetchedResource(smallestRepresentation.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .image)) - let fetchedFullSize = account.postbox.mediaBox.fetchedResource(largestRepresentation.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .image)) + let fetchedThumbnail = postbox.mediaBox.fetchedResource(smallestRepresentation.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .image)) + let fetchedFullSize = postbox.mediaBox.fetchedResource(largestRepresentation.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .image)) let thumbnail = Signal { subscriber in let fetchedDisposable = fetchedThumbnail.start() - let thumbnailDisposable = account.postbox.mediaBox.resourceData(smallestRepresentation.resource).start(next: { next in + let thumbnailDisposable = postbox.mediaBox.resourceData(smallestRepresentation.resource).start(next: { next in subscriber.putNext(next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: [])) }, error: subscriber.putError, completed: subscriber.putCompletion) @@ -45,7 +45,7 @@ private func chatMessagePhotoDatas(account: Account, photo: TelegramMediaImage, if autoFetchFullSize { fullSizeData = Signal<(Data?, Bool), NoError> { subscriber in let fetchedFullSizeDisposable = fetchedFullSize.start() - let fullSizeDisposable = account.postbox.mediaBox.resourceData(largestRepresentation.resource).start(next: { next in + let fullSizeDisposable = postbox.mediaBox.resourceData(largestRepresentation.resource).start(next: { next in subscriber.putNext((next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []), next.complete)) }, error: subscriber.putError, completed: subscriber.putCompletion) @@ -55,7 +55,7 @@ private func chatMessagePhotoDatas(account: Account, photo: TelegramMediaImage, } } } else { - fullSizeData = account.postbox.mediaBox.resourceData(largestRepresentation.resource) + fullSizeData = postbox.mediaBox.resourceData(largestRepresentation.resource) |> map { next -> (Data?, Bool) in return (next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []), next.complete) } @@ -68,7 +68,13 @@ private func chatMessagePhotoDatas(account: Account, photo: TelegramMediaImage, } } } - } |> filter({ $0.0 != nil || $0.1 != nil }) + } |> distinctUntilChanged(isEqual: { lhs, rhs in + if (lhs.0 == nil && lhs.1 == nil) && (rhs.0 == nil && rhs.1 == nil) { + return true + } else { + return false + } + }) return signal } else { @@ -125,12 +131,88 @@ private func chatMessageFileDatas(account: Account, file: TelegramMediaFile, pat return signal } -private func chatMessageVideoDatas(account: Account, file: TelegramMediaFile, thumbnailSize: Bool = false) -> Signal<(Data?, (Data, String)?, Bool), NoError> { +private let thumbnailGenerationMimeTypes: Set = Set([ + "image/jpeg", + "image/jpg", + "image/png", + "image/gif" +]) + +private func chatMessageImageFileThumbnailDatas(account: Account, file: TelegramMediaFile, pathExtension: String? = nil, progressive: Bool = false) -> Signal<(Data?, String?, Bool), NoError> { + let thumbnailResource = smallestImageRepresentation(file.previewRepresentations)?.resource + + if !thumbnailGenerationMimeTypes.contains(file.mimeType) { + if let thumbnailResource = thumbnailResource { + let fetchedThumbnail: Signal = account.postbox.mediaBox.fetchedResource(thumbnailResource, tag: TelegramMediaResourceFetchTag(statsCategory: .file)) + return Signal { subscriber in + let fetchedDisposable = fetchedThumbnail.start() + let thumbnailDisposable = account.postbox.mediaBox.resourceData(thumbnailResource, pathExtension: pathExtension).start(next: { next in + subscriber.putNext(((next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: [])), nil, false)) + }, error: subscriber.putError, completed: subscriber.putCompletion) + + return ActionDisposable { + fetchedDisposable.dispose() + thumbnailDisposable.dispose() + } + } + } else { + return .single((nil, nil, false)) + } + } + + let fullSizeResource: MediaResource = file.resource + + let maybeFullSize = account.postbox.mediaBox.cachedResourceRepresentation(fullSizeResource, representation: CachedScaledImageRepresentation(size: CGSize(width: 180.0, height: 180.0), mode: .aspectFit), complete: false) + + let signal = maybeFullSize |> take(1) |> mapToSignal { maybeData -> Signal<(Data?, String?, Bool), NoError> in + if maybeData.complete { + return .single((nil, maybeData.path, true)) + } else { + let fetchedThumbnail: Signal + if let thumbnailResource = thumbnailResource { + fetchedThumbnail = account.postbox.mediaBox.fetchedResource(thumbnailResource, tag: TelegramMediaResourceFetchTag(statsCategory: .file)) + } else { + fetchedThumbnail = .complete() + } + + let thumbnail: Signal + if let thumbnailResource = thumbnailResource { + thumbnail = Signal { subscriber in + let fetchedDisposable = fetchedThumbnail.start() + let thumbnailDisposable = account.postbox.mediaBox.resourceData(thumbnailResource, pathExtension: pathExtension).start(next: { next in + subscriber.putNext(next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: [])) + }, error: subscriber.putError, completed: subscriber.putCompletion) + + return ActionDisposable { + fetchedDisposable.dispose() + thumbnailDisposable.dispose() + } + } + } else { + thumbnail = .single(nil) + } + + let fullSizeDataAndPath = maybeFullSize |> map { next -> (String?, Bool) in + return (next.size == 0 ? nil : next.path, next.complete) + } + + return thumbnail |> mapToSignal { thumbnailData in + return fullSizeDataAndPath |> map { (dataPath, complete) in + return (thumbnailData, dataPath, complete) + } + } + } + } |> filter({ $0.0 != nil || $0.1 != nil }) + + return signal +} + +private func chatMessageVideoDatas(postbox: Postbox, file: TelegramMediaFile, thumbnailSize: Bool = false) -> Signal<(Data?, (Data, String)?, Bool), NoError> { if let smallestRepresentation = smallestImageRepresentation(file.previewRepresentations) { let thumbnailResource = smallestRepresentation.resource let fullSizeResource = file.resource - let maybeFullSize = account.postbox.mediaBox.cachedResourceRepresentation(fullSizeResource, representation: thumbnailSize ? CachedScaledVideoFirstFrameRepresentation(size: CGSize(width: 160.0, height: 160.0)) : CachedVideoFirstFrameRepresentation(), complete: false) + let maybeFullSize = postbox.mediaBox.cachedResourceRepresentation(fullSizeResource, representation: thumbnailSize ? CachedScaledVideoFirstFrameRepresentation(size: CGSize(width: 160.0, height: 160.0)) : CachedVideoFirstFrameRepresentation(), complete: false) let signal = maybeFullSize |> take(1) |> mapToSignal { maybeData -> Signal<(Data?, (Data, String)?, Bool), NoError> in if maybeData.complete { @@ -138,11 +220,11 @@ private func chatMessageVideoDatas(account: Account, file: TelegramMediaFile, th return .single((nil, loadedData == nil ? nil : (loadedData!, maybeData.path), true)) } else { - let fetchedThumbnail = account.postbox.mediaBox.fetchedResource(thumbnailResource, tag: TelegramMediaResourceFetchTag(statsCategory: .video)) + let fetchedThumbnail = postbox.mediaBox.fetchedResource(thumbnailResource, tag: TelegramMediaResourceFetchTag(statsCategory: .video)) let thumbnail = Signal { subscriber in let fetchedDisposable = fetchedThumbnail.start() - let thumbnailDisposable = account.postbox.mediaBox.resourceData(thumbnailResource).start(next: { next in + let thumbnailDisposable = postbox.mediaBox.resourceData(thumbnailResource).start(next: { next in subscriber.putNext(next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: [])) }, error: subscriber.putError, completed: subscriber.putCompletion) @@ -403,15 +485,21 @@ private func addCorners(_ context: DrawingContext, arguments: TransformImageArgu let corner = cornerContext(.BottomLeft(Int(radius))) context.blt(corner, at: CGPoint(x: drawingRect.minX, y: drawingRect.maxY - radius)) } - case let .Tail(radius): + case let .Tail(radius, enabled): if radius > CGFloat.ulpOfOne { - let tail = tailContext(.BottomLeft(Int(radius))) - let color = context.colorAt(CGPoint(x: drawingRect.minX, y: drawingRect.maxY - 1.0)) - context.withContext { c in - c.setFillColor(color.cgColor) - c.fill(CGRect(x: 0.0, y: drawingRect.maxY - 6.0, width: 3.0, height: 6.0)) + if enabled { + let tail = tailContext(.BottomLeft(Int(radius))) + let color = context.colorAt(CGPoint(x: drawingRect.minX, y: drawingRect.maxY - 1.0)) + context.withContext { c in + c.clear(CGRect(x: drawingRect.minX - 3.0, y: 0.0, width: 3.0, height: drawingRect.maxY - 6.0)) + c.setFillColor(color.cgColor) + c.fill(CGRect(x: 0.0, y: drawingRect.maxY - 6.0, width: 3.0, height: 6.0)) + } + context.blt(tail, at: CGPoint(x: drawingRect.minX - 3.0, y: drawingRect.maxY - radius)) + } else { + let corner = cornerContext(.BottomLeft(Int(radius))) + context.blt(corner, at: CGPoint(x: drawingRect.minX, y: drawingRect.maxY - radius)) } - context.blt(tail, at: CGPoint(x: drawingRect.minX - 3.0, y: drawingRect.maxY - radius)) } } @@ -422,21 +510,27 @@ private func addCorners(_ context: DrawingContext, arguments: TransformImageArgu let corner = cornerContext(.BottomRight(Int(radius))) context.blt(corner, at: CGPoint(x: drawingRect.maxX - radius, y: drawingRect.maxY - radius)) } - case let .Tail(radius): + case let .Tail(radius, enabled): if radius > CGFloat.ulpOfOne { - let tail = tailContext(.BottomRight(Int(radius))) - let color = context.colorAt(CGPoint(x: drawingRect.maxX - 1.0, y: drawingRect.maxY - 1.0)) - context.withContext { c in - c.setFillColor(color.cgColor) - c.fill(CGRect(x: drawingRect.maxX, y: drawingRect.maxY - 6.0, width: 3.0, height: 6.0)) + if enabled { + let tail = tailContext(.BottomRight(Int(radius))) + let color = context.colorAt(CGPoint(x: drawingRect.maxX - 1.0, y: drawingRect.maxY - 1.0)) + context.withContext { c in + c.clear(CGRect(x: drawingRect.maxX, y: 0.0, width: 3.0, height: drawingRect.maxY - 6.0)) + c.setFillColor(color.cgColor) + c.fill(CGRect(x: drawingRect.maxX, y: drawingRect.maxY - 6.0, width: 3.0, height: 6.0)) + } + context.blt(tail, at: CGPoint(x: drawingRect.maxX - radius, y: drawingRect.maxY - radius)) + } else { + let corner = cornerContext(.BottomRight(Int(radius))) + context.blt(corner, at: CGPoint(x: drawingRect.maxX - radius, y: drawingRect.maxY - radius)) } - context.blt(tail, at: CGPoint(x: drawingRect.maxX - radius, y: drawingRect.maxY - radius)) } } } -func rawMessagePhoto(account: Account, photo: TelegramMediaImage) -> Signal { - return chatMessagePhotoDatas(account: account, photo: photo, autoFetchFullSize: true) +func rawMessagePhoto(postbox: Postbox, photo: TelegramMediaImage) -> Signal { + return chatMessagePhotoDatas(postbox: postbox, photo: photo, autoFetchFullSize: true) |> map { (thumbnailData, fullSizeData, fullSizeComplete) -> UIImage? in if let fullSizeData = fullSizeData { if fullSizeComplete { @@ -450,8 +544,8 @@ func rawMessagePhoto(account: Account, photo: TelegramMediaImage) -> Signal Signal<(TransformImageArguments) -> DrawingContext?, NoError> { - let signal = chatMessagePhotoDatas(account: account, photo: photo) +func chatMessagePhoto(postbox: Postbox, photo: TelegramMediaImage) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { + let signal = chatMessagePhotoDatas(postbox: postbox, photo: photo) return signal |> map { (thumbnailData, fullSizeData, fullSizeComplete) in return { arguments in @@ -471,12 +565,6 @@ func chatMessagePhoto(account: Account, photo: TelegramMediaImage) -> Signal<(Tr var fullSizeImage: CGImage? if let fullSizeData = fullSizeData { if fullSizeComplete { - /*let options = NSMutableDictionary() - options.setValue(max(fittedSize.width * context.scale, fittedSize.height * context.scale) as NSNumber, forKey: kCGImageSourceThumbnailMaxPixelSize as String) - options.setValue(true as NSNumber, forKey: kCGImageSourceCreateThumbnailFromImageAlways as String) - if let imageSource = CGImageSourceCreateWithData(fullSizeData as CFData, nil), let image = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options as CFDictionary) { - fullSizeImage = image - }*/ let options = NSMutableDictionary() options[kCGImageSourceShouldCache as NSString] = false as NSNumber if let imageSource = CGImageSourceCreateWithData(fullSizeData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options as CFDictionary) { @@ -515,45 +603,49 @@ func chatMessagePhoto(account: Account, photo: TelegramMediaImage) -> Signal<(Tr context.withFlippedContext { c in c.setBlendMode(.copy) - if arguments.imageSize.width < arguments.boundingSize.width || arguments.imageSize.height < arguments.boundingSize.height { - - let blurSourceImage = thumbnailImage ?? fullSizeImage - - if let fullSizeImage = blurSourceImage { - let thumbnailSize = CGSize(width: fullSizeImage.width, height: fullSizeImage.height) - let thumbnailContextSize = thumbnailSize.aspectFitted(CGSize(width: 74.0, height: 74.0)) - let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) - thumbnailContext.withFlippedContext { c in - c.interpolationQuality = .none - c.draw(fullSizeImage, in: CGRect(origin: CGPoint(), size: thumbnailContextSize)) - } - telegramFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) - telegramFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) + if thumbnailImage == nil && fullSizeImage == nil { + c.setFillColor(UIColor.white.cgColor) + c.fill(fittedRect) + } else { + if arguments.imageSize.width < arguments.boundingSize.width || arguments.imageSize.height < arguments.boundingSize.height { + let blurSourceImage = thumbnailImage ?? fullSizeImage - if let blurredImage = thumbnailContext.generateImage() { - let filledSize = thumbnailSize.aspectFilled(arguments.drawingRect.size) - c.interpolationQuality = .medium - c.draw(blurredImage.cgImage!, in: CGRect(origin: CGPoint(x: (arguments.drawingRect.width - filledSize.width) / 2.0, y: (arguments.drawingRect.height - filledSize.height) / 2.0), size: filledSize)) - c.setBlendMode(.normal) - c.setFillColor(UIColor(white: 1.0, alpha: 0.5).cgColor) + if let fullSizeImage = blurSourceImage { + let thumbnailSize = CGSize(width: fullSizeImage.width, height: fullSizeImage.height) + let thumbnailContextSize = thumbnailSize.aspectFitted(CGSize(width: 74.0, height: 74.0)) + let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) + thumbnailContext.withFlippedContext { c in + c.interpolationQuality = .none + c.draw(fullSizeImage, in: CGRect(origin: CGPoint(), size: thumbnailContextSize)) + } + telegramFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) + telegramFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) + + if let blurredImage = thumbnailContext.generateImage() { + let filledSize = thumbnailSize.aspectFilled(arguments.drawingRect.size) + c.interpolationQuality = .medium + c.draw(blurredImage.cgImage!, in: CGRect(origin: CGPoint(x:arguments.drawingRect.minX + (arguments.drawingRect.width - filledSize.width) / 2.0, y: arguments.drawingRect.minY + (arguments.drawingRect.height - filledSize.height) / 2.0), size: filledSize)) + c.setBlendMode(.normal) + c.setFillColor(UIColor(white: 1.0, alpha: 0.5).cgColor) + c.fill(arguments.drawingRect) + c.setBlendMode(.copy) + } + } else { c.fill(arguments.drawingRect) - c.setBlendMode(.copy) } - } else { - c.fill(arguments.drawingRect) } - } - - c.setBlendMode(.copy) - if let blurredThumbnailImage = blurredThumbnailImage, let cgImage = blurredThumbnailImage.cgImage { - c.interpolationQuality = .low - c.draw(cgImage, in: fittedRect) - c.setBlendMode(.normal) - } - - if let fullSizeImage = fullSizeImage { - c.interpolationQuality = .medium - c.draw(fullSizeImage, in: fittedRect) + + c.setBlendMode(.copy) + if let blurredThumbnailImage = blurredThumbnailImage, let cgImage = blurredThumbnailImage.cgImage { + c.interpolationQuality = .low + c.draw(cgImage, in: fittedRect) + c.setBlendMode(.normal) + } + + if let fullSizeImage = fullSizeImage { + c.interpolationQuality = .medium + c.draw(fullSizeImage, in: fittedRect) + } } } @@ -568,7 +660,7 @@ private func chatMessagePhotoThumbnailDatas(account: Account, photo: TelegramMed let fullRepresentationSize: CGSize = CGSize(width: 1280.0, height: 1280.0) if let smallestRepresentation = smallestImageRepresentation(photo.representations), let largestRepresentation = photo.representationForDisplayAtSize(fullRepresentationSize) { - let maybeFullSize = account.postbox.mediaBox.cachedResourceRepresentation(largestRepresentation.resource, representation: CachedScaledImageRepresentation(size: CGSize(width: 160.0, height: 160.0)), complete: false) + let maybeFullSize = account.postbox.mediaBox.cachedResourceRepresentation(largestRepresentation.resource, representation: CachedScaledImageRepresentation(size: CGSize(width: 180.0, height: 180.0), mode: .aspectFit), complete: false) let signal = maybeFullSize |> take(1) |> mapToSignal { maybeData -> Signal<(Data?, Data?, Bool), NoError> in if maybeData.complete { @@ -694,7 +786,7 @@ func chatMessagePhotoThumbnail(account: Account, photo: TelegramMediaImage) -> S } func chatMessageVideoThumbnail(account: Account, file: TelegramMediaFile) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { - let signal = chatMessageVideoDatas(account: account, file: file, thumbnailSize: true) + let signal = chatMessageVideoDatas(postbox: account.postbox, file: file, thumbnailSize: true) return signal |> map { (thumbnailData, fullSizeData, fullSizeComplete) in return { arguments in @@ -778,7 +870,7 @@ func chatMessageVideoThumbnail(account: Account, file: TelegramMediaFile) -> Sig } func chatSecretPhoto(account: Account, photo: TelegramMediaImage) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { - let signal = chatMessagePhotoDatas(account: account, photo: photo) + let signal = chatMessagePhotoDatas(postbox: account.postbox, photo: photo) return signal |> map { (thumbnailData, fullSizeData, fullSizeComplete) in return { arguments in @@ -889,7 +981,7 @@ func chatSecretPhoto(account: Account, photo: TelegramMediaImage) -> Signal<(Tra } func mediaGridMessagePhoto(account: Account, photo: TelegramMediaImage) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { - let signal = chatMessagePhotoDatas(account: account, photo: photo, fullRepresentationSize: CGSize(width: 127.0, height: 127.0), autoFetchFullSize: true) + let signal = chatMessagePhotoDatas(postbox: account.postbox, photo: photo, fullRepresentationSize: CGSize(width: 127.0, height: 127.0), autoFetchFullSize: true) return signal |> map { (thumbnailData, fullSizeData, fullSizeComplete) in return { arguments in @@ -1035,8 +1127,8 @@ func gifPaneVideoThumbnail(account: Account, video: TelegramMediaFile) -> Signal } } -func mediaGridMessageVideo(account: Account, video: TelegramMediaFile) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { - let signal = chatMessageVideoDatas(account: account, file: video) +func mediaGridMessageVideo(postbox: Postbox, video: TelegramMediaFile) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { + let signal = chatMessageVideoDatas(postbox: postbox, file: video) return signal |> map { (thumbnailData, fullSizeData, fullSizeComplete) in return { arguments in @@ -1044,7 +1136,13 @@ func mediaGridMessageVideo(account: Account, video: TelegramMediaFile) -> Signal let context = DrawingContext(size: arguments.drawingSize, clear: true) let drawingRect = arguments.drawingRect - let fittedSize = arguments.imageSize.aspectFilled(arguments.boundingSize).fitted(arguments.imageSize) + var fittedSize = arguments.imageSize.aspectFilled(arguments.boundingSize).fitted(arguments.imageSize) + if fittedSize.width < drawingRect.size.width && fittedSize.width >= drawingRect.size.width - 2.0 { + fittedSize.width = drawingRect.size.width + } + if fittedSize.height < drawingRect.size.height && fittedSize.height >= drawingRect.size.height - 2.0 { + fittedSize.height = drawingRect.size.height + } let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) var fullSizeImage: CGImage? @@ -1106,7 +1204,7 @@ func mediaGridMessageVideo(account: Account, video: TelegramMediaFile) -> Signal if let blurredImage = thumbnailContext.generateImage() { let filledSize = thumbnailSize.aspectFilled(arguments.drawingRect.size) c.interpolationQuality = .medium - c.draw(blurredImage.cgImage!, in: CGRect(origin: CGPoint(x: (arguments.drawingRect.width - filledSize.width) / 2.0, y: (arguments.drawingRect.height - filledSize.height) / 2.0), size: filledSize)) + c.draw(blurredImage.cgImage!, in: CGRect(origin: CGPoint(x: arguments.drawingRect.minX + (arguments.drawingRect.width - filledSize.width) / 2.0, y: arguments.drawingRect.minY + (arguments.drawingRect.height - filledSize.height) / 2.0), size: filledSize)) c.setBlendMode(.normal) c.setFillColor(UIColor(white: 1.0, alpha: 0.5).cgColor) c.fill(arguments.drawingRect) @@ -1231,8 +1329,8 @@ func chatWebpageSnippetPhoto(account: Account, photo: TelegramMediaImage) -> Sig } } -func chatMessageVideo(account: Account, video: TelegramMediaFile) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { - return mediaGridMessageVideo(account: account, video: video) +func chatMessageVideo(postbox: Postbox, video: TelegramMediaFile) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { + return mediaGridMessageVideo(postbox: postbox, video: video) } private func chatSecretMessageVideoData(account: Account, file: TelegramMediaFile) -> Signal { @@ -1362,8 +1460,13 @@ func chatSecretMessageVideo(account: Account, video: TelegramMediaFile) -> Signa } } -func chatMessageImageFile(account: Account, file: TelegramMediaFile, progressive: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { - let signal = chatMessageFileDatas(account: account, file: file, progressive: progressive) +func chatMessageImageFile(account: Account, file: TelegramMediaFile, thumbnail: Bool) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { + let signal: Signal<(Data?, String?, Bool), NoError> + if thumbnail { + signal = chatMessageImageFileThumbnailDatas(account: account, file: file) + } else { + signal = chatMessageFileDatas(account: account, file: file, progressive: false) + } return signal |> map { (thumbnailData, fullSizePath, fullSizeComplete) in return { arguments in @@ -1371,8 +1474,12 @@ func chatMessageImageFile(account: Account, file: TelegramMediaFile, progressive let context = DrawingContext(size: arguments.drawingSize, clear: true) let drawingRect = arguments.drawingRect - let fittedSize = arguments.imageSize.aspectFilled(arguments.boundingSize).fitted(arguments.imageSize) - let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) + var fittedSize: CGSize + if thumbnail { + fittedSize = arguments.imageSize.aspectFilled(arguments.boundingSize) + } else { + fittedSize = arguments.imageSize.aspectFilled(arguments.boundingSize).fitted(arguments.imageSize) + } var fullSizeImage: CGImage? if let fullSizePath = fullSizePath { @@ -1382,17 +1489,23 @@ func chatMessageImageFile(account: Account, file: TelegramMediaFile, progressive options.setValue(true as NSNumber, forKey: kCGImageSourceCreateThumbnailFromImageAlways as String) if let imageSource = CGImageSourceCreateWithURL(URL(fileURLWithPath: fullSizePath) as CFURL, nil), let image = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options) { fullSizeImage = image + if thumbnail { + fittedSize = CGSize(width: CGFloat(image.width), height: CGFloat(image.height)).aspectFilled(arguments.boundingSize) + } } - } else if progressive { - assertionFailure() } } var thumbnailImage: CGImage? if let thumbnailData = thumbnailData, let imageSource = CGImageSourceCreateWithData(thumbnailData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) { thumbnailImage = image + if thumbnail { + fittedSize = CGSize(width: CGFloat(image.width), height: CGFloat(image.height)).aspectFilled(arguments.boundingSize) + } } + let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) + var blurredThumbnailImage: UIImage? if let thumbnailImage = thumbnailImage { let thumbnailSize = CGSize(width: thumbnailImage.width, height: thumbnailImage.height) @@ -1409,8 +1522,8 @@ func chatMessageImageFile(account: Account, file: TelegramMediaFile, progressive context.withFlippedContext { c in c.setBlendMode(.copy) - if arguments.boundingSize != arguments.imageSize { - c.fill(arguments.drawingRect) + if arguments.boundingSize != fittedSize { + c.fill(drawingRect) } c.setBlendMode(.copy) @@ -1757,3 +1870,214 @@ func chatWebFileImage(account: Account, file: TelegramMediaWebFile) -> Signal<(T } } } + +private let precomposedSmallAlbumArt = Atomic(value: nil) + +private func albumArtThumbnailData(postbox: Postbox, thumbnail: MediaResource) -> Signal<(Data?), NoError> { + let thumbnailResource = postbox.mediaBox.resourceData(thumbnail) + + let signal = thumbnailResource |> take(1) |> mapToSignal { maybeData -> Signal<(Data?), NoError> in + if maybeData.complete { + let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []) + return .single((loadedData)) + } else { + let fetchedThumbnail = postbox.mediaBox.fetchedResource(thumbnail, tag: TelegramMediaResourceFetchTag(statsCategory: .image)) + + let thumbnail = Signal { subscriber in + let fetchedDisposable = fetchedThumbnail.start() + let thumbnailDisposable = thumbnailResource.start(next: { next in + subscriber.putNext(next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: [])) + }, error: subscriber.putError, completed: subscriber.putCompletion) + + return ActionDisposable { + fetchedDisposable.dispose() + thumbnailDisposable.dispose() + } + } + + return thumbnail + } + } |> distinctUntilChanged(isEqual: { lhs, rhs in + if lhs == nil && rhs == nil { + return true + } else { + return false + } + }) + + return signal +} + +private func albumArtFullSizeDatas(postbox: Postbox, thumbnail: MediaResource, fullSize: MediaResource, autoFetchFullSize: Bool = true) -> Signal<(Data?, Data?, Bool), NoError> { + let fullSizeResource = postbox.mediaBox.resourceData(fullSize) + let thumbnailResource = postbox.mediaBox.resourceData(thumbnail) + + let signal = fullSizeResource |> take(1) |> mapToSignal { maybeData -> Signal<(Data?, Data?, Bool), NoError> in + if maybeData.complete { + let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []) + return .single((nil, loadedData, true)) + } else { + let fetchedThumbnail = postbox.mediaBox.fetchedResource(thumbnail, tag: TelegramMediaResourceFetchTag(statsCategory: .image)) + let fetchedFullSize = postbox.mediaBox.fetchedResource(fullSize, tag: TelegramMediaResourceFetchTag(statsCategory: .image)) + + let thumbnail = Signal { subscriber in + let fetchedDisposable = fetchedThumbnail.start() + let thumbnailDisposable = thumbnailResource.start(next: { next in + subscriber.putNext(next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: [])) + }, error: subscriber.putError, completed: subscriber.putCompletion) + + return ActionDisposable { + fetchedDisposable.dispose() + thumbnailDisposable.dispose() + } + } + + let fullSizeData: Signal<(Data?, Bool), NoError> + + if autoFetchFullSize { + fullSizeData = Signal<(Data?, Bool), NoError> { subscriber in + let fetchedFullSizeDisposable = fetchedFullSize.start() + let fullSizeDisposable = fullSizeResource.start(next: { next in + subscriber.putNext((next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []), next.complete)) + }, error: subscriber.putError, completed: subscriber.putCompletion) + + return ActionDisposable { + fetchedFullSizeDisposable.dispose() + fullSizeDisposable.dispose() + } + } + } else { + fullSizeData = fullSizeResource + |> map { next -> (Data?, Bool) in + return (next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []), next.complete) + } + } + + + return thumbnail |> mapToSignal { thumbnailData in + return fullSizeData |> map { (fullSizeData, complete) in + return (thumbnailData, fullSizeData, complete) + } + } + } + } |> distinctUntilChanged(isEqual: { lhs, rhs in + if (lhs.0 == nil && lhs.1 == nil) && (rhs.0 == nil && rhs.1 == nil) { + return true + } else { + return false + } + }) + + return signal +} + +private func drawAlbumArtPlaceholder(into c: CGContext, arguments: TransformImageArguments, thumbnail: Bool) { + c.setBlendMode(.copy) + c.setFillColor(UIColor(rgb: 0xeeeeee).cgColor) + c.fill(arguments.drawingRect) + + c.setBlendMode(.normal) + + if thumbnail { + var image: UIImage? + let precomposed = precomposedSmallAlbumArt.with { $0 } + if let precomposed = precomposed { + image = precomposed + } else { + if let sourceImage = UIImage(bundleImageName: "GlobalMusicPlayer/AlbumArtPlaceholder"), let cgImage = sourceImage.cgImage { + + let fittedSize = sourceImage.size.aspectFitted(CGSize(width: 28.0, height: 28.0)) + + image = generateImage(fittedSize, contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.draw(cgImage, in: CGRect(origin: CGPoint(), size: size)) + }) + + if let image = image { + let _ = precomposedSmallAlbumArt.swap(image) + } + } + } + if let image = image, let cgImage = image.cgImage { + c.draw(cgImage, in: CGRect(origin: CGPoint(x: floor(arguments.drawingRect.size.width - image.size.width) / 2.0, y: floor(arguments.drawingRect.size.height - image.size.height) / 2.0), size: image.size)) + } + } else { + if let sourceImage = UIImage(bundleImageName: "GlobalMusicPlayer/AlbumArtPlaceholder"), let cgImage = sourceImage.cgImage { + let fittedSize = sourceImage.size.aspectFitted(CGSize(width: floor(arguments.drawingRect.size.width * 0.66), height: floor(arguments.drawingRect.size.width * 0.66))) + + c.draw(cgImage, in: CGRect(origin: CGPoint(x: floor(arguments.drawingRect.size.width - fittedSize.width) / 2.0, y: floor(arguments.drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize)) + } + } +} + +func playerAlbumArt(postbox: Postbox, albumArt: SharedMediaPlaybackAlbumArt?, thumbnail: Bool) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { + if let albumArt = albumArt { + if thumbnail { + return albumArtThumbnailData(postbox: postbox, thumbnail: albumArt.thumbnailResource) |> map { thumbnailData in + return { arguments in + let context = DrawingContext(size: arguments.drawingSize, clear: true) + + var sourceImage: UIImage? + if let thumbnailData = thumbnailData, let image = UIImage(data: thumbnailData) { + sourceImage = image + } + + if let sourceImage = sourceImage, let cgImage = sourceImage.cgImage { + let imageSize = sourceImage.size.aspectFilled(arguments.drawingRect.size) + context.withFlippedContext { c in + c.draw(cgImage, in: CGRect(origin: CGPoint(x: floor((arguments.drawingRect.size.width - imageSize.width) / 2.0), y: floor((arguments.drawingRect.size.height - imageSize.height) / 2.0)), size: imageSize)) + } + } else { + context.withFlippedContext { c in + drawAlbumArtPlaceholder(into: c, arguments: arguments, thumbnail: thumbnail) + } + } + + addCorners(context, arguments: arguments) + + return context + } + } + } else { + return albumArtFullSizeDatas(postbox: postbox, thumbnail: albumArt.thumbnailResource, fullSize: albumArt.fullSizeResource) |> map { (thumbnailData, fullSizeData, fullSizeComplete) in + return { arguments in + let context = DrawingContext(size: arguments.drawingSize, clear: true) + + var sourceImage: UIImage? + if fullSizeComplete, let fullSizeData = fullSizeData, let image = UIImage(data: fullSizeData) { + sourceImage = image + } else if let thumbnailData = thumbnailData, let image = UIImage(data: thumbnailData) { + sourceImage = image + } + + if let sourceImage = sourceImage, let cgImage = sourceImage.cgImage { + let imageSize = sourceImage.size.aspectFilled(arguments.drawingRect.size) + context.withFlippedContext { c in + c.draw(cgImage, in: CGRect(origin: CGPoint(x: floor((arguments.drawingRect.size.width - imageSize.width) / 2.0), y: floor((arguments.drawingRect.size.height - imageSize.height) / 2.0)), size: imageSize)) + } + } else { + context.withFlippedContext { c in + drawAlbumArtPlaceholder(into: c, arguments: arguments, thumbnail: thumbnail) + } + } + + addCorners(context, arguments: arguments) + + return context + } + } + } + } else { + return .single({ arguments in + let context = DrawingContext(size: arguments.drawingSize, clear: true) + + context.withFlippedContext { c in + drawAlbumArtPlaceholder(into: c, arguments: arguments, thumbnail: thumbnail) + } + + addCorners(context, arguments: arguments) + + return context + }) + } +} diff --git a/TelegramUI/PictureInPictureVideoControlsNode.swift b/TelegramUI/PictureInPictureVideoControlsNode.swift index 237cbd40c5..b4891f0bfc 100644 --- a/TelegramUI/PictureInPictureVideoControlsNode.swift +++ b/TelegramUI/PictureInPictureVideoControlsNode.swift @@ -35,7 +35,7 @@ final class PictureInPictureVideoControlsNode: ASDisplayNode { case .playing: self.playButton.isHidden = true self.pauseButton.isHidden = false - case let .buffering(whilePlaying): + case let .buffering(_, whilePlaying): if whilePlaying { self.playButton.isHidden = true self.pauseButton.isHidden = false diff --git a/TelegramUI/PreferencesKeys.swift b/TelegramUI/PreferencesKeys.swift index 9f0eb1a5a7..8f8ec6d5ea 100644 --- a/TelegramUI/PreferencesKeys.swift +++ b/TelegramUI/PreferencesKeys.swift @@ -10,6 +10,9 @@ private enum ApplicationSpecificPreferencesKeyValues: Int32 { case voiceCallSettings = 4 case presentationThemeSettings = 5 case instantPagePresentationSettings = 6 + case callListSettings = 7 + case experimentalSettings = 8 + case musicPlaybackSettings = 9 } public struct ApplicationSpecificPreferencesKeys { @@ -20,4 +23,7 @@ public struct ApplicationSpecificPreferencesKeys { public static let voiceCallSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.voiceCallSettings.rawValue) public static let presentationThemeSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.presentationThemeSettings.rawValue) public static let instantPagePresentationSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.instantPagePresentationSettings.rawValue) + public static let callListSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.callListSettings.rawValue) + public static let experimentalSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.experimentalSettings.rawValue) + public static let musicPlaybackSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.musicPlaybackSettings.rawValue) } diff --git a/TelegramUI/PreparedChatHistoryViewTransition.swift b/TelegramUI/PreparedChatHistoryViewTransition.swift index 1d226ced2c..7a601e2e5b 100644 --- a/TelegramUI/PreparedChatHistoryViewTransition.swift +++ b/TelegramUI/PreparedChatHistoryViewTransition.swift @@ -4,18 +4,23 @@ import Postbox import TelegramCore import Display -func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toView: ChatHistoryView, reason: ChatHistoryViewTransitionReason, account: Account, peerId: PeerId, controllerInteraction: ChatControllerInteraction, scrollPosition: ChatHistoryViewScrollPosition?, initialData: InitialMessageHistoryData?, keyboardButtonsMessage: Message?, cachedData: CachedPeerData?, cachedDataMessages: [MessageId: Message]?, readStateData: ChatHistoryCombinedInitialReadStateData?) -> Signal { +func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toView: ChatHistoryView, reason: ChatHistoryViewTransitionReason, reverse: Bool, account: Account, chatLocation: ChatLocation, controllerInteraction: ChatControllerInteraction, scrollPosition: ChatHistoryViewScrollPosition?, initialData: InitialMessageHistoryData?, keyboardButtonsMessage: Message?, cachedData: CachedPeerData?, cachedDataMessages: [MessageId: Message]?, readStateData: [PeerId: ChatHistoryCombinedInitialReadStateData]?) -> Signal { return Signal { subscriber in - let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromView?.filteredEntries ?? [], rightList: toView.filteredEntries) + let mergeResult: (deleteIndices: [Int], indicesAndItems: [(Int, ChatHistoryEntry, Int?)], updateIndices: [(Int, ChatHistoryEntry, Int)]) + if reverse { + mergeResult = mergeListsStableWithUpdatesReversed(leftList: fromView?.filteredEntries ?? [], rightList: toView.filteredEntries) + } else { + mergeResult = mergeListsStableWithUpdates(leftList: fromView?.filteredEntries ?? [], rightList: toView.filteredEntries) + } var adjustedDeleteIndices: [ListViewDeleteItem] = [] let previousCount: Int if let fromView = fromView { previousCount = fromView.filteredEntries.count } else { - previousCount = 0; + previousCount = 0 } - for index in deleteIndices { + for index in mergeResult.deleteIndices { adjustedDeleteIndices.append(ListViewDeleteItem(index: previousCount - 1 - index, directionHint: nil)) } @@ -41,14 +46,14 @@ func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toVie let _ = options.insert(.AnimateAlpha) let _ = options.insert(.AnimateInsertion) - for (index, _, _) in indicesAndItems.sorted(by: { $0.0 > $1.0 }) { + for (index, _, _) in mergeResult.indicesAndItems.sorted(by: { $0.0 > $1.0 }) { let adjustedIndex = updatedCount - 1 - index if adjustedIndex == maxAnimatedInsertionIndex + 1 { maxAnimatedInsertionIndex += 1 } } case .Reload: - break + stationaryItemRange = (0, Int.max) case let .HoleChanges(filledHoleDirections, removeHoleDirections): if let (_, removeDirection) = removeHoleDirections.first { switch removeDirection { @@ -71,13 +76,13 @@ func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toVie } case .UpperToLower: break - case .AroundIndex: + case .AroundId, .AroundIndex: break } } } - for (index, entry, previousIndex) in indicesAndItems { + for (index, entry, previousIndex) in mergeResult.indicesAndItems { let adjustedIndex = updatedCount - 1 - index let adjustedPrevousIndex: Int? @@ -95,7 +100,7 @@ func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toVie adjustedIndicesAndItems.append(ChatHistoryViewTransitionInsertEntry(index: adjustedIndex, previousIndex: adjustedPrevousIndex, entry: entry, directionHint: directionHint)) } - for (index, entry, previousIndex) in updateIndices { + for (index, entry, previousIndex) in mergeResult.updateIndices { let adjustedIndex = updatedCount - 1 - index let adjustedPreviousIndex = previousCount - 1 - previousIndex @@ -103,7 +108,7 @@ func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toVie adjustedUpdateItems.append(ChatHistoryViewTransitionUpdateEntry(index: adjustedIndex, previousIndex: adjustedPreviousIndex, entry: entry, directionHint: directionHint)) } - var scrolledToIndex: MessageIndex? + var scrolledToIndex: MessageHistoryAnchorIndex? if let scrollPosition = scrollPosition { switch scrollPosition { @@ -164,7 +169,7 @@ func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toVie } var index = toView.filteredEntries.count - 1 for entry in toView.filteredEntries { - if entry.index >= scrollIndex { + if scrollIndex.isLessOrEqual(to: entry.index) { scrollToItem = ListViewScrollToItem(index: index, position: position, animated: animated, curve: .Default, directionHint: directionHint) break } @@ -174,7 +179,7 @@ func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toVie if scrollToItem == nil { var index = 0 for entry in toView.filteredEntries.reversed() { - if entry.index < scrollIndex { + if !scrollIndex.isLess(than: entry.index) { scrollToItem = ListViewScrollToItem(index: index, position: position, animated: animated, curve: .Default, directionHint: directionHint) break } diff --git a/TelegramUI/PresenceStrings.swift b/TelegramUI/PresenceStrings.swift index 66e9e696d4..640e5b1d72 100644 --- a/TelegramUI/PresenceStrings.swift +++ b/TelegramUI/PresenceStrings.swift @@ -66,38 +66,34 @@ func stringForMonth(strings: PresentationStrings, month: Int32, ofYear year: Int return stringForMonth(strings: strings, month: month) + " \(1900 + year)" } -func stringForTime(hours: Int32, minutes: Int32) -> String { - return String(format: "%d:%02d", hours, minutes) -} - enum RelativeTimestampFormatDay { case today case yesterday } -func stringForUserPresence(strings: PresentationStrings, day: RelativeTimestampFormatDay, hours: Int32, minutes: Int32) -> String { +func stringForUserPresence(strings: PresentationStrings, day: RelativeTimestampFormatDay, timeFormat: PresentationTimeFormat, hours: Int32, minutes: Int32) -> String { let dayString: String switch day { case .today: - dayString = strings.LastSeen_AtDate(strings.Time_TodayAt(stringForTime(hours: hours, minutes: minutes)).0).0 + dayString = strings.LastSeen_AtDate(strings.Time_TodayAt(stringForShortTimestamp(hours: hours, minutes: minutes, timeFormat: timeFormat)).0).0 case .yesterday: - dayString = strings.LastSeen_AtDate(strings.Time_YesterdayAt(stringForTime(hours: hours, minutes: minutes)).0).0 + dayString = strings.LastSeen_AtDate(strings.Time_YesterdayAt(stringForShortTimestamp(hours: hours, minutes: minutes, timeFormat: timeFormat)).0).0 } return dayString } -private func humanReadableStringForTimestamp(strings: PresentationStrings, day: RelativeTimestampFormatDay, hours: Int32, minutes: Int32) -> String { +private func humanReadableStringForTimestamp(strings: PresentationStrings, day: RelativeTimestampFormatDay, timeFormat: PresentationTimeFormat, hours: Int32, minutes: Int32) -> String { let dayString: String switch day { case .today: - dayString = strings.Time_TodayAt(stringForTime(hours: hours, minutes: minutes)).0 + dayString = strings.Time_TodayAt(stringForShortTimestamp(hours: hours, minutes: minutes, timeFormat: timeFormat)).0 case .yesterday: - dayString = strings.Time_YesterdayAt(stringForTime(hours: hours, minutes: minutes)).0 + dayString = strings.Time_YesterdayAt(stringForShortTimestamp(hours: hours, minutes: minutes, timeFormat: timeFormat)).0 } return dayString } -func humanReadableStringForTimestamp(strings: PresentationStrings, timestamp: Int32) -> String { +func humanReadableStringForTimestamp(strings: PresentationStrings, timeFormat: PresentationTimeFormat, timestamp: Int32) -> String { var t: time_t = time_t(timestamp) var timeinfo: tm = tm() localtime_r(&t, &timeinfo) @@ -119,7 +115,7 @@ func humanReadableStringForTimestamp(strings: PresentationStrings, timestamp: In } else { day = .yesterday } - return humanReadableStringForTimestamp(strings: strings, day: day, hours: timeinfo.tm_hour, minutes: timeinfo.tm_min) + return humanReadableStringForTimestamp(strings: strings, day: day, timeFormat: timeFormat, hours: timeinfo.tm_hour, minutes: timeinfo.tm_min) } else { return "\(stringForTimestamp(day: timeinfo.tm_mday, month: timeinfo.tm_mon + 1, year: timeinfo.tm_year))" } @@ -163,7 +159,7 @@ func relativeUserPresenceStatus(_ presence: TelegramUserPresence, relativeTo tim } } -func stringForRelativeTimestamp(strings: PresentationStrings, relativeTimestamp: Int32, relativeTo timestamp: Int32) -> String { +func stringForRelativeTimestamp(strings: PresentationStrings, relativeTimestamp: Int32, relativeTo timestamp: Int32, timeFormat: PresentationTimeFormat) -> String { var t: time_t = time_t(relativeTimestamp) var timeinfo: tm = tm() localtime_r(&t, &timeinfo) @@ -179,7 +175,7 @@ func stringForRelativeTimestamp(strings: PresentationStrings, relativeTimestamp: let dayDifference = timeinfo.tm_yday - timeinfoNow.tm_yday if dayDifference > -7 { if dayDifference == 0 { - return stringForTime(hours: timeinfo.tm_hour, minutes: timeinfo.tm_min) + return stringForShortTimestamp(hours: timeinfo.tm_hour, minutes: timeinfo.tm_min, timeFormat: timeFormat) } else { return shortStringForDayOfWeek(strings: strings, day: timeinfo.tm_wday) } @@ -188,10 +184,64 @@ func stringForRelativeTimestamp(strings: PresentationStrings, relativeTimestamp: } } -func stringAndActivityForUserPresence(strings: PresentationStrings, presence: TelegramUserPresence, relativeTo timestamp: Int32) -> (String, Bool) { +func stringForRelativeLiveLocationTimestamp(strings: PresentationStrings, relativeTimestamp: Int32, relativeTo timestamp: Int32, timeFormat: PresentationTimeFormat) -> String { + let difference = timestamp - relativeTimestamp + if difference < 60 { + return strings.LiveLocationUpdated_JustNow + } else if difference < 60 * 60 { + let minutes = difference / 60 + return strings.LiveLocationUpdated_MinutesAgo(minutes) + } else { + var t: time_t = time_t(relativeTimestamp) + var timeinfo: tm = tm() + localtime_r(&t, &timeinfo) + + var now: time_t = time_t(timestamp) + var timeinfoNow: tm = tm() + localtime_r(&now, &timeinfoNow) + + let dayDifference = timeinfo.tm_yday - timeinfoNow.tm_yday + + let hours = timeinfo.tm_hour + let minutes = timeinfo.tm_min + + if dayDifference == 0 { + return strings.LiveLocationUpdated_TodayAt(stringForShortTimestamp(hours: hours, minutes: minutes, timeFormat: timeFormat)).0 + } else { + return stringForFullDate(timestamp: relativeTimestamp, strings: strings, timeFormat: timeFormat) + } + } +} + +func stringForRelativeLiveLocationUpdateTimestamp(strings: PresentationStrings, relativeTimestamp: Int32, relativeTo timestamp: Int32, timeFormat: PresentationTimeFormat) -> String { + var t: time_t = time_t(relativeTimestamp) + var timeinfo: tm = tm() + localtime_r(&t, &timeinfo) + + var now: time_t = time_t(timestamp) + var timeinfoNow: tm = tm() + localtime_r(&now, &timeinfoNow) + + if timeinfo.tm_year != timeinfoNow.tm_year { + return stringForTimestamp(day: timeinfo.tm_mday, month: timeinfo.tm_mon + 1, year: timeinfo.tm_year) + } + + let dayDifference = timeinfo.tm_yday - timeinfoNow.tm_yday + if dayDifference > -7 { + if dayDifference == 0 { + return stringForShortTimestamp(hours: timeinfo.tm_hour, minutes: timeinfo.tm_min, timeFormat: timeFormat) + } else { + return shortStringForDayOfWeek(strings: strings, day: timeinfo.tm_wday) + } + } else { + return stringForTimestamp(day: timeinfo.tm_mday, month: timeinfo.tm_mon + 1) + } +} + +func stringAndActivityForUserPresence(strings: PresentationStrings, timeFormat: PresentationTimeFormat, presence: TelegramUserPresence, relativeTo timestamp: Int32) -> (String, Bool) { switch presence.status { case .none: - return (strings.Presence_offline, false) + return (strings.LastSeen_ALongTimeAgo, false) case let .present(statusTimestamp): if statusTimestamp >= timestamp { return (strings.Presence_online, true) @@ -223,7 +273,7 @@ func stringAndActivityForUserPresence(strings: PresentationStrings, presence: Te } else { day = .yesterday } - return (stringForUserPresence(strings: strings, day: day, hours: timeinfo.tm_hour, minutes: timeinfo.tm_min), false) + return (stringForUserPresence(strings: strings, day: day, timeFormat: timeFormat, hours: timeinfo.tm_hour, minutes: timeinfo.tm_min), false) } else { return (strings.LastSeen_AtDate(stringForTimestamp(day: timeinfo.tm_mday, month: timeinfo.tm_mon + 1, year: timeinfo.tm_year)).0, false) } diff --git a/TelegramUI/PresentationCall.swift b/TelegramUI/PresentationCall.swift index 7a6d4e98f6..5cec699bbb 100644 --- a/TelegramUI/PresentationCall.swift +++ b/TelegramUI/PresentationCall.swift @@ -178,7 +178,7 @@ public final class PresentationCall { } if let audioSessionControl = audioSessionControl, previous == nil || previousControl == nil { - audioSessionControl.setSpeaker(self.speakerModeValue) + audioSessionControl.setOutputMode(self.speakerModeValue ? .custom(.speaker) : .system) audioSessionControl.setup(synchronous: true) } @@ -222,14 +222,14 @@ public final class PresentationCall { if let callKitIntegration = self.callKitIntegration { audioSessionActive = callKitIntegration.audioSessionActive |> filter { $0 } |> timeout(2.0, queue: Queue.mainQueue(), alternate: Signal { [weak self] subscriber in if let strongSelf = self, let audioSessionControl = strongSelf.audioSessionControl { - audioSessionControl.activate() + audioSessionControl.activate({ _ in }) } subscriber.putNext(true) subscriber.putCompletion() return EmptyDisposable }) } else { - audioSessionControl.activate() + audioSessionControl.activate({ _ in }) audioSessionActive = .single(true) } @@ -281,7 +281,7 @@ public final class PresentationCall { self.speakerModeValue = !self.speakerModeValue self.speakerModePromise.set(self.speakerModeValue) if let audioSessionControl = self.audioSessionControl { - audioSessionControl.setSpeaker(self.speakerModeValue) + audioSessionControl.setOutputMode(self.speakerModeValue ? .speakerIfNoHeadphones : .system) } } } diff --git a/TelegramUI/PresentationData.swift b/TelegramUI/PresentationData.swift index a8f1965777..85c8073953 100644 --- a/TelegramUI/PresentationData.swift +++ b/TelegramUI/PresentationData.swift @@ -3,23 +3,32 @@ import SwiftSignalKit import Postbox import TelegramCore +public enum PresentationTimeFormat { + case regular + case military +} + public final class PresentationData: Equatable { public let strings: PresentationStrings public let theme: PresentationTheme public let chatWallpaper: TelegramWallpaper + public let fontSize: PresentationFontSize + public let timeFormat: PresentationTimeFormat - public init(strings: PresentationStrings, theme: PresentationTheme, chatWallpaper: TelegramWallpaper) { + public init(strings: PresentationStrings, theme: PresentationTheme, chatWallpaper: TelegramWallpaper, fontSize: PresentationFontSize, timeFormat: PresentationTimeFormat) { self.strings = strings self.theme = theme self.chatWallpaper = chatWallpaper + self.fontSize = fontSize + self.timeFormat = timeFormat } public static func ==(lhs: PresentationData, rhs: PresentationData) -> Bool { - return lhs.strings === rhs.strings && lhs.theme == rhs.theme && lhs.chatWallpaper == rhs.chatWallpaper + return lhs.strings === rhs.strings && lhs.theme === rhs.theme && lhs.chatWallpaper == rhs.chatWallpaper && lhs.fontSize == rhs.fontSize && lhs.timeFormat == rhs.timeFormat } } -private func dictFromLocalization(_ value: Localization) -> [String: String] { +func dictFromLocalization(_ value: Localization) -> [String: String] { var dict: [String: String] = [:] for entry in value.entries { switch entry { @@ -47,8 +56,23 @@ private func dictFromLocalization(_ value: Localization) -> [String: String] { return dict } -public func currentPresentationDataAndSettings(postbox: Postbox) -> Signal<(PresentationData, AutomaticMediaDownloadSettings), NoError> { - return postbox.modify { modifier -> (PresentationThemeSettings, LocalizationSettings?, AutomaticMediaDownloadSettings) in +private func currentTimeFormat() -> PresentationTimeFormat { + let dateFormatter = DateFormatter() + dateFormatter.locale = Locale.current + dateFormatter.dateStyle = .none + dateFormatter.timeStyle = .medium + dateFormatter.timeZone = TimeZone.current + let dateString = dateFormatter.string(from: Date()) + + if dateString.contains(dateFormatter.amSymbol) || dateString.contains(dateFormatter.pmSymbol) { + return .regular + } else { + return .military + } +} + +public func currentPresentationDataAndSettings(postbox: Postbox) -> Signal<(PresentationData, AutomaticMediaDownloadSettings, LoggingSettings, CallListSettings), NoError> { + return postbox.modify { modifier -> (PresentationThemeSettings, LocalizationSettings?, AutomaticMediaDownloadSettings, LoggingSettings, CallListSettings) in let themeSettings: PresentationThemeSettings if let current = modifier.getPreferencesEntry(key: ApplicationSpecificPreferencesKeys.presentationThemeSettings) as? PresentationThemeSettings { themeSettings = current @@ -70,16 +94,34 @@ public func currentPresentationDataAndSettings(postbox: Postbox) -> Signal<(Pres automaticMediaDownloadSettings = AutomaticMediaDownloadSettings.defaultSettings } - return (themeSettings, localizationSettings, automaticMediaDownloadSettings) - } |> map { (themeSettings, localizationSettings, automaticMediaDownloadSettings) -> (PresentationData, AutomaticMediaDownloadSettings) in + let loggingSettings: LoggingSettings + if let value = modifier.getPreferencesEntry(key: PreferencesKeys.loggingSettings) as? LoggingSettings { + loggingSettings = value + } else { + loggingSettings = LoggingSettings.defaultSettings + } + + let callListSettings: CallListSettings + if let value = modifier.getPreferencesEntry(key: ApplicationSpecificPreferencesKeys.callListSettings) as? CallListSettings { + callListSettings = value + } else { + callListSettings = CallListSettings.defaultSettings + } + + return (themeSettings, localizationSettings, automaticMediaDownloadSettings, loggingSettings, callListSettings) + } |> map { (themeSettings, localizationSettings, automaticMediaDownloadSettings, loggingSettings, callListSettings) -> (PresentationData, AutomaticMediaDownloadSettings, LoggingSettings, CallListSettings) in let themeValue: PresentationTheme switch themeSettings.theme { case let .builtin(reference): switch reference { - case .light: + case .dayClassic: themeValue = defaultPresentationTheme - case .dark: + case .nightGrayscale: themeValue = defaultDarkPresentationTheme + case .nightAccent: + themeValue = defaultDarkAccentPresentationTheme + case .day: + themeValue = defaultDayPresentationTheme } } let stringsValue: PresentationStrings @@ -88,7 +130,8 @@ public func currentPresentationDataAndSettings(postbox: Postbox) -> Signal<(Pres } else { stringsValue = defaultPresentationStrings } - return (PresentationData(strings: stringsValue, theme: themeValue, chatWallpaper: themeSettings.chatWallpaper), automaticMediaDownloadSettings) + let timeFormat: PresentationTimeFormat = currentTimeFormat() + return (PresentationData(strings: stringsValue, theme: themeValue, chatWallpaper: themeSettings.chatWallpaper, fontSize: themeSettings.fontSize, timeFormat: timeFormat), automaticMediaDownloadSettings, loggingSettings, callListSettings) } } @@ -108,10 +151,14 @@ public func updatedPresentationData(postbox: Postbox) -> Signal Signal PresentationPasscodeSettings) -> Signal { return postbox.modify { modifier -> Void in - modifier.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.presentationPasscodeSettings, { entry in - let currentSettings: PresentationPasscodeSettings - if let entry = entry as? PresentationPasscodeSettings { - currentSettings = entry - } else { - currentSettings = PresentationPasscodeSettings.defaultSettings - } - return f(currentSettings) - }) + updatePresentationPasscodeSettingsInternal(modifier: modifier, f) } } + +func updatePresentationPasscodeSettingsInternal(modifier: Modifier, _ f: @escaping (PresentationPasscodeSettings) -> PresentationPasscodeSettings) { + modifier.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.presentationPasscodeSettings, { entry in + let currentSettings: PresentationPasscodeSettings + if let entry = entry as? PresentationPasscodeSettings { + currentSettings = entry + } else { + currentSettings = PresentationPasscodeSettings.defaultSettings + } + return f(currentSettings) + }) +} diff --git a/TelegramUI/PresentationResourceKey.swift b/TelegramUI/PresentationResourceKey.swift index af4f8fef7a..ce991cd53b 100644 --- a/TelegramUI/PresentationResourceKey.swift +++ b/TelegramUI/PresentationResourceKey.swift @@ -16,6 +16,7 @@ enum PresentationResourceKey: Int32 { case navigationComposeIcon case navigationCallIcon case navigationShareIcon + case navigationSearchIcon case navigationPlayerCloseButton case navigationPlayerPlayIcon @@ -32,6 +33,8 @@ enum PresentationResourceKey: Int32 { case itemListCheckIcon case itemListSecondaryCheckIcon case itemListPlusIcon + case itemListDeleteIndicatorIcon + case itemListAddPersonIcon case itemListStickerItemUnreadDot case itemListVerifiedPeerIcon @@ -40,6 +43,7 @@ enum PresentationResourceKey: Int32 { case chatListLockBottomLockedImage case chatListLockTopUnlockedImage case chatListLockBottomUnlockedImage + case chatListPending case chatListSingleCheck case chatListDoubleCheck case chatListBadgeBackgroundActive @@ -48,10 +52,12 @@ enum PresentationResourceKey: Int32 { case chatListBadgeBackgroundPinned case chatListMutedIcon case chatListVerifiedIcon + case chatListSecretIcon case chatPrincipalThemeEssentialGraphics case chatBubbleVerticalLineIncomingImage case chatBubbleVerticalLineOutgoingImage + case chatServiceVerticalLineImage case chatBubbleCheckBubbleFullImage case chatBubbleBubblePartialImage @@ -63,8 +69,10 @@ enum PresentationResourceKey: Int32 { case chatBubbleConsumableContentIncomingIcon case chatBubbleConsumableContentOutgoingIcon + case chatMediaConsumableContentIcon case chatBubbleShareButtonImage + case chatBubbleNavigateButtonImage case chatBubbleMediaOverlayControlSecret @@ -78,10 +86,15 @@ enum PresentationResourceKey: Int32 { case chatInstantVideoBackgroundImage case chatUnreadBarBackgroundImage - case chatBubbleActionButtonMiddleImage - case chatBubbleActionButtonBottomLeftImage - case chatBubbleActionButtonBottomRightImage - case chatBubbleActionButtonBottomSingleImage + case chatBubbleActionButtonIncomingMiddleImage + case chatBubbleActionButtonIncomingBottomLeftImage + case chatBubbleActionButtonIncomingBottomRightImage + case chatBubbleActionButtonIncomingBottomSingleImage + + case chatBubbleActionButtonOutgoingMiddleImage + case chatBubbleActionButtonOutgoingBottomLeftImage + case chatBubbleActionButtonOutgoingBottomRightImage + case chatBubbleActionButtonOutgoingBottomSingleImage case chatBubbleReplyThumbnailPlayImage @@ -96,6 +109,8 @@ enum PresentationResourceKey: Int32 { case chatInputMediaPanelSavedStickersIconImage case chatInputMediaPanelRecentStickersIconImage case chatInputMediaPanelRecentGifsIconImage + case chatInputMediaPanelTrendingIconImage + case chatInputMediaPanelSettingsIconImage case chatInputButtonPanelButtonImage case chatInputButtonPanelButtonHighlightedImage @@ -103,6 +118,7 @@ enum PresentationResourceKey: Int32 { case chatInputTextFieldBackgroundImage case chatInputTextFieldClearImage case chatInputPanelSendButtonImage + case chatInputPanelApplyButtonImage case chatInputPanelVoiceButtonImage case chatInputPanelVideoButtonImage case chatInputPanelVoiceActiveButtonImage @@ -120,6 +136,7 @@ enum PresentationResourceKey: Int32 { case chatInputSearchPanelDownImage case chatInputSearchPanelDownDisabledImage case chatInputSearchPanelCalendarImage + case chatInputSearchPanelMembersImage case chatTitlePanelInfoImage case chatTitlePanelSearchImage @@ -142,19 +159,29 @@ enum PresentationResourceKey: Int32 { case chatMessageAttachedContentButtonIconInstantOutgoing case chatMessageAttachedContentHighlightedButtonIconInstantOutgoing + case chatCommandPanelArrowImage + case sharedMediaFileDownloadStartIcon case sharedMediaFileDownloadPauseIcon case chatInfoCallButtonImage + case chatInstantMessageInfoBackgroundImage case chatInstantMessageMuteIconImage case chatBubbleIncomingCallButtonImage case chatBubbleOutgoingCallButtonImage + case chatBubbleMapPinImage + + case chatSelectionButtonChecked + case chatSelectionButtonUnchecked + case callListOutgoingIcon case callListInfoButton case genericSearchBarLoupeImage case genericSearchBar + + case inAppNotificationBackground } diff --git a/TelegramUI/PresentationResourcesChat.swift b/TelegramUI/PresentationResourcesChat.swift index 3ac3f09817..2fcc5311eb 100644 --- a/TelegramUI/PresentationResourcesChat.swift +++ b/TelegramUI/PresentationResourcesChat.swift @@ -44,13 +44,19 @@ struct PresentationResourcesChat { static func chatBubbleVerticalLineIncomingImage(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatBubbleVerticalLineIncomingImage.rawValue, { theme in - return generateLineImage(color: theme.chat.bubble.incomingAccentColor) + return generateLineImage(color: theme.chat.bubble.incomingAccentControlColor) }) } static func chatBubbleVerticalLineOutgoingImage(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatBubbleVerticalLineOutgoingImage.rawValue, { theme in - return generateLineImage(color: theme.chat.bubble.outgoingAccentColor) + return generateLineImage(color: theme.chat.bubble.outgoingAccentControlColor) + }) + } + + static func chatServiceVerticalLineImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatServiceVerticalLineImage.rawValue, { theme in + return generateLineImage(color: theme.chat.serviceMessage.serviceMessagePrimaryTextColor) }) } @@ -68,13 +74,19 @@ struct PresentationResourcesChat { static func chatBubbleConsumableContentIncomingIcon(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatBubbleConsumableContentIncomingIcon.rawValue, { theme in - return generateFilledCircleImage(diameter: 4.0, color: theme.chat.bubble.incomingAccentColor) + return generateFilledCircleImage(diameter: 4.0, color: theme.chat.bubble.incomingAccentControlColor) }) } static func chatBubbleConsumableContentOutgoingIcon(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatBubbleConsumableContentOutgoingIcon.rawValue, { theme in - return generateFilledCircleImage(diameter: 4.0, color: theme.chat.bubble.outgoingAccentColor) + return generateFilledCircleImage(diameter: 4.0, color: theme.chat.bubble.outgoingAccentControlColor) + }) + } + + static func chatMediaConsumableContentIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatMediaConsumableContentIcon.rawValue, { theme in + return generateFilledCircleImage(diameter: 4.0, color: theme.chat.bubble.mediaDateAndStatusTextColor) }) } @@ -84,6 +96,15 @@ struct PresentationResourcesChat { context.clear(CGRect(origin: CGPoint(), size: size)) context.setFillColor(theme.chat.bubble.shareButtonFillColor.cgColor) context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + let lineWidth: CGFloat = 1.0 + let halfLineWidth = lineWidth / 2.0 + var strokeAlpha: CGFloat = 0.0 + theme.chat.bubble.shareButtonStrokeColor.getRed(nil, green: nil, blue: nil, alpha: &strokeAlpha) + if !strokeAlpha.isZero { + context.setStrokeColor(theme.chat.bubble.shareButtonStrokeColor.cgColor) + context.setLineWidth(lineWidth) + context.strokeEllipse(in: CGRect(origin: CGPoint(x: halfLineWidth, y: halfLineWidth), size: CGSize(width: size.width - lineWidth, height: size.width - lineWidth))) + } if let image = UIImage(bundleImageName: "Chat/Message/ShareIcon") { let imageRect = CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size) @@ -99,6 +120,36 @@ struct PresentationResourcesChat { }) } + static func chatBubbleNavigateButtonImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatBubbleNavigateButtonImage.rawValue, { theme in + return generateImage(CGSize(width: 29.0, height: 29.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(theme.chat.bubble.shareButtonFillColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + let lineWidth: CGFloat = 1.0 + let halfLineWidth = lineWidth / 2.0 + var strokeAlpha: CGFloat = 0.0 + theme.chat.bubble.shareButtonStrokeColor.getRed(nil, green: nil, blue: nil, alpha: &strokeAlpha) + if !strokeAlpha.isZero { + context.setStrokeColor(theme.chat.bubble.shareButtonStrokeColor.cgColor) + context.setLineWidth(lineWidth) + context.strokeEllipse(in: CGRect(origin: CGPoint(x: halfLineWidth, y: halfLineWidth), size: CGSize(width: size.width - lineWidth, height: size.width - lineWidth))) + } + + if let image = UIImage(bundleImageName: "Chat/Message/NavigateToMessageIcon") { + let imageRect = CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0) + 1.0), size: image.size) + + context.translateBy(x: imageRect.midX, y: imageRect.midY) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -imageRect.midX, y: -imageRect.midY) + context.clip(to: imageRect, mask: image.cgImage!) + context.setFillColor(theme.chat.bubble.shareButtonForegroundColor.cgColor) + context.fill(imageRect) + } + }) + }) + } + static func chatServiceBubbleFillImage(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatServiceBubbleFillImage.rawValue, { theme in return generateImage(CGSize(width: 20.0, height: 20.0), contextGenerator: { size, context -> Void in @@ -140,27 +191,51 @@ struct PresentationResourcesChat { }) } - static func chatBubbleActionButtonMiddleImage(_ theme: PresentationTheme) -> UIImage? { - return theme.image(PresentationResourceKey.chatBubbleActionButtonMiddleImage.rawValue, { theme in - return messageBubbleActionButtonImage(color: theme.chat.bubble.actionButtonsFillColor, position: .middle) + static func chatBubbleActionButtonIncomingMiddleImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatBubbleActionButtonIncomingMiddleImage.rawValue, { theme in + return messageBubbleActionButtonImage(color: theme.chat.bubble.actionButtonsIncomingFillColor, strokeColor: theme.chat.bubble.actionButtonsIncomingStrokeColor, position: .middle) }) } - static func chatBubbleActionButtonBottomLeftImage(_ theme: PresentationTheme) -> UIImage? { - return theme.image(PresentationResourceKey.chatBubbleActionButtonBottomLeftImage.rawValue, { theme in - return messageBubbleActionButtonImage(color: theme.chat.bubble.actionButtonsFillColor, position: .bottomLeft) + static func chatBubbleActionButtonIncomingBottomLeftImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatBubbleActionButtonIncomingBottomLeftImage.rawValue, { theme in + return messageBubbleActionButtonImage(color: theme.chat.bubble.actionButtonsIncomingFillColor, strokeColor: theme.chat.bubble.actionButtonsIncomingStrokeColor, position: .bottomLeft) }) } - static func chatBubbleActionButtonBottomRightImage(_ theme: PresentationTheme) -> UIImage? { - return theme.image(PresentationResourceKey.chatBubbleActionButtonBottomRightImage.rawValue, { theme in - return messageBubbleActionButtonImage(color: theme.chat.bubble.actionButtonsFillColor, position: .bottomRight) + static func chatBubbleActionButtonIncomingBottomRightImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatBubbleActionButtonIncomingBottomRightImage.rawValue, { theme in + return messageBubbleActionButtonImage(color: theme.chat.bubble.actionButtonsIncomingFillColor, strokeColor: theme.chat.bubble.actionButtonsIncomingStrokeColor, position: .bottomRight) }) } - static func chatBubbleActionButtonBottomSingleImage(_ theme: PresentationTheme) -> UIImage? { - return theme.image(PresentationResourceKey.chatBubbleActionButtonBottomSingleImage.rawValue, { theme in - return messageBubbleActionButtonImage(color: theme.chat.bubble.actionButtonsFillColor, position: .bottomSingle) + static func chatBubbleActionButtonIncomingBottomSingleImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatBubbleActionButtonIncomingBottomSingleImage.rawValue, { theme in + return messageBubbleActionButtonImage(color: theme.chat.bubble.actionButtonsIncomingFillColor, strokeColor: theme.chat.bubble.actionButtonsIncomingStrokeColor, position: .bottomSingle) + }) + } + + static func chatBubbleActionButtonOutgoingMiddleImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatBubbleActionButtonOutgoingMiddleImage.rawValue, { theme in + return messageBubbleActionButtonImage(color: theme.chat.bubble.actionButtonsOutgoingFillColor, strokeColor: theme.chat.bubble.actionButtonsOutgoingStrokeColor, position: .middle) + }) + } + + static func chatBubbleActionButtonOutgoingBottomLeftImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatBubbleActionButtonOutgoingBottomLeftImage.rawValue, { theme in + return messageBubbleActionButtonImage(color: theme.chat.bubble.actionButtonsOutgoingFillColor, strokeColor: theme.chat.bubble.actionButtonsOutgoingStrokeColor, position: .bottomLeft) + }) + } + + static func chatBubbleActionButtonOutgoingBottomRightImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatBubbleActionButtonOutgoingBottomRightImage.rawValue, { theme in + return messageBubbleActionButtonImage(color: theme.chat.bubble.actionButtonsOutgoingFillColor, strokeColor: theme.chat.bubble.actionButtonsOutgoingStrokeColor, position: .bottomRight) + }) + } + + static func chatBubbleActionButtonOutgoingBottomSingleImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatBubbleActionButtonOutgoingBottomSingleImage.rawValue, { theme in + return messageBubbleActionButtonImage(color: theme.chat.bubble.actionButtonsOutgoingFillColor, strokeColor: theme.chat.bubble.actionButtonsOutgoingStrokeColor, position: .bottomSingle) }) } @@ -266,6 +341,18 @@ struct PresentationResourcesChat { }) } + static func chatInputMediaPanelTrendingIconImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatInputMediaPanelTrendingIconImage.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/TrendingIcon"), color: theme.chat.inputMediaPanel.panelIconColor) + }) + } + + static func chatInputMediaPanelSettingsIconImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatInputMediaPanelSettingsIconImage.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/SettingsIcon"), color: theme.chat.inputMediaPanel.panelIconColor) + }) + } + static func chatInputButtonPanelButtonImage(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatInputButtonPanelButtonImage.rawValue, { theme in return generateInputPanelButtonBackgroundImage(fillColor: theme.chat.inputButtonPanel.buttonFillColor, strokeColor: theme.chat.inputButtonPanel.buttonStrokeColor) @@ -281,12 +368,15 @@ struct PresentationResourcesChat { static func chatInputTextFieldBackgroundImage(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatInputTextFieldBackgroundImage.rawValue, { theme in let diameter: CGFloat = 35.0 - UIGraphicsBeginImageContextWithOptions(CGSize(width: diameter, height: diameter), true, 0.0) + UIGraphicsBeginImageContextWithOptions(CGSize(width: diameter, height: diameter), false, 0.0) let context = UIGraphicsGetCurrentContext()! context.setFillColor(theme.chat.inputPanel.panelBackgroundColor.cgColor) context.fill(CGRect(x: 0.0, y: 0.0, width: diameter, height: diameter)) - context.setFillColor(theme.chat.inputPanel.inputBackgroundColor.cgColor) + + context.setBlendMode(.clear) + context.setFillColor(UIColor.clear.cgColor) context.fillEllipse(in: CGRect(x: 0.0, y: 0.0, width: diameter, height: diameter)) + context.setBlendMode(.normal) context.setStrokeColor(theme.chat.inputPanel.inputStrokeColor.cgColor) let strokeWidth: CGFloat = 0.5 context.setLineWidth(strokeWidth) @@ -329,7 +419,34 @@ struct PresentationResourcesChat { static func chatInputPanelSendButtonImage(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatInputPanelSendButtonImage.rawValue, { theme in - return UIImage(bundleImageName: "Chat/Input/Text/IconSend") + return generateImage(CGSize(width: 33.0, height: 33.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(theme.chat.inputPanel.actionControlFillColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + context.setStrokeColor(theme.chat.inputPanel.actionControlForegroundColor.cgColor) + context.setFillColor(theme.chat.inputPanel.actionControlForegroundColor.cgColor) + context.setLineWidth(2.0) + context.setLineCap(.round) + context.setLineJoin(.round) + let _ = try? drawSvgPath(context, path: "M11,14.6666667 L16.4310816,9.40016333 L16.4310816,9.40016333 C16.4694824,9.36292619 16.5305176,9.36292619 16.5689184,9.40016333 L22,14.6666667 S ") + let _ = try? drawSvgPath(context, path: "M16.5,9.33333333 C17.0522847,9.33333333 17.5,9.78104858 17.5,10.3333333 L17.5,24 C17.5,24.5522847 17.0522847,25 16.5,25 C15.9477153,25 15.5,24.5522847 15.5,24 L15.5,10.3333333 C15.5,9.78104858 15.9477153,9.33333333 16.5,9.33333333 Z ") + }) + }) + } + + static func chatInputPanelApplyButtonImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatInputPanelApplyButtonImage.rawValue, { theme in + return generateImage(CGSize(width: 33.0, height: 33.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(theme.chat.inputPanel.actionControlFillColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + context.setStrokeColor(theme.chat.inputPanel.actionControlForegroundColor.cgColor) + context.setFillColor(theme.chat.inputPanel.actionControlForegroundColor.cgColor) + context.setLineWidth(2.0) + context.setLineCap(.round) + context.setLineJoin(.round) + let _ = try? drawSvgPath(context, path: "M9.33333333,17.2686567 L14.1849216,22.120245 L14.1849216,22.120245 C14.2235835,22.1589069 14.2862668,22.1589069 14.3249287,22.120245 C14.3261558,22.1190179 14.3273504,22.1177588 14.3285113,22.1164689 L24.3333333,11 S ") + }) }) } @@ -439,7 +556,7 @@ struct PresentationResourcesChat { static func chatHistoryNavigationButtonBadgeImage(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatHistoryNavigationButtonBadgeImage.rawValue, { theme in - return generateStretchableFilledCircleImage(diameter: 18.0, color: theme.chat.historyNavigation.badgeBackgroundColor, backgroundColor: nil) + return generateStretchableFilledCircleImage(diameter: 18.0, color: theme.chat.historyNavigation.badgeBackgroundColor, strokeColor: theme.chat.historyNavigation.badgeStrokeColor, strokeWidth: 1.0, backgroundColor: nil) }) } @@ -468,12 +585,20 @@ struct PresentationResourcesChat { }) } - static func chatInstantMessageMuteIconImage(_ theme: PresentationTheme) -> UIImage? { - return theme.image(PresentationResourceKey.chatInstantMessageMuteIconImage.rawValue, { theme in + static func chatInstantMessageInfoBackgroundImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatInstantMessageInfoBackgroundImage.rawValue, { theme in return generateImage(CGSize(width: 24.0, height: 24.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.setFillColor(theme.chat.bubble.mediaDateAndStatusFillColor.cgColor) context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + })?.stretchableImage(withLeftCapWidth: 12, topCapHeight: 12) + }) + } + + static func chatInstantMessageMuteIconImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatInstantMessageMuteIconImage.rawValue, { theme in + return generateImage(CGSize(width: 24.0, height: 24.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/InstantVideoMute"), color: .white, backgroundColor: nil) { context.draw(image.cgImage!, in: CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size)) @@ -484,13 +609,30 @@ struct PresentationResourcesChat { static func chatBubbleIncomingCallButtonImage(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatBubbleIncomingCallButtonImage.rawValue, { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Info/CallButton"), color: theme.chat.bubble.incomingAccentColor) + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Info/CallButton"), color: theme.chat.bubble.incomingAccentControlColor) }) } static func chatBubbleOutgoingCallButtonImage(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatBubbleOutgoingCallButtonImage.rawValue, { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Info/CallButton"), color: theme.chat.bubble.outgoingAccentColor) + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Info/CallButton"), color: theme.chat.bubble.outgoingAccentControlColor) + }) + } + + static func chatBubbleMapPinImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatBubbleMapPinImage.rawValue, { theme in + return generateImage(CGSize(width: 62.0, height: 74.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + if let shadowImage = UIImage(bundleImageName: "Chat/Message/LocationPinShadow"), let cgImage = shadowImage.cgImage { + context.draw(cgImage, in: CGRect(origin: CGPoint(), size: shadowImage.size)) + } + if let backgroundImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/LocationPinBackground"), color: theme.list.itemAccentColor), let cgImage = backgroundImage.cgImage { + context.draw(cgImage, in: CGRect(origin: CGPoint(), size: backgroundImage.size)) + } + if let foregroundImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/LocationPinForeground"), color: theme.list.plainBackgroundColor), let cgImage = foregroundImage.cgImage { + context.draw(cgImage, in: CGRect(origin: CGPoint(x: 15.0, y: 26.0), size: foregroundImage.size)) + } + }) }) } @@ -524,12 +666,11 @@ struct PresentationResourcesChat { }) } - /* - case chatTitlePanelSearchImage - case chatTitlePanelMuteImage - case chatTitlePanelUnmuteImage - case chatTitlePanelCallImage - */ + static func chatInputSearchPanelMembersImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatInputSearchPanelMembersImage.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Search/Members"), color: theme.chat.inputPanel.panelControlAccentColor) + }) + } static func chatTitlePanelInfoImage(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatTitlePanelInfoImage.rawValue, { theme in @@ -557,7 +698,7 @@ struct PresentationResourcesChat { static func chatTitlePanelCallImage(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatTitlePanelCallImage.rawValue, { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat List/Tabs/IconCalls"), color: theme.chat.inputPanel.panelControlAccentColor) + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Info/CallButton"), color: theme.chat.inputPanel.panelControlAccentColor) }) } @@ -569,31 +710,31 @@ struct PresentationResourcesChat { static func chatMessageAttachedContentButtonIncoming(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatMessageAttachedContentButtonIncoming.rawValue, { theme in - return generateStretchableFilledCircleImage(diameter: 9.0, color: nil, strokeColor: theme.chat.bubble.incomingAccentColor, strokeWidth: 1.0, backgroundColor: nil) + return generateStretchableFilledCircleImage(diameter: 9.0, color: nil, strokeColor: theme.chat.bubble.incomingAccentControlColor, strokeWidth: 1.0, backgroundColor: nil) }) } static func chatMessageAttachedContentHighlightedButtonIncoming(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatMessageAttachedContentHighlightedButtonIncoming.rawValue, { theme in - return generateStretchableFilledCircleImage(diameter: 9.0, color: theme.chat.bubble.incomingAccentColor) + return generateStretchableFilledCircleImage(diameter: 9.0, color: theme.chat.bubble.incomingAccentControlColor) }) } static func chatMessageAttachedContentButtonOutgoing(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatMessageAttachedContentButtonOutgoing.rawValue, { theme in - return generateStretchableFilledCircleImage(diameter: 9.0, color: nil, strokeColor: theme.chat.bubble.outgoingAccentColor, strokeWidth: 1.0, backgroundColor: nil) + return generateStretchableFilledCircleImage(diameter: 9.0, color: nil, strokeColor: theme.chat.bubble.outgoingAccentControlColor, strokeWidth: 1.0, backgroundColor: nil) }) } static func chatMessageAttachedContentHighlightedButtonOutgoing(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatMessageAttachedContentHighlightedButtonOutgoing.rawValue, { theme in - return generateStretchableFilledCircleImage(diameter: 9.0, color: theme.chat.bubble.outgoingAccentColor) + return generateStretchableFilledCircleImage(diameter: 9.0, color: theme.chat.bubble.outgoingAccentControlColor) }) } static func chatMessageAttachedContentButtonIconInstantIncoming(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatMessageAttachedContentButtonIconInstantIncoming.rawValue, { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/AttachedContentInstantIcon"), color: theme.chat.bubble.incomingAccentColor) + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/AttachedContentInstantIcon"), color: theme.chat.bubble.incomingAccentControlColor) }) } @@ -605,7 +746,7 @@ struct PresentationResourcesChat { static func chatMessageAttachedContentButtonIconInstantOutgoing(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatMessageAttachedContentButtonIconInstantOutgoing.rawValue, { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/AttachedContentInstantIcon"), color: theme.chat.bubble.outgoingAccentColor) + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/AttachedContentInstantIcon"), color: theme.chat.bubble.outgoingAccentControlColor) }) } @@ -652,4 +793,61 @@ struct PresentationResourcesChat { }) }) } + + static func chatCommandPanelArrowImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatCommandPanelArrowImage.rawValue, { theme in + return generateImage(CGSize(width: 11.0, height: 11.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + + context.setStrokeColor(theme.list.disclosureArrowColor.cgColor) + context.setLineCap(.round) + context.setLineJoin(.round) + context.setLineWidth(2.0) + context.setLineJoin(.round) + + context.beginPath() + context.move(to: CGPoint(x: 1.0, y: 2.0)) + context.addLine(to: CGPoint(x: 1.0, y: 10.0)) + context.addLine(to: CGPoint(x: 9.0, y: 10.0)) + context.strokePath() + + context.beginPath() + context.move(to: CGPoint(x: 1.0, y: 10.0)) + context.addLine(to: CGPoint(x: 10.0, y: 1.0)) + context.strokePath() + }) + }) + } + + static func chatSelectionButtonChecked(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatSelectionButtonChecked.rawValue, { theme in + return generateImage(CGSize(width: 22.0, height: 22.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.setFillColor(theme.chat.bubble.selectionControlFillColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))) + + context.setStrokeColor(theme.chat.bubble.selectionControlForegroundColor.cgColor) + context.setLineWidth(1.66) + context.setLineCap(.round) + context.setLineJoin(.round) + let _ = try? drawSvgPath(context, path: "M6.22222222,11.5124378 L9.43201945,14.722235 L9.43201945,14.722235 C9.47068136,14.7608969 9.53336469,14.7608969 9.57202659,14.722235 C9.57325367,14.721008 9.57444826,14.7197488 9.57560914,14.718459 L16.2222222,7.33333333 S ") + }) + }) + } + + static func chatSelectionButtonUnchecked(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatSelectionButtonUnchecked.rawValue, { theme in + return generateImage(CGSize(width: 22.0, height: 22.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.setStrokeColor(theme.chat.bubble.selectionControlBorderColor.cgColor) + context.setLineWidth(1.0) + context.strokeEllipse(in: CGRect(origin: CGPoint(x: 0.5, y: 0.5), size: CGSize(width: size.width - 1.0, height: size.height - 1.0))) + }) + }) + } } diff --git a/TelegramUI/PresentationResourcesChatList.swift b/TelegramUI/PresentationResourcesChatList.swift index 8025a16bc4..b4d641b859 100644 --- a/TelegramUI/PresentationResourcesChatList.swift +++ b/TelegramUI/PresentationResourcesChatList.swift @@ -2,23 +2,18 @@ import Foundation import Display private func generateStatusCheckImage(theme: PresentationTheme, single: Bool) -> UIImage? { - return generateImage(CGSize(width: single ? 13.0 : 18.0, height: 13.0), contextGenerator: { size, context in + return generateImage(CGSize(width: single ? 13.0 : 18.0, height: 13.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) - - context.translateBy(x: size.width / 2.0, y: size.height / 2.0) - context.scaleBy(x: 1.0, y: -1.0) - context.translateBy(x: -size.width / 2.0 + 1.0, y: -size.height / 2.0 + 1.0) - - context.scaleBy(x: 0.5, y: 0.5) + context.translateBy(x: 1.0, y: 2.0) context.setStrokeColor(theme.chatList.checkmarkColor.cgColor) - context.setLineWidth(2.8) - if single { - let _ = try? drawSvgPath(context, path: "M0,12 L6.75230742,19.080349 L22.4821014,0.277229071 ") - } else { - let _ = try? drawSvgPath(context, path: "M0,12 L6.75230742,19.080349 L22.4821014,0.277229071 ") - let _ = try? drawSvgPath(context, path: "M13.4492402,16.500967 L15.7523074,18.8031199 L31.4821014,0 ") + context.setLineWidth(1.32) + context.setLineCap(.round) + context.setLineJoin(.round) + let _ = try? drawSvgPath(context, path: "M0,4.48 L3.59439858,7.93062264 L3.59439858,7.93062264 C3.63384129,7.96848764 3.69651158,7.96720866 3.73437658,7.92776595 C3.7346472,7.92748405 3.73491615,7.92720055 3.73518342,7.92691547 L11.1666667,0 S ") + + if !single { + let _ = try? drawSvgPath(context, path: "M7.33333333,8 L14.8333333,0 S ") } - context.strokePath() }) } @@ -38,6 +33,21 @@ private func generateBadgeBackgroundImage(theme: PresentationTheme, active: Bool } struct PresentationResourcesChatList { + static func pendingImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatListPending.rawValue, { theme in + return generateImage(CGSize(width: 12.0, height: 14.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.translateBy(x: 0.0, y: 1.0) + context.setStrokeColor(theme.chatList.pendingIndicatorColor.cgColor) + let lineWidth: CGFloat = 0.99 + context.setLineWidth(lineWidth) + context.strokeEllipse(in: CGRect(origin: CGPoint(x: lineWidth / 2.0, y: lineWidth / 2.0), size: CGSize(width: 12.0 - lineWidth, height: 12.0 - lineWidth))) + context.setLineCap(.round) + let _ = try? drawSvgPath(context, path: "M6.01830142,3 L6.01830142,6.23251697 L4.5,7.81306587 S ") + }) + }) + } + static func singleCheckImage(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatListSingleCheck.rawValue, { theme in return generateStatusCheckImage(theme: theme, single: true) @@ -88,7 +98,7 @@ struct PresentationResourcesChatList { static func badgeBackgroundMention(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatListBadgeBackgroundMention.rawValue, { theme in - return generateBadgeBackgroundImage(theme: theme, active: true, icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/MentionBadgeIcon"), color: .white)) + return generateBadgeBackgroundImage(theme: theme, active: true, icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/MentionBadgeIcon"), color: theme.chatList.unreadBadgeActiveTextColor)) }) } @@ -109,4 +119,18 @@ struct PresentationResourcesChatList { return UIImage(bundleImageName: "Chat List/PeerVerifiedIcon")?.precomposed() }) } + + static func secretIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatListSecretIcon.rawValue, { theme in + return generateImage(CGSize(width: 9.0, height: 12.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(theme.chatList.secretIconColor.cgColor) + context.setStrokeColor(theme.chatList.secretIconColor.cgColor) + context.setLineWidth(1.32) + + let _ = try? drawSvgPath(context, path: "M4.5,0.66 C3.11560623,0.66 1.99333333,1.78227289 1.99333333,3.16666667 L1.99333333,7.8047619 C1.99333333,9.18915568 3.11560623,10.3114286 4.5,10.3114286 C5.88439377,10.3114286 7.00666667,9.18915568 7.00666667,7.8047619 L7.00666667,3.16666667 C7.00666667,1.78227289 5.88439377,0.66 4.5,0.66 S ") + let _ = try? drawSvgPath(context, path: "M1.32,5.48571429 L7.68,5.48571429 C8.40901587,5.48571429 9,6.07669842 9,6.80571429 L9,10.68 C9,11.4090159 8.40901587,12 7.68,12 L1.32,12 C0.59098413,12 8.92786951e-17,11.4090159 0,10.68 L2.22044605e-16,6.80571429 C1.3276591e-16,6.07669842 0.59098413,5.48571429 1.32,5.48571429 Z ") + }) + }) + } } diff --git a/TelegramUI/PresentationResourcesItemList.swift b/TelegramUI/PresentationResourcesItemList.swift index 9cadd2cb86..e76f075d81 100644 --- a/TelegramUI/PresentationResourcesItemList.swift +++ b/TelegramUI/PresentationResourcesItemList.swift @@ -2,23 +2,28 @@ import Foundation import Display private func generateArrowImage(_ theme: PresentationTheme) -> UIImage? { - return generateImage(CGSize(width: 8.0, height: 14.0), rotatedContext: { size, context in + return generateImage(CGSize(width: 7.0, height: 13.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(theme.list.disclosureArrowColor.cgColor) + context.setStrokeColor(theme.list.disclosureArrowColor.cgColor) + context.setLineWidth(1.98) + context.setLineCap(.round) + context.setLineJoin(.round) + context.translateBy(x: 1.0, y: 1.0) - let _ = try? drawSvgPath(context, path: "M5.41663691,6.58336309 L0,12 L1.16672619,13.1667262 L7.75008928,6.58336309 L1.16672619,0 L0,1.16672619 Z ") + let _ = try? drawSvgPath(context, path: "M0,0 L4.79819816,5.27801798 L4.79819816,5.27801798 C4.91262453,5.40388698 4.91262453,5.59611302 4.79819816,5.72198202 L0,11 S ") }) } private func generateCheckIcon(color: UIColor) -> UIImage? { - return generateImage(CGSize(width: 14.0, height: 11.0), rotatedContext: { size, context in + return generateImage(CGSize(width: 12.0, height: 10.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.setStrokeColor(color.cgColor) - context.setLineWidth(2.0) - context.move(to: CGPoint(x: 12.0, y: 1.0)) - context.addLine(to: CGPoint(x: 4.16482734, y: 9.0)) - context.addLine(to: CGPoint(x: 1.0, y: 5.81145833)) - context.strokePath() + context.setLineWidth(1.98) + context.setLineCap(.round) + context.setLineJoin(.round) + context.translateBy(x: 1.0, y: 1.0) + + let _ = try? drawSvgPath(context, path: "M0.215053763,4.36080467 L3.31621263,7.70466293 L3.31621263,7.70466293 C3.35339229,7.74475231 3.41603123,7.74711109 3.45612061,7.70993143 C3.45920681,7.70706923 3.46210733,7.70401312 3.46480451,7.70078171 L9.89247312,0 S ") }) } @@ -64,4 +69,24 @@ struct PresentationResourcesItemList { return UIImage(bundleImageName: "Item List/PeerVerifiedIcon")?.precomposed() }) } + + static func itemListDeleteIndicatorIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.itemListDeleteIndicatorIcon.rawValue, { theme in + generateImage(CGSize(width: 22.0, height: 26.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(UIColor(white: 0.0, alpha: 0.06).cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 22.0, height: 22.0))) + context.setFillColor(theme.list.itemDisclosureActions.destructive.fillColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: 2.0), size: CGSize(width: 22.0, height: 22.0))) + context.setFillColor(theme.list.itemDisclosureActions.destructive.foregroundColor.cgColor) + context.fill(CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - 11.0) / 2.0), y: 2.0 + floorToScreenPixels((size.width - 1.0) / 2.0)), size: CGSize(width: 11.0, height: 1.0))) + }) + }) + } + + static func addPersonIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.itemListAddPersonIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Contact List/AddMemberIcon"), color: theme.list.itemAccentColor) + }) + } } diff --git a/TelegramUI/PresentationResourcesRootController.swift b/TelegramUI/PresentationResourcesRootController.swift index 4483eb15d2..1b0fc7d2e3 100644 --- a/TelegramUI/PresentationResourcesRootController.swift +++ b/TelegramUI/PresentationResourcesRootController.swift @@ -8,8 +8,13 @@ private func generateShareButtonImage(theme: PresentationTheme) -> UIImage? { func generateIndefiniteActivityIndicatorImage(color: UIColor, diameter: CGFloat = 22.0) -> UIImage? { return generateImage(CGSize(width: diameter, height: diameter), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(color.cgColor) - let _ = try? drawSvgPath(context, path: "M11,22 C17.0751322,22 22,17.0751322 22,11 C22,4.92486775 17.0751322,0 11,0 C4.92486775,0 0,4.92486775 0,11 C0,12.4564221 0.28362493,13.8747731 0.827833595,15.1935223 C1.00609922,15.6255031 1.50080164,15.8311798 1.93278238,15.6529142 C2.36476311,15.4746485 2.57043984,14.9799461 2.39217421,14.5479654 C1.93209084,13.4330721 1.69230769,12.233965 1.69230769,11 C1.69230769,5.85950348 5.85950348,1.69230769 11,1.69230769 C16.1404965,1.69230769 20.3076923,5.85950348 20.3076923,11 C20.3076923,16.1404965 16.1404965,20.3076923 11,20.3076923 C10.5326821,20.3076923 10.1538462,20.6865283 10.1538462,21.1538462 C10.1538462,21.621164 10.5326821,22 11,22 Z ") + context.setStrokeColor(color.cgColor) + let lineWidth: CGFloat = 2.0 + context.setLineWidth(lineWidth) + context.setLineCap(.round) + let cutoutAngle: CGFloat = CGFloat.pi * 30.0 / 180.0 + context.addArc(center: CGPoint(x: size.width / 2.0, y: size.height / 2.0), radius: size.width / 2.0 - lineWidth / 2.0, startAngle: 0.0, endAngle: CGFloat.pi * 2.0 - cutoutAngle, clockwise: false) + context.strokePath() }) } @@ -28,7 +33,7 @@ struct PresentationResourcesRootController { static func tabContactsSelectedIcon(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.rootTabContactsSelectedIcon.rawValue, { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat List/Tabs/IconContactsSelected"), color: theme.rootController.tabBar.selectedIconColor) + return generateTintedImage(image: UIImage(bundleImageName: "Chat List/Tabs/IconContacts"), color: theme.rootController.tabBar.selectedIconColor) }) } @@ -40,7 +45,7 @@ struct PresentationResourcesRootController { static func tabChatsSelectedIcon(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.rootTabChatsSelectedIcon.rawValue, { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat List/Tabs/IconChatsSelected"), color: theme.rootController.tabBar.selectedIconColor) + return generateTintedImage(image: UIImage(bundleImageName: "Chat List/Tabs/IconChats"), color: theme.rootController.tabBar.selectedIconColor) }) } @@ -52,7 +57,7 @@ struct PresentationResourcesRootController { static func tabSettingsSelectedIcon(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.rootTabSettingsSelectedIcon.rawValue, { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat List/Tabs/IconSettingsSelected"), color: theme.rootController.tabBar.selectedIconColor) + return generateTintedImage(image: UIImage(bundleImageName: "Chat List/Tabs/IconSettings"), color: theme.rootController.tabBar.selectedIconColor) }) } @@ -72,6 +77,12 @@ struct PresentationResourcesRootController { }) } + static func navigationSearchIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.navigationSearchIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat List/SearchIcon"), color: theme.rootController.navigationBar.accentTextColor) + }) + } + static func navigationPlayerCloseButton(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.navigationPlayerCloseButton.rawValue, { theme in return generateImage(CGSize(width: 12.0, height: 12.0), contextGenerator: { size, context in @@ -142,4 +153,15 @@ struct PresentationResourcesRootController { return generateStretchableFilledCircleImage(diameter: 7.0, color: theme.rootController.navigationBar.controlColor) }) } + + static func inAppNotificationBackground(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.inAppNotificationBackground.rawValue, { theme in + return generateImage(CGSize(width: 30.0 + 8.0 * 2.0, height: 30.0 + 8.0 + 20.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setShadow(offset: CGSize(width: 0.0, height: -4.0), blur: 40.0, color: UIColor(white: 0.0, alpha: 0.3).cgColor) + context.setFillColor(theme.inAppNotification.fillColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 8.0, y: 8.0), size: CGSize(width: 30.0, height: 30.0))) + })?.stretchableImage(withLeftCapWidth: 8 + 15, topCapHeight: 8 + 15) + }) + } } diff --git a/TelegramUI/PresentationStrings.swift b/TelegramUI/PresentationStrings.swift index fe1935ccf3..8717846c55 100644 --- a/TelegramUI/PresentationStrings.swift +++ b/TelegramUI/PresentationStrings.swift @@ -30,15 +30,15 @@ private extension PluralizationForm { var canonicalSuffix: String { switch self { case .zero: - return "_many" + return "_0" case .one: return "_1" case .two: return "_2" case .few: - return "_1_3" + return "_3_10" case .many: - return "_any" + return "_many" case .other: return "_any" } @@ -104,19 +104,15 @@ public final class PresentationStrings { public let EnterPasscode_EnterNewPasscodeNew: String public let Privacy_Calls_WhoCanCallMe: String public let Watch_NoConnection: String - private let _Group_Username_LinkHint: String - private let _Group_Username_LinkHint_r: [(Int, NSRange)] - public func Group_Username_LinkHint(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_Group_Username_LinkHint, self._Group_Username_LinkHint_r, [_0]) - } public let Activity_UploadingPhoto: String public let PrivacySettings_PrivacyTitle: String - public let Settings_LogoutError: String private let _DialogList_PinLimitError: String private let _DialogList_PinLimitError_r: [(Int, NSRange)] public func DialogList_PinLimitError(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_DialogList_PinLimitError, self._DialogList_PinLimitError_r, [_0]) } + public let FastTwoStepSetup_PasswordSection: String + public let FastTwoStepSetup_EmailSection: String public let Cache_ClearCache: String public let Common_Close: String public let ChangePhoneNumberCode_Called: String @@ -133,12 +129,12 @@ public final class PresentationStrings { public let TwoStepAuth_SetupPasswordConfirmPassword: String public let ChannelIntro_Text: String public let PrivacySettings_SecurityTitle: String + public let DialogList_SavedMessages: String private let _Login_SmsRequestState1: String private let _Login_SmsRequestState1_r: [(Int, NSRange)] - public func Login_SmsRequestState1(_ _0: Int) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_Login_SmsRequestState1, self._Login_SmsRequestState1_r, ["\(_0)"]) + public func Login_SmsRequestState1(_ _0: Int, _ _1: Int) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Login_SmsRequestState1, self._Login_SmsRequestState1_r, ["\(_0)", String(format: "%.2d", _1)]) } - public let Conversation_Download: String private let _Call_StatusOngoing: String private let _Call_StatusOngoing_r: [(Int, NSRange)] public func Call_StatusOngoing(_ _0: String) -> (String, [(Int, NSRange)]) { @@ -147,7 +143,7 @@ public final class PresentationStrings { public let Settings_LogoutConfirmationText: String public let BlockedUsers_Info: String public let ChatSettings_AutomaticAudioDownload: String - public let Map_OpenInFoursquare: String + public let Settings_SetUsername: String public let Privacy_Calls_CustomShareHelp: String public let Group_MessagePhotoUpdated: String public let Message_PinnedInvoice: String @@ -160,16 +156,13 @@ public final class PresentationStrings { return formatWithArgumentRanges(_CHAT_MESSAGE_TEXT, self._CHAT_MESSAGE_TEXT_r, [_1, _2, _3]) } public let Message_Sticker: String - public let Channel_Management_Remove: String - public let Channel_Username_Help: String public let Paint_Regular: String + public let Channel_Username_Help: String private let _Profile_CreateEncryptedChatOutdatedError: String private let _Profile_CreateEncryptedChatOutdatedError_r: [(Int, NSRange)] public func Profile_CreateEncryptedChatOutdatedError(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Profile_CreateEncryptedChatOutdatedError, self._Profile_CreateEncryptedChatOutdatedError_r, [_0, _1]) } - public let Login_InactiveHelp: String - public let ChatSettings_Security: String private let _PINNED_STICKER: String private let _PINNED_STICKER_r: [(Int, NSRange)] public func PINNED_STICKER(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { @@ -181,13 +174,14 @@ public final class PresentationStrings { public func Channel_AdminLog_MessageEdited(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Channel_AdminLog_MessageEdited, self._Channel_AdminLog_MessageEdited_r, [_0]) } + public let Group_Setup_HistoryHidden: String + public let Your_cards_expiration_year_is_invalid: String + public let AccessDenied_MicrophoneRestricted: String private let _PHONE_CALL_REQUEST: String private let _PHONE_CALL_REQUEST_r: [(Int, NSRange)] public func PHONE_CALL_REQUEST(_ _1: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_PHONE_CALL_REQUEST, self._PHONE_CALL_REQUEST_r, [_1]) } - public let AccessDenied_MicrophoneRestricted: String - public let Your_cards_expiration_year_is_invalid: String public let GroupInfo_InviteByLink: String private let _Notification_LeftChat: String private let _Notification_LeftChat_r: [(Int, NSRange)] @@ -201,6 +195,7 @@ public final class PresentationStrings { } public let PrivacyLastSeenSettings_NeverShareWith_Placeholder: String public let TwoStepAuth_SetupEmail: String + public let Checkout_PayWithFaceId: String public let Login_ResetAccountProtected_Reset: String public let SocksProxySetup_Hostname: String public let Channel_AdminLog_CanEditMessages: String @@ -209,11 +204,6 @@ public final class PresentationStrings { public func MESSAGE_CONTACT(_ _1: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_MESSAGE_CONTACT, self._MESSAGE_CONTACT_r, [_1]) } - private let _Group_Management_ErrorNotMember: String - private let _Group_Management_ErrorNotMember_r: [(Int, NSRange)] - public func Group_Management_ErrorNotMember(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_Group_Management_ErrorNotMember, self._Group_Management_ErrorNotMember_r, [_0]) - } public let MediaPicker_MomentsDateRangeSameMonthYearFormat: String public let Notification_MessageLifetime1w: String public let PasscodeSettings_AutoLock_IfAwayFor_5minutes: String @@ -232,28 +222,25 @@ public final class PresentationStrings { } public let Paint_Delete: String public let Channel_MessagePhotoUpdated: String - public let SharedMedia_All: String public let Cache_Help: String private let _Login_EmailPhoneBody: String private let _Login_EmailPhoneBody_r: [(Int, NSRange)] - public func Login_EmailPhoneBody(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_Login_EmailPhoneBody, self._Login_EmailPhoneBody_r, [_0]) + public func Login_EmailPhoneBody(_ _0: String, _ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Login_EmailPhoneBody, self._Login_EmailPhoneBody_r, [_0, _1, _2]) } public let Checkout_ShippingAddress: String public let Channel_BanList_RestrictedTitle: String public let Checkout_TotalAmount: String public let Conversation_MessageEditedLabel: String public let SharedMedia_EmptyLinksText: String - public let Channel_Members_Kick: String - public let GoogleDrive_FolderIsEmpty: String private let _Conversation_RestrictedTextTimed: String private let _Conversation_RestrictedTextTimed_r: [(Int, NSRange)] public func Conversation_RestrictedTextTimed(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Conversation_RestrictedTextTimed, self._Conversation_RestrictedTextTimed_r, [_0]) } public let Calls_NoCallsPlaceholder: String - public let Message_PinnedDeletedMessage: String public let Conversation_PinMessageAlert_OnlyPin: String + public let PasscodeSettings_UnlockWithFaceId: String public let ReportPeer_ReasonOther_Send: String public let Conversation_InstantPagePreview: String public let PasscodeSettings_SimplePasscodeHelp: String @@ -262,10 +249,8 @@ public final class PresentationStrings { public func Time_PreciseDate_m9(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Time_PreciseDate_m9, self._Time_PreciseDate_m9_r, [_1, _2, _3]) } - public let Group_ErrorAddTooMuch: String public let GroupInfo_Title: String public let State_Updating: String - public let StickerSettings_ContextShow: String public let Map_GetDirections: String private let _TwoStepAuth_PendingEmailHelp: String private let _TwoStepAuth_PendingEmailHelp_r: [(Int, NSRange)] @@ -274,23 +259,23 @@ public final class PresentationStrings { } public let UserInfo_PhoneCall: String public let MusicPlayer_VoiceNote: String - public let Channel_Username_InvalidTaken: String public let Paint_Duplicate: String - private let _Profile_ShareContactGroupFormat: String - private let _Profile_ShareContactGroupFormat_r: [(Int, NSRange)] - public func Profile_ShareContactGroupFormat(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_Profile_ShareContactGroupFormat, self._Profile_ShareContactGroupFormat_r, [_0]) - } + public let Channel_Username_InvalidTaken: String + public let Stickers_GroupStickersHelp: String public let SecretChat_Title: String public let Group_UpgradeConfirmation: String public let Checkout_LiabilityAlertTitle: String public let GroupInfo_GroupNamePlaceholder: String - public let Conversation_InfoBroadcastList: String private let _Time_PreciseDate_m11: String private let _Time_PreciseDate_m11_r: [(Int, NSRange)] public func Time_PreciseDate_m11(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Time_PreciseDate_m11, self._Time_PreciseDate_m11_r, [_1, _2, _3]) } + private let _MESSAGE_GEOLIVE: String + private let _MESSAGE_GEOLIVE_r: [(Int, NSRange)] + public func MESSAGE_GEOLIVE(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_MESSAGE_GEOLIVE, self._MESSAGE_GEOLIVE_r, [_1]) + } private let _Notification_JoinedGroupByLink: String private let _Notification_JoinedGroupByLink_r: [(Int, NSRange)] public func Notification_JoinedGroupByLink(_ _0: String) -> (String, [(Int, NSRange)]) { @@ -298,22 +283,19 @@ public final class PresentationStrings { } public let LoginPassword_Title: String public let Login_HaveNotReceivedCodeInternal: String - public let Conversation_PlayVideo: String public let PasscodeSettings_SimplePasscode: String - public let Conversation_MicrophoneAccessDisabled: String public let NewContact_Title: String public let Username_CheckingUsername: String - public let UserInfo_InviteBotToGroup: String public let Login_ResetAccountProtected_TimerTitle: String public let Checkout_Email: String public let CheckoutInfo_SaveInfo: String + public let UserInfo_InviteBotToGroup: String private let _ChangePhoneNumberCode_CallTimer: String private let _ChangePhoneNumberCode_CallTimer_r: [(Int, NSRange)] public func ChangePhoneNumberCode_CallTimer(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_ChangePhoneNumberCode_CallTimer, self._ChangePhoneNumberCode_CallTimer_r, [_0]) } public let TwoStepAuth_SetupPasswordEnterPasswordNew: String - public let Weekday_Wednesday: String private let _Channel_AdminLog_MessageToggleSignaturesOff: String private let _Channel_AdminLog_MessageToggleSignaturesOff_r: [(Int, NSRange)] public func Channel_AdminLog_MessageToggleSignaturesOff(_ _0: String) -> (String, [(Int, NSRange)]) { @@ -327,16 +309,17 @@ public final class PresentationStrings { public let Privacy_GroupsAndChannels_NeverAllow_Placeholder: String public let Message_Video: String public let Notification_ChannelInviterSelf: String - private let _VideoPreview_OptionSD: String - private let _VideoPreview_OptionSD_r: [(Int, NSRange)] - public func VideoPreview_OptionSD(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_VideoPreview_OptionSD, self._VideoPreview_OptionSD_r, [_0]) - } - public let Conversation_SecretLinkPreviewAlert: String public let Channel_AdminLog_BanEmbedLinks: String + public let Conversation_SecretLinkPreviewAlert: String + private let _CHANNEL_MESSAGE_GEOLIVE: String + private let _CHANNEL_MESSAGE_GEOLIVE_r: [(Int, NSRange)] + public func CHANNEL_MESSAGE_GEOLIVE(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_CHANNEL_MESSAGE_GEOLIVE, self._CHANNEL_MESSAGE_GEOLIVE_r, [_1]) + } public let Cache_Videos: String public let Call_ReportSkip: String public let NetworkUsageSettings_MediaImageDataSection: String + public let Group_Setup_HistoryTitle: String public let TwoStepAuth_GenericHelp: String private let _DialogList_SingleRecordingAudioSuffix: String private let _DialogList_SingleRecordingAudioSuffix_r: [(Int, NSRange)] @@ -350,31 +333,25 @@ public final class PresentationStrings { public func GroupInfo_AddParticipantConfirmation(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_GroupInfo_AddParticipantConfirmation, self._GroupInfo_AddParticipantConfirmation_r, [_0]) } + private let _Notification_PinnedLiveLocationMessage: String + private let _Notification_PinnedLiveLocationMessage_r: [(Int, NSRange)] + public func Notification_PinnedLiveLocationMessage(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Notification_PinnedLiveLocationMessage, self._Notification_PinnedLiveLocationMessage_r, [_0]) + } public let AccessDenied_PhotosRestricted: String public let Map_Locating: String - private let _SearchImages_Downloading_Kb: String - private let _SearchImages_Downloading_Kb_r: [(Int, NSRange)] - public func SearchImages_Downloading_Kb(_ _0: Int) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_SearchImages_Downloading_Kb, self._SearchImages_Downloading_Kb_r, ["\(_0)"]) - } - private let _Profile_ShareBotPersonFormat: String - private let _Profile_ShareBotPersonFormat_r: [(Int, NSRange)] - public func Profile_ShareBotPersonFormat(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_Profile_ShareBotPersonFormat, self._Profile_ShareBotPersonFormat_r, [_0]) - } - public let SearchImages_SearchImages: String public let SocksProxySetup_Title: String public let SharedMedia_EmptyMusicText: String public let Cache_ByPeerHeader: String public let Bot_GroupStatusReadsHistory: String public let TwoStepAuth_ResetAccountConfirmation: String public let CallSettings_Always: String - public let SearchImages_DownloadCancelled: String - public let Channel_BanUser_Unban: String public let Message_ImageExpired: String + public let Channel_BanUser_Unban: String + public let Stickers_GroupChooseStickerPack: String + public let Group_Setup_TypePrivate: String public let Settings_LogoutConfirmationTitle: String public let UserInfo_FirstNamePlaceholder: String - public let ChatSettings_AutoPlayAudio: String public let LoginPassword_ResetAccount: String public let Privacy_GroupsAndChannels_AlwaysAllow: String private let _Notification_JoinedChat: String @@ -392,6 +369,7 @@ public final class PresentationStrings { public func Channel_AdminLog_MessageToggleSignaturesOn(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Channel_AdminLog_MessageToggleSignaturesOn, self._Channel_AdminLog_MessageToggleSignaturesOn_r, [_0]) } + public let Map_PullUpForPlaces: String private let _Conversation_EncryptionWaiting: String private let _Conversation_EncryptionWaiting_r: [(Int, NSRange)] public func Conversation_EncryptionWaiting(_ _0: String) -> (String, [(Int, NSRange)]) { @@ -426,7 +404,7 @@ public final class PresentationStrings { public func Notification_PinnedRoundMessage(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Notification_PinnedRoundMessage, self._Notification_PinnedRoundMessage_r, [_0]) } - public let Conversation_DeleteGroup: String + public let Conversation_ViewMessage: String public let Settings_SaveEditedPhotos: String public let Channel_Management_LabelCreator: String private let _Notification_PinnedStickerMessage: String @@ -448,6 +426,8 @@ public final class PresentationStrings { public let CheckoutInfo_ReceiverInfoPhone: String public let SocksProxySetup_TypeNone: String public let GroupInfo_AddParticipantTitle: String + public let Map_LiveLocationShowAll: String + public let Settings_SavedMessages: String private let _CHANNEL_MESSAGE_TEXT: String private let _CHANNEL_MESSAGE_TEXT_r: [(Int, NSRange)] public func CHANNEL_MESSAGE_TEXT(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { @@ -455,7 +435,6 @@ public final class PresentationStrings { } public let Checkout_PayNone: String public let CheckoutInfo_ErrorNameInvalid: String - public let Channel_Share: String public let Notification_PaymentSent: String public let Settings_Username: String public let Notification_CallMissedShort: String @@ -470,18 +449,12 @@ public final class PresentationStrings { public let StickerPack_Share: String public let Watch_MessageView_Reply: String public let Call_AudioRouteSpeaker: String - public let PrivacySettings_DeleteAccountNever: String - private let _WelcomeScreen_ContactsAccessHelp: String - private let _WelcomeScreen_ContactsAccessHelp_r: [(Int, NSRange)] - public func WelcomeScreen_ContactsAccessHelp(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_WelcomeScreen_ContactsAccessHelp, self._WelcomeScreen_ContactsAccessHelp_r, [_0]) - } + public let Checkout_Title: String private let _MESSAGE_GEO: String private let _MESSAGE_GEO_r: [(Int, NSRange)] public func MESSAGE_GEO(_ _1: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_MESSAGE_GEO, self._MESSAGE_GEO_r, [_1]) } - public let Checkout_Title: String public let Privacy_Calls: String public let Channel_AdminLogFilter_EventsInfo: String private let _Channel_AdminLog_MessagePinned: String @@ -494,23 +467,21 @@ public final class PresentationStrings { public func Channel_AdminLog_MessageToggleInvitesOn(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Channel_AdminLog_MessageToggleInvitesOn, self._Channel_AdminLog_MessageToggleInvitesOn_r, [_0]) } - public let Conversation_SearchWebImages: String public let KeyCommand_ScrollDown: String public let Conversation_LinkDialogSave: String - public let Presence_offline: String - public let Conversation_SendMessageErrorFlood: String - private let _Conversation_ForwardToPersonFormat: String - private let _Conversation_ForwardToPersonFormat_r: [(Int, NSRange)] - public func Conversation_ForwardToPersonFormat(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_Conversation_ForwardToPersonFormat, self._Conversation_ForwardToPersonFormat_r, [_0]) - } public let CheckoutInfo_ErrorShippingNotAvailable: String - public let SharedMedia_Incoming: String + public let Conversation_SendMessageErrorFlood: String private let _Checkout_SavePasswordTimeoutAndTouchId: String private let _Checkout_SavePasswordTimeoutAndTouchId_r: [(Int, NSRange)] public func Checkout_SavePasswordTimeoutAndTouchId(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Checkout_SavePasswordTimeoutAndTouchId, self._Checkout_SavePasswordTimeoutAndTouchId_r, [_0]) } + public let HashtagSearch_AllChats: String + private let _Date_ChatDateHeaderYear: String + private let _Date_ChatDateHeaderYear_r: [(Int, NSRange)] + public func Date_ChatDateHeaderYear(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Date_ChatDateHeaderYear, self._Date_ChatDateHeaderYear_r, [_1, _2, _3]) + } public let CheckoutInfo_ShippingInfoCountry: String public let Map_ShowPlaces: String public let Camera_VideoMode: String @@ -522,13 +493,12 @@ public final class PresentationStrings { public let UserInfo_TelegramCall: String public let PrivacyLastSeenSettings_CustomShareSettingsHelp: String public let Channel_AdminLog_InfoPanelAlertText: String - public let Watch_State_WaitingForNetwork: String - public let Cache_Photos: String private let _Channel_AdminLog_MessageUnpinned: String private let _Channel_AdminLog_MessageUnpinned_r: [(Int, NSRange)] public func Channel_AdminLog_MessageUnpinned(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Channel_AdminLog_MessageUnpinned, self._Channel_AdminLog_MessageUnpinned_r, [_0]) } + public let Cache_Photos: String public let Message_PinnedStickerMessage: String public let PhotoEditor_QualityMedium: String public let Privacy_PaymentsClearInfo: String @@ -569,13 +539,11 @@ public final class PresentationStrings { public let Login_Code: String public let Channel_Username_InvalidCharacters: String public let FeatureDisabled_Oops: String - public let Login_InviteButton: String - public let ShareMenu_Send: String - public let Conversation_InfoGroup: String - public let WatchRemote_AlertTitle: String - public let Preview_ProfilePhotoTitle: String public let Calls_CallTabTitle: String + public let ShareMenu_Send: String + public let WatchRemote_AlertTitle: String public let Channel_Members_AddBannedErrorAdmin: String + public let Conversation_InfoGroup: String public let Checkout_Phone: String public let Channel_SignMessages_Help: String public let Calls_SubmitRating: String @@ -583,11 +551,8 @@ public final class PresentationStrings { public let Watch_MessageView_Forward: String public let GroupInfo_ActionPromote: String public let DialogList_You: String - public let Weekday_Monday: String - public let Watch_Suggestion_Yes: String public let AccessDenied_Camera: String public let WatchRemote_NotificationText: String - public let Activity_Location: String public let SharedMedia_ViewInChat: String public let Activity_RecordingAudio: String public let Watch_Stickers_StickerPacks: String @@ -597,25 +562,17 @@ public final class PresentationStrings { return formatWithArgumentRanges(_Target_ShareGameConfirmationPrivate, self._Target_ShareGameConfirmationPrivate_r, [_0]) } public let Checkout_NewCard_PostcodePlaceholder: String - public let Conversation_SearchImages: String public let DialogList_DeleteConversationConfirmation: String public let AttachmentMenu_SendAsFile: String - public let Message_GamePreviewLabel: String - public let Checkout_ShippingOption_Header: String public let Watch_Conversation_Unblock: String public let Channel_AdminLog_MessagePreviousLink: String - public let CallSettings_PrivacyDescription: String public let Conversation_ContextMenuCopy: String public let GroupInfo_UpgradeButton: String public let PrivacyLastSeenSettings_NeverShareWith: String public let ConvertToSupergroup_HelpText: String public let MediaPicker_VideoMuteDescription: String - private let _SearchImages_Downloading_Mb: String - private let _SearchImages_Downloading_Mb_r: [(Int, NSRange)] - public func SearchImages_Downloading_Mb(_ _0: Int) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_SearchImages_Downloading_Mb, self._SearchImages_Downloading_Mb_r, ["\(_0)"]) - } public let UserInfo_ShareMyContactInfo: String + public let Channel_Info_Stickers: String private let _FileSize_GB: String private let _FileSize_GB_r: [(Int, NSRange)] public func FileSize_GB(_ _0: String) -> (String, [(Int, NSRange)]) { @@ -636,6 +593,7 @@ public final class PresentationStrings { public let Contacts_InviteSearchLabel: String public let Tour_StartButton: String public let CheckoutInfo_Title: String + public let Conversation_Admin: String private let _Channel_AdminLog_MessageRestrictedNameUsername: String private let _Channel_AdminLog_MessageRestrictedNameUsername_r: [(Int, NSRange)] public func Channel_AdminLog_MessageRestrictedNameUsername(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { @@ -645,8 +603,6 @@ public final class PresentationStrings { public let Web_Error: String public let ShareFileTip_Title: String public let Username_InvalidStartsWithNumber: String - public let ChatSettings_RevertLanguage: String - public let Conversation_ReportSpamAndLeave: String private let _DialogList_EncryptedChatStartedIncoming: String private let _DialogList_EncryptedChatStartedIncoming_r: [(Int, NSRange)] public func DialogList_EncryptedChatStartedIncoming(_ _0: String) -> (String, [(Int, NSRange)]) { @@ -670,21 +626,22 @@ public final class PresentationStrings { } public let Month_GenFebruary: String public let Contacts_SelectAll: String + public let FastTwoStepSetup_EmailHelp: String public let Month_GenOctober: String public let CheckoutInfo_ErrorPhoneInvalid: String - public let SharedMedia_TitleVideo: String + public let Group_Setup_TypePublic: String public let Checkout_PaymentMethod_New: String public let ShareMenu_Comment: String public let Channel_Management_LabelEditor: String public let TwoStepAuth_SetPasswordHelp: String public let Channel_AdminLogFilter_EventsTitle: String + public let NotificationSettings_ContactJoined: String public let Username_LinkCopied: String private let _Time_MonthOfYear_m9: String private let _Time_MonthOfYear_m9_r: [(Int, NSRange)] public func Time_MonthOfYear_m9(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Time_MonthOfYear_m9, self._Time_MonthOfYear_m9_r, [_0]) } - public let DialogList_Conversations: String public let Channel_EditAdmin_PermissionAddAdmins: String public let Conversation_SendMessage: String public let Notification_CallIncoming: String @@ -693,8 +650,9 @@ public final class PresentationStrings { public func MESSAGE_FWDS(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_MESSAGE_FWDS, self._MESSAGE_FWDS_r, [_1, _2]) } - public let Conversation_InputTextCommentPlaceholder: String public let Map_OpenInYandexMaps: String + public let FastTwoStepSetup_PasswordHelp: String + public let GroupInfo_GroupHistoryHidden: String public let Month_ShortNovember: String public let AccessDenied_Settings: String public let EncryptionKey_Title: String @@ -704,13 +662,12 @@ public final class PresentationStrings { public func Map_DistanceAway(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Map_DistanceAway, self._Map_DistanceAway_r, [_0]) } - public let Compose_NewMessage: String public let Checkout_ErrorPaymentFailed: String + public let Compose_NewMessage: String + public let Conversation_LiveLocationYou: String public let Map_OpenInWaze: String - public let Common_ChooseVideo: String public let Checkout_ShippingMethod: String public let Login_InfoFirstNamePlaceholder: String - public let DialogList_Broadcast: String public let Checkout_ErrorProviderAccountInvalid: String public let CallSettings_TabIconDescription: String public let Checkout_WebConfirmation_Title: String @@ -720,7 +677,6 @@ public final class PresentationStrings { public let MessageTimer_Custom: String public let Conversation_SilentBroadcastTooltipOff: String public let Conversation_Mute: String - public let Call_CallBack: String public let CreateGroup_SoftUserLimitAlert: String public let AccessDenied_LocationDenied: String public let Tour_Title6: String @@ -734,14 +690,10 @@ public final class PresentationStrings { return formatWithArgumentRanges(_Channel_AdminLog_MessageDeleted, self._Channel_AdminLog_MessageDeleted_r, [_0]) } public let DialogList_DeleteBotConfirmation: String + public let EditProfile_Title: String + public let PasscodeSettings_HelpTop: String public let Common_TakePhotoOrVideo: String public let Notification_MessageLifetime2s: String - public let Conversation_FileGoogleDrive: String - private let _MediaPicker_Processing: String - private let _MediaPicker_Processing_r: [(Int, NSRange)] - public func MediaPicker_Processing(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_MediaPicker_Processing, self._MediaPicker_Processing_r, [_0]) - } public let Checkout_ErrorGeneric: String public let Channel_AdminLog_CanBanUsers: String public let Cache_Indexing: String @@ -751,12 +703,12 @@ public final class PresentationStrings { return formatWithArgumentRanges(_ENCRYPTION_REQUEST, self._ENCRYPTION_REQUEST_r, [_1]) } public let StickerSettings_ContextInfo: String - public let Message_SharedContact: String public let Channel_BanUser_PermissionEmbedLinks: String - public let Channel_Username_CreateCommentsEnabled: String + public let Map_Location: String public let GroupInfo_InviteLink_LinkSection: String public let Privacy_Calls_AlwaysAllow_Placeholder: String public let CheckoutInfo_ShippingInfoPostcode: String + public let Group_Setup_HistoryVisibleHelp: String private let _Time_PreciseDate_m7: String private let _Time_PreciseDate_m7_r: [(Int, NSRange)] public func Time_PreciseDate_m7(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { @@ -768,7 +720,6 @@ public final class PresentationStrings { public let Cache_KeepMedia: String public let WebPreview_GettingLinkInfo: String public let Group_Setup_TypePublicHelp: String - public let Channel_Moderator_AccessLevelModeratorHelp: String public let Map_Satellite: String public let Username_InvalidTaken: String private let _Notification_PinnedAudioMessage: String @@ -806,13 +757,14 @@ public final class PresentationStrings { public let Privacy_GroupsAndChannels_WhoCanAddMe: String public let Login_CodeExpiredError: String public let Settings_PhoneNumber: String + public let FastTwoStepSetup_EmailPlaceholder: String private let _DialogList_MultipleTypingSuffix: String private let _DialogList_MultipleTypingSuffix_r: [(Int, NSRange)] public func DialogList_MultipleTypingSuffix(_ _0: Int) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_DialogList_MultipleTypingSuffix, self._DialogList_MultipleTypingSuffix_r, ["\(_0)"]) } - public let ChannelMembers_Blacklist_EmptyText: String public let Bot_GenericBotStatus: String + public let PrivacySettings_PasscodeAndTouchId: String public let Common_edit: String public let Settings_AppLanguage: String public let PrivacyLastSeenSettings_WhoCanSeeMyTimestamp: String @@ -821,12 +773,10 @@ public final class PresentationStrings { public func Notification_Kicked(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Notification_Kicked, self._Notification_Kicked_r, [_0, _1]) } - public let Conversation_Send: String public let Channel_AdminLog_MessageRestrictedForever: String public let ChannelInfo_DeleteChannelConfirmation: String public let Weekday_ShortSaturday: String public let Map_SendThisLocation: String - public let DialogList_RecentTitleBots: String private let _Notification_PinnedDocumentMessage: String private let _Notification_PinnedDocumentMessage_r: [(Int, NSRange)] public func Notification_PinnedDocumentMessage(_ _0: String) -> (String, [(Int, NSRange)]) { @@ -837,16 +787,12 @@ public final class PresentationStrings { public let NetworkUsageSettings_Wifi: String public let Call_Accept: String public let GroupInfo_SetGroupPhotoDelete: String + public let Login_PhoneBannedError: String public let PhotoEditor_CropAuto: String public let PhotoEditor_ContrastTool: String - public let MediaPicker_MomentsDateYearFormat: String public let CheckoutInfo_ReceiverInfoNamePlaceholder: String public let Channel_AdminLog_MessagePreviousCaption: String public let Privacy_PaymentsClear_ShippingInfo: String - public let TwoStepAuth_GenericError: String - public let Channel_Moderator_AccessLevelEditorHelp: String - public let Compose_NewChannelButton: String - public let ConversationMedia_EmptyTitle: String public let Date_DialogDateFormat: String public let ReportPeer_ReasonSpam: String public let Privacy_Calls_P2P: String @@ -857,6 +803,7 @@ public final class PresentationStrings { return formatWithArgumentRanges(_PINNED_VIDEO, self._PINNED_VIDEO_r, [_1]) } public let StickerPacksSettings_Title: String + public let Privacy_PaymentsClearInfoDoneHelp: String public let Privacy_Calls_NeverAllow_Placeholder: String public let Settings_Support: String public let Notification_GroupInviterSelf: String @@ -866,14 +813,10 @@ public final class PresentationStrings { return formatWithArgumentRanges(_SecretImage_NotViewedYet, self._SecretImage_NotViewedYet_r, [_0]) } public let MaskStickerSettings_Title: String - public let Watch_Suggestion_ThankYou: String public let TwoStepAuth_SetPassword: String - public let GoogleDrive_LoadErrorMessage: String public let GroupInfo_InviteLink_ShareLink: String - public let ChannelMembers_AllMembersMayInviteOnHelp: String public let Common_Cancel: String public let UserInfo_About_Placeholder: String - public let Preview_LoadingImages: String public let ChangePhoneNumberCode_RequestingACall: String public let PrivacyLastSeenSettings_NeverShareWith_Title: String public let KeyCommand_JumpToNextChat: String @@ -883,7 +826,6 @@ public final class PresentationStrings { return formatWithArgumentRanges(_Time_MonthOfYear_m8, self._Time_MonthOfYear_m8_r, [_0]) } public let Tour_Text1: String - public let StickerPack_Remove: String public let Conversation_HoldForVideo: String public let Checkout_NewCard_Title: String public let Channel_TitleInfo: String @@ -901,8 +843,8 @@ public final class PresentationStrings { public func ENCRYPTION_ACCEPT(_ _1: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_ENCRYPTION_ACCEPT, self._ENCRYPTION_ACCEPT_r, [_1]) } - public let Conversation_ShareBotLocationConfirmationTitle: String public let NetworkUsageSettings_BytesSent: String + public let Conversation_ShareBotLocationConfirmationTitle: String public let Conversation_ForwardContacts: String private let _Notification_ChangedGroupName: String private let _Notification_ChangedGroupName_r: [(Int, NSRange)] @@ -931,20 +873,13 @@ public final class PresentationStrings { public let Watch_Compose_CreateMessage: String public let ChatSettings_ConnectionType_UseProxy: String public let Message_Audio: String - public let Notification_CreatedGroup: String public let Conversation_SearchNoResults: String - public let ChannelMembers_BanList_EmptyText: String public let ReportPeer_ReasonViolence: String public let Group_Username_RemoveExistingUsernamesInfo: String public let Message_InvoiceLabel: String - private let _LastSeen_AtWeekday: String - private let _LastSeen_AtWeekday_r: [(Int, NSRange)] - public func LastSeen_AtWeekday(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_LastSeen_AtWeekday, self._LastSeen_AtWeekday_r, [_0]) - } + public let Channel_AdminLogFilter_Title: String public let Contacts_SearchLabel: String public let Group_Username_InvalidStartsWithNumber: String - public let Channel_AdminLogFilter_Title: String public let ChatAdmins_AllMembersAreAdminsOnHelp: String public let Month_ShortSeptember: String public let Group_Username_CreatePublicLinkHelp: String @@ -953,24 +888,28 @@ public final class PresentationStrings { public let Bot_Unblock: String public let SharedMedia_CategoryMedia: String public let Conversation_HoldForAudio: String + public let Conversation_ClousStorageInfo_Description1: String public let Channel_Members_InviteLink: String public let Core_ServiceUserStatus: String public let WebSearch_RecentClearConfirmation: String - public let Conversation_ClousStorageInfo_Description1: String public let Notification_ChannelMigratedFrom: String public let Settings_Title: String public let Call_StatusBusy: String - public let ConversationMedia_Title: String public let ArchivedPacksAlert_Title: String + public let ConversationMedia_Title: String private let _Conversation_MessageViaUser: String private let _Conversation_MessageViaUser_r: [(Int, NSRange)] public func Conversation_MessageViaUser(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Conversation_MessageViaUser, self._Conversation_MessageViaUser_r, [_0]) } - public let Presence_invisible: String - public let DialogList_Create: String public let Tour_Title4: String public let Call_StatusEnded: String + public let LiveLocationUpdated_JustNow: String + private let _Login_BannedPhoneSubject: String + private let _Login_BannedPhoneSubject_r: [(Int, NSRange)] + public func Login_BannedPhoneSubject(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Login_BannedPhoneSubject, self._Login_BannedPhoneSubject_r, [_0]) + } private let _Channel_Management_RestrictedBy: String private let _Channel_Management_RestrictedBy_r: [(Int, NSRange)] public func Channel_Management_RestrictedBy(_ _0: String) -> (String, [(Int, NSRange)]) { @@ -1018,10 +957,7 @@ public final class PresentationStrings { } public let AttachmentMenu_SendAsFiles: String public let Profile_MessageLifetime1m: String - public let DialogList_SelectContact: String public let Settings_AppleWatch: String - public let Conversation_View: String - public let Contacts_Invite: String public let Channel_AdminLog_MessagePreviousDescription: String public let Your_card_was_declined: String public let PhoneNumberHelp_ChangeNumber: String @@ -1033,26 +969,29 @@ public final class PresentationStrings { public let Notifications_GroupNotificationsPreview: String public let Message_PinnedLocationMessage: String public let Settings_Logout: String + private let _UserInfo_BlockConfirmation: String + private let _UserInfo_BlockConfirmation_r: [(Int, NSRange)] + public func UserInfo_BlockConfirmation(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_UserInfo_BlockConfirmation, self._UserInfo_BlockConfirmation_r, [_0]) + } public let Profile_Username: String public let Group_Username_InvalidTooShort: String public let AuthSessions_TerminateOtherSessions: String public let PasscodeSettings_TryAgainIn1Minute: String public let Notifications_InAppNotifications: String - public let Channels_Title: String public let StickerPack_ViewPack: String public let EnterPasscode_ChangeTitle: String public let Call_Decline: String public let UserInfo_AddPhone: String - public let Web_CopyLink: String public let Activity_PlayingGame: String public let CheckoutInfo_ShippingInfoStatePlaceholder: String public let Notifications_MessageNotificationsSound: String public let Call_StatusWaiting: String public let Weekday_ShortWednesday: String - public let DC_UPDATE: String - public let PasscodeSettings_AutoLock_IfAwayFor_5hours: String public let Notifications_Title: String + public let PasscodeSettings_AutoLock_IfAwayFor_5hours: String public let Conversation_PinnedMessage: String + public let Channel_AdminLog_MessagePreviousMessage: String private let _Time_MonthOfYear_m12: String private let _Time_MonthOfYear_m12_r: [(Int, NSRange)] public func Time_MonthOfYear_m12(_ _0: String) -> (String, [(Int, NSRange)]) { @@ -1060,21 +999,19 @@ public final class PresentationStrings { } public let ConversationProfile_LeaveDeleteAndExit: String public let State_connecting: String - public let Channel_AdminLog_MessagePreviousMessage: String - public let WebPreview_LinkPreview: String public let Map_OpenInHereMaps: String + public let Stickers_FavoriteStickers: String public let CheckoutInfo_Pay: String - public let DialogList_Messages: String public let Login_CountryCode: String + public let PasscodeSettings_AutoLock_IfAwayFor_1hour: String public let CheckoutInfo_ShippingInfoState: String - public let Map_OpenInGooglePlus: String private let _CHAT_MESSAGE_AUDIO: String private let _CHAT_MESSAGE_AUDIO_r: [(Int, NSRange)] public func CHAT_MESSAGE_AUDIO(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_CHAT_MESSAGE_AUDIO, self._CHAT_MESSAGE_AUDIO_r, [_1, _2]) } - public let Preview_SaveToCameraRoll: String public let Login_SmsRequestState2: String + public let Preview_SaveToCameraRoll: String public let PasscodeSettings_ChangePasscode: String public let TwoStepAuth_RecoveryCodeInvalid: String private let _Message_PaymentSent: String @@ -1090,8 +1027,6 @@ public final class PresentationStrings { return formatWithArgumentRanges(_Conversation_RestrictedMediaTimed, self._Conversation_RestrictedMediaTimed_r, [_0]) } public let Login_InfoDeletePhoto: String - public let Group_Members_AddMemberErrorNotAllowed: String - public let Settings_SaveIncomingPhotosHelp: String public let TwoStepAuth_RecoveryCodeExpired: String public let TwoStepAuth_EmailTitle: String public let Privacy_GroupsAndChannels_NeverAllow: String @@ -1103,23 +1038,22 @@ public final class PresentationStrings { return formatWithArgumentRanges(_Time_MonthOfYear_m7, self._Time_MonthOfYear_m7_r, [_0]) } public let PhotoEditor_QualityLow: String - public let State_ConnectingToProxyInfo: String public let Paint_Outlined: String + public let State_ConnectingToProxyInfo: String public let Checkout_PasswordEntry_Title: String public let Common_Done: String public let PrivacySettings_LastSeenContacts: String public let CheckoutInfo_ShippingInfoAddress1: String public let UserInfo_LastNamePlaceholder: String - public let GroupInfo_InviteLink_RevokeAlert_Text: String public let Conversation_StatusKickedFromChannel: String + public let CheckoutInfo_ShippingInfoAddress2: String private let _DialogList_SingleTypingSuffix: String private let _DialogList_SingleTypingSuffix_r: [(Int, NSRange)] public func DialogList_SingleTypingSuffix(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_DialogList_SingleTypingSuffix, self._DialogList_SingleTypingSuffix_r, [_0]) } public let LastSeen_JustNow: String - public let CheckoutInfo_ShippingInfoAddress2: String - public let Watch_Suggestion_No: String + public let GroupInfo_InviteLink_RevokeAlert_Text: String public let BroadcastListInfo_AddRecipient: String private let _Channel_Management_ErrorNotMember: String private let _Channel_Management_ErrorNotMember_r: [(Int, NSRange)] @@ -1129,11 +1063,10 @@ public final class PresentationStrings { public let Privacy_Calls_NeverAllow: String public let Settings_About_Title: String public let PhoneNumberHelp_Help: String - public let Service_NetworkConfigurationUpdatedMessage: String public let Channel_LinkItem: String public let Camera_Retake: String - public let StickerPack_ShowStickers: String public let Conversation_RestrictedText: String + public let Channel_Stickers_YourStickers: String private let _CHAT_CREATED: String private let _CHAT_CREATED_r: [(Int, NSRange)] public func CHAT_CREATED(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { @@ -1145,7 +1078,6 @@ public final class PresentationStrings { public func PrivacySettings_LastSeenContactsPlus(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_PrivacySettings_LastSeenContactsPlus, self._PrivacySettings_LastSeenContactsPlus_r, [_0]) } - public let Conversation_FileHowTo: String public let ChangePhoneNumberNumber_NewNumber: String public let Compose_NewChannel: String public let Channel_AdminLog_CanChangeInviteLink: String @@ -1166,13 +1098,10 @@ public final class PresentationStrings { return formatWithArgumentRanges(_CancelResetAccount_TextSMS, self._CancelResetAccount_TextSMS_r, [_0]) } public let Channel_EditAdmin_PermissionInviteUsers: String - public let Conversation_Document: String - public let SearchImages_RetryDownload: String public let GroupInfo_DeleteAndExit: String public let GroupInfo_InviteLink_CopyLink: String - public let Weekday_Friday: String - public let Settings_SetProfilePhoto: String public let Login_ResetAccountProtected_Title: String + public let Settings_SetProfilePhoto: String public let Compose_ChannelTokenListPlaceholder: String public let Channel_EditAdmin_PermissionPinMessages: String public let Your_card_has_expired: String @@ -1190,15 +1119,20 @@ public final class PresentationStrings { return formatWithArgumentRanges(_Username_UsernameIsAvailable, self._Username_UsernameIsAvailable_r, [_0]) } public let KeyCommand_JumpToNextUnreadChat: String + private let _Date_ChatDateHeader: String + private let _Date_ChatDateHeader_r: [(Int, NSRange)] + public func Date_ChatDateHeader(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Date_ChatDateHeader, self._Date_ChatDateHeader_r, [_1, _2]) + } public let Conversation_EncryptedDescriptionTitle: String + public let DialogList_Pin: String private let _Notification_RemovedGroupPhoto: String private let _Notification_RemovedGroupPhoto_r: [(Int, NSRange)] public func Notification_RemovedGroupPhoto(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Notification_RemovedGroupPhoto, self._Notification_RemovedGroupPhoto_r, [_0]) } - public let GroupInfo_SharedMediaNone: String public let Channel_ErrorAddTooMuch: String - public let DialogList_Pin: String + public let GroupInfo_SharedMediaNone: String public let ChatSettings_TextSizeUnits: String public let ChatSettings_AutoPlayAnimations: String public let Conversation_FileOpenIn: String @@ -1208,11 +1142,10 @@ public final class PresentationStrings { public func ChangePhone_ErrorOccupied(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_ChangePhone_ErrorOccupied, self._ChangePhone_ErrorOccupied_r, [_0]) } - public let DialogList_RecentTitleGroups: String + public let Clipboard_SendPhoto: String public let Privacy_GroupsAndChannels_CustomShareHelp: String public let KeyCommand_ChatInfo: String public let Channel_AdminLog_EmptyFilterTitle: String - public let Notification_CreatedBroadcastList: String public let PhotoEditor_HighlightsTint: String public let Watch_Compose_AddContact: String private let _Time_PreciseDate_m5: String @@ -1242,15 +1175,9 @@ public final class PresentationStrings { return formatWithArgumentRanges(_Generic_OpenHiddenLinkAlert, self._Generic_OpenHiddenLinkAlert_r, [_0]) } public let Conversation_Contact: String - public let Service_ApplyLocalization: String public let NetworkUsageSettings_GeneralDataSection: String - private let _StickerPack_RemovePrompt: String - private let _StickerPack_RemovePrompt_r: [(Int, NSRange)] - public func StickerPack_RemovePrompt(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_StickerPack_RemovePrompt, self._StickerPack_RemovePrompt_r, [_0]) - } - public let Channel_NotificationCommentsDisabled: String public let EnterPasscode_RepeatNewPasscode: String + public let Conversation_ContextMenuCopyLink: String public let InstantPage_AutoNightTheme: String public let CloudStorage_Title: String public let Month_ShortOctober: String @@ -1262,34 +1189,30 @@ public final class PresentationStrings { public let Tour_Text6: String public let PhotoEditor_WarmthTool: String public let Common_TakePhoto: String - public let PhotoEditor_Current: String public let UserInfo_CreateNewContact: String + public let NetworkUsageSettings_MediaDocumentDataSection: String + public let Login_CodeSentCall: String public let Watch_PhotoView_Title: String private let _PrivacySettings_LastSeenContactsMinus: String private let _PrivacySettings_LastSeenContactsMinus_r: [(Int, NSRange)] public func PrivacySettings_LastSeenContactsMinus(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_PrivacySettings_LastSeenContactsMinus, self._PrivacySettings_LastSeenContactsMinus_r, [_0]) } - public let Login_InfoUpdatePhoto: String - public let Login_CodeSentCall: String public let ShareMenu_SelectChats: String - public let NetworkUsageSettings_MediaDocumentDataSection: String public let Group_ErrorSendRestrictedMedia: String + public let Group_Setup_HistoryVisible: String public let Channel_EditAdmin_PermissinAddAdminOff: String public let Cache_Files: String public let PhotoEditor_EnhanceTool: String public let Conversation_SearchPlaceholder: String - public let Calls_Search: String - public let BroadcastListInfo_Title: String + public let Channel_Stickers_NotFound: String public let WatchRemote_AlertText: String public let Channel_AdminLog_CanInviteUsers: String - public let Conversation_Block: String - public let AttachmentMenu_PhotoOrVideo: String public let Channel_BanUser_PermissionReadMessages: String + public let AttachmentMenu_PhotoOrVideo: String public let Month_ShortMarch: String public let GroupInfo_InviteLink_Title: String public let Watch_LastSeen_JustNow: String - public let BroadcastLists_Title: String public let PhoneLabel_Title: String public let PrivacySettings_Passcode: String public let Paint_ClearConfirm: String @@ -1310,11 +1233,6 @@ public final class PresentationStrings { } public let Login_PhoneAndCountryHelp: String public let CheckoutInfo_ReceiverInfoName: String - private let _LastSeen_TodayAt: String - private let _LastSeen_TodayAt_r: [(Int, NSRange)] - public func LastSeen_TodayAt(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_LastSeen_TodayAt, self._LastSeen_TodayAt_r, [_0]) - } private let _Time_YesterdayAt: String private let _Time_YesterdayAt_r: [(Int, NSRange)] public func Time_YesterdayAt(_ _0: String) -> (String, [(Int, NSRange)]) { @@ -1325,7 +1243,6 @@ public final class PresentationStrings { public let Embed_PlayingInPIP: String public let Localization_EnglishLanguageName: String public let Call_StatusIncoming: String - public let Conversation_Play: String public let Settings_PrivacySettings: String public let Conversation_SilentBroadcastTooltipOn: String private let _SecretVideo_NotViewedYet: String @@ -1343,7 +1260,6 @@ public final class PresentationStrings { public let Channel_AdminLog_BanSendMessages: String public let Channel_MessagePhotoRemoved: String public let Conversation_StatusKickedFromGroup: String - public let Compose_NewChannel_AddMemberHelp: String public let GroupInfo_ChatAdmins: String public let PhotoEditor_CurvesAll: String private let _Notification_LeftChannel: String @@ -1370,12 +1286,6 @@ public final class PresentationStrings { } public let Forward_ChannelReadOnly: String public let Privacy_GroupsAndChannels_NeverAllow_Title: String - public let Conversation_StatusGroupDeactivated: String - private let _CHAT_JOINED: String - private let _CHAT_JOINED_r: [(Int, NSRange)] - public func CHAT_JOINED(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_CHAT_JOINED, self._CHAT_JOINED_r, [_1, _2]) - } private let _Channel_AdminLog_MessageInvitedName: String private let _Channel_AdminLog_MessageInvitedName_r: [(Int, NSRange)] public func Channel_AdminLog_MessageInvitedName(_ _1: String) -> (String, [(Int, NSRange)]) { @@ -1383,13 +1293,12 @@ public final class PresentationStrings { } public let Conversation_Moderate_Ban: String public let Group_Status: String - public let Watch_Suggestion_Absolutely: String public let Conversation_InputTextPlaceholder: String - public let SharedMedia_TitleAudio: String public let TwoStepAuth_RecoveryCode: String public let SharedMedia_CategoryDocs: String public let Channel_AdminLog_CanChangeInfo: String public let Channel_AdminLogFilter_EventsAdmins: String + public let Group_Setup_HistoryHiddenHelp: String private let _AuthSessions_AppUnofficial: String private let _AuthSessions_AppUnofficial_r: [(Int, NSRange)] public func AuthSessions_AppUnofficial(_ _0: String) -> (String, [(Int, NSRange)]) { @@ -1422,7 +1331,6 @@ public final class PresentationStrings { public let Channel_Info_Members: String public let ShareFileTip_CloseTip: String public let KeyCommand_Find: String - public let Preview_VideoNotYetDownloaded: String public let SecretVideo_Title: String public let Checkout_NewCard_PostcodeTitle: String private let _Channel_AdminLog_MessageRestricted: String @@ -1432,20 +1340,20 @@ public final class PresentationStrings { } public let Channel_EditAdmin_PermissinAddAdminOn: String public let WebSearch_GIFs: String + public let Conversation_SavedMessages: String public let TwoStepAuth_EnterPasswordTitle: String private let _CHANNEL_MESSAGE_GAME: String private let _CHANNEL_MESSAGE_GAME_r: [(Int, NSRange)] public func CHANNEL_MESSAGE_GAME(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_CHANNEL_MESSAGE_GAME, self._CHANNEL_MESSAGE_GAME_r, [_1, _2]) } + public let Channel_Subscribers_Title: String public let AccessDenied_CallMicrophone: String public let Conversation_DeleteMessagesForEveryone: String public let UserInfo_TapToCall: String public let Common_Edit: String public let Conversation_OpenFile: String public let Message_PinnedDocumentMessage: String - public let Channel_ShareChannel: String - public let PrivacySettings_DeleteAccountNowConfirmation: String public let Checkout_TotalPaidAmount: String public let Conversation_UnsupportedMedia: String private let _Message_ForwardedMessage: String @@ -1466,33 +1374,36 @@ public final class PresentationStrings { public let Profile_CreateEncryptedChatError: String public let Map_LocationTitle: String public let Call_RateCall: String - public let Compose_Recipients: String public let Message_ReplyActionButtonShowReceipt: String public let PhotoEditor_ShadowsTool: String public let Checkout_NewCard_CardholderNamePlaceholder: String public let Cache_Title: String public let Month_GenMay: String + public let PasscodeSettings_HelpBottom: String private let _Notification_CreatedChat: String private let _Notification_CreatedChat_r: [(Int, NSRange)] public func Notification_CreatedChat(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Notification_CreatedChat, self._Notification_CreatedChat_r, [_0]) } public let Calls_NoMissedCallsPlacehoder: String + public let Channel_Stickers_NotFoundHelp: String public let Watch_UserInfo_Block: String public let Watch_LastSeen_ALongTimeAgo: String public let StickerPacksSettings_ManagingHelp: String public let Privacy_GroupsAndChannels_InviteToChannelMultipleError: String - public let PrivacySettings_TouchIdEnable: String public let SearchImages_Title: String public let Channel_BlackList_Title: String + private let _Conversation_LiveLocationYouAnd: String + private let _Conversation_LiveLocationYouAnd_r: [(Int, NSRange)] + public func Conversation_LiveLocationYouAnd(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Conversation_LiveLocationYouAnd, self._Conversation_LiveLocationYouAnd_r, [_0]) + } public let Checkout_NewCard_SaveInfo: String public let Notification_CallMissed: String public let Profile_ShareContactButton: String public let Group_ErrorSendRestrictedStickers: String public let Bot_GroupStatusDoesNotReadHistory: String public let Notification_Mute1h: String - public let Cache_ClearCacheAlert: String - public let BroadcastLists_NoListsYet: String public let Settings_TabTitle: String public let NetworkUsageSettings_MediaAudioDataSection: String public let GroupInfo_DeactivatedStatus: String @@ -1507,7 +1418,13 @@ public final class PresentationStrings { public func PrivacySettings_LastSeenEverybodyMinus(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_PrivacySettings_LastSeenEverybodyMinus, self._PrivacySettings_LastSeenEverybodyMinus_r, [_0]) } + public let Map_ShareLiveLocation: String public let Weekday_Today: String + private let _PINNED_GEOLIVE: String + private let _PINNED_GEOLIVE_r: [(Int, NSRange)] + public func PINNED_GEOLIVE(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_PINNED_GEOLIVE, self._PINNED_GEOLIVE_r, [_1]) + } private let _Conversation_RestrictedStickersTimed: String private let _Conversation_RestrictedStickersTimed_r: [(Int, NSRange)] public func Conversation_RestrictedStickersTimed(_ _0: String) -> (String, [(Int, NSRange)]) { @@ -1519,13 +1436,8 @@ public final class PresentationStrings { public func Notification_Joined(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Notification_Joined, self._Notification_Joined_r, [_0]) } - private let _VideoPreview_OptionHD: String - private let _VideoPreview_OptionHD_r: [(Int, NSRange)] - public func VideoPreview_OptionHD(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_VideoPreview_OptionHD, self._VideoPreview_OptionHD_r, [_0]) - } - public let TwoStepAuth_RecoveryFailed: String public let Paint_Clear: String + public let TwoStepAuth_RecoveryFailed: String private let _MESSAGE_AUDIO: String private let _MESSAGE_AUDIO_r: [(Int, NSRange)] public func MESSAGE_AUDIO(_ _1: String) -> (String, [(Int, NSRange)]) { @@ -1533,9 +1445,7 @@ public final class PresentationStrings { } public let Checkout_PasswordEntry_Pay: String public let Notifications_MessageNotificationsHelp: String - public let Notification_EncryptedChatRequested: String public let EnterPasscode_EnterCurrentPasscode: String - public let Channel_Management_LabelModerator: String private let _MESSAGE_GAME: String private let _MESSAGE_GAME_r: [(Int, NSRange)] public func MESSAGE_GAME(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { @@ -1543,6 +1453,7 @@ public final class PresentationStrings { } public let Conversation_Moderate_Report: String public let MessageTimer_Forever: String + public let DialogList_SavedMessagesHelp: String private let _Conversation_EncryptedPlaceholderTitleIncoming: String private let _Conversation_EncryptedPlaceholderTitleIncoming_r: [(Int, NSRange)] public func Conversation_EncryptedPlaceholderTitleIncoming(_ _0: String) -> (String, [(Int, NSRange)]) { @@ -1559,10 +1470,8 @@ public final class PresentationStrings { return formatWithArgumentRanges(_Call_ParticipantVersionOutdatedError, self._Call_ParticipantVersionOutdatedError_r, [_0]) } public let Tour_Text2: String - public let Preview_ViewStickerPack: String public let Call_StatusNoAnswer: String public let Conversation_MessageDialogDelete: String - public let Calls_Clear: String public let Username_Placeholder: String private let _Notification_PinnedDeletedMessage: String private let _Notification_PinnedDeletedMessage_r: [(Int, NSRange)] @@ -1575,21 +1484,16 @@ public final class PresentationStrings { return formatWithArgumentRanges(_Time_MonthOfYear_m11, self._Time_MonthOfYear_m11_r, [_0]) } public let UserInfo_BotHelp: String - public let Contacts_contact: String public let TwoStepAuth_PasswordSet: String - public let Channel_Moderator_AccessLevelEditor: String - public let EnterPasscode_TouchId: String private let _CHANNEL_MESSAGE_VIDEO: String private let _CHANNEL_MESSAGE_VIDEO_r: [(Int, NSRange)] public func CHANNEL_MESSAGE_VIDEO(_ _1: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_CHANNEL_MESSAGE_VIDEO, self._CHANNEL_MESSAGE_VIDEO_r, [_1]) } + public let EnterPasscode_TouchId: String public let Checkout_ErrorInvoiceAlreadyPaid: String public let ChatAdmins_Title: String - public let BroadcastLists_NoListsText: String public let ChannelMembers_WhoCanAddMembers: String - public let ChannelMembers_AllMembersMayInviteOffHelp: String - public let Conversation_InfoPrivate: String public let PasscodeSettings_Help: String public let Conversation_EditingMessagePanelTitle: String public let Settings_AboutEmpty: String @@ -1605,26 +1509,17 @@ public final class PresentationStrings { return formatWithArgumentRanges(_Notification_PinnedContactMessage, self._Notification_PinnedContactMessage_r, [_0]) } public let CallSettings_UseLessDataLongDescription: String + public let FastTwoStepSetup_PasswordPlaceholder: String public let Conversation_SecretChatContextBotAlert: String public let Channel_Moderator_AccessLevelRevoke: String public let CheckoutInfo_ReceiverInfoTitle: String public let Channel_AdminLogFilter_EventsRestrictions: String public let GroupInfo_InviteLink_RevokeLink: String - public let Conversation_Unmute: String public let Checkout_PaymentMethod_Title: String + public let Conversation_Unmute: String public let Notifications_MessageNotifications: String public let ChannelMembers_WhoCanAddMembersAdminsHelp: String public let DialogList_DeleteBotConversationConfirmation: String - private let _MediaPicker_AccessDeniedHelp: String - private let _MediaPicker_AccessDeniedHelp_r: [(Int, NSRange)] - public func MediaPicker_AccessDeniedHelp(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_MediaPicker_AccessDeniedHelp, self._MediaPicker_AccessDeniedHelp_r, [_0]) - } - private let _GroupInfo_InvitationLinkAccept: String - private let _GroupInfo_InvitationLinkAccept_r: [(Int, NSRange)] - public func GroupInfo_InvitationLinkAccept(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_GroupInfo_InvitationLinkAccept, self._GroupInfo_InvitationLinkAccept_r, [_0]) - } public let Conversation_ClousStorageInfo_Description2: String private let _Time_MonthOfYear_m5: String private let _Time_MonthOfYear_m5_r: [(Int, NSRange)] @@ -1633,6 +1528,7 @@ public final class PresentationStrings { } public let Map_Hybrid: String public let Channel_Setup_Title: String + public let MediaPicker_TimerTooltip: String public let Activity_UploadingVideo: String public let Channel_Info_Management: String private let _Notification_MessageLifetimeChangedOutgoing: String @@ -1640,22 +1536,18 @@ public final class PresentationStrings { public func Notification_MessageLifetimeChangedOutgoing(_ _1: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Notification_MessageLifetimeChangedOutgoing, self._Notification_MessageLifetimeChangedOutgoing_r, [_1]) } - public let Conversation_DeleteOneMessage: String public let PhotoEditor_QualityVeryLow: String + public let Stickers_AddToFavorites: String public let Month_ShortFebruary: String - public let Compose_NewBroadcast: String public let Conversation_ForwardTitle: String public let Settings_FAQ_URL: String - public let TwoStepAuth_ConfirmationChangeEmail: String public let Activity_RecordingVideoMessage: String - public let WelcomeScreen_ContactsAccessSettings: String public let SharedMedia_EmptyFilesText: String private let _Contacts_AccessDeniedHelpLandscape: String private let _Contacts_AccessDeniedHelpLandscape_r: [(Int, NSRange)] public func Contacts_AccessDeniedHelpLandscape(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Contacts_AccessDeniedHelpLandscape, self._Contacts_AccessDeniedHelpLandscape_r, [_0]) } - public let Channel_NotificationCommentsEnabled: String public let PasscodeSettings_UnlockWithTouchId: String public let Contacts_AccessDeniedHelpON: String public let NetworkUsageSettings_ResetStats: String @@ -1672,6 +1564,12 @@ public final class PresentationStrings { } public let SocksProxySetup_TypeSocks: String public let Profile_MessageLifetimeForever: String + public let MediaPicker_UngroupDescription: String + private let _Checkout_SavePasswordTimeoutAndFaceId: String + private let _Checkout_SavePasswordTimeoutAndFaceId_r: [(Int, NSRange)] + public func Checkout_SavePasswordTimeoutAndFaceId(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Checkout_SavePasswordTimeoutAndFaceId, self._Checkout_SavePasswordTimeoutAndFaceId_r, [_0]) + } public let SocksProxySetup_Username: String public let Conversation_Edit: String public let TwoStepAuth_ResetAccountHelp: String @@ -1684,7 +1582,6 @@ public final class PresentationStrings { public let Channel_ErrorAddBlocked: String public let Conversation_Unpin: String public let Call_RecordingDisabledMessage: String - public let Conversation_Stop: String public let Conversation_UnblockUser: String public let Conversation_Unblock: String private let _CHANNEL_MESSAGE_GIF: String @@ -1700,23 +1597,19 @@ public final class PresentationStrings { public let Profile_MessageLifetime1w: String public let DialogList_TabTitle: String public let UserInfo_GenericPhoneLabel: String - public let MediaPicker_MomentsDateFormat: String - private let _Conversation_DownloadKilobytes: String - private let _Conversation_DownloadKilobytes_r: [(Int, NSRange)] - public func Conversation_DownloadKilobytes(_ _0: Int) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_Conversation_DownloadKilobytes, self._Conversation_DownloadKilobytes_r, ["\(_0)"]) - } private let _Channel_AdminLog_MessagePromotedName: String private let _Channel_AdminLog_MessagePromotedName_r: [(Int, NSRange)] public func Channel_AdminLog_MessagePromotedName(_ _1: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Channel_AdminLog_MessagePromotedName, self._Channel_AdminLog_MessagePromotedName_r, [_1]) } + public let Group_Members_AddMemberBotErrorNotAllowed: String private let _Username_LinkHint: String private let _Username_LinkHint_r: [(Int, NSRange)] public func Username_LinkHint(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Username_LinkHint, self._Username_LinkHint_r, [_0]) } - public let Group_Members_AddMemberBotErrorNotAllowed: String + public let Map_StopLiveLocation: String + public let Message_LiveLocation: String public let NetworkUsageSettings_Title: String public let CheckoutInfo_ShippingInfoPostcodePlaceholder: String public let Wallpaper_Wallpaper: String @@ -1727,27 +1620,26 @@ public final class PresentationStrings { public func Channel_AdminLog_MessageRestrictedName(_ _1: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Channel_AdminLog_MessageRestrictedName, self._Channel_AdminLog_MessageRestrictedName_r, [_1]) } - public let Channel_JoinChannel: String - public let AccessDenied_LocationDisabled: String - public let Group_ErrorNotMutualContact: String - public let Conversation_DownloadPhoto: String - public let Presence_online: String - public let Login_UnknownError: String - public let DialogList_Title: String - public let SearchImages_NoImagesFound: String - private let _Notification_RemovedUserPhoto: String - private let _Notification_RemovedUserPhoto_r: [(Int, NSRange)] - public func Notification_RemovedUserPhoto(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_Notification_RemovedUserPhoto, self._Notification_RemovedUserPhoto_r, [_0]) + private let _Channel_AdminLog_MessageGroupPreHistoryHidden: String + private let _Channel_AdminLog_MessageGroupPreHistoryHidden_r: [(Int, NSRange)] + public func Channel_AdminLog_MessageGroupPreHistoryHidden(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Channel_AdminLog_MessageGroupPreHistoryHidden, self._Channel_AdminLog_MessageGroupPreHistoryHidden_r, [_0]) } + public let Channel_JoinChannel: String + public let StickerPack_Add: String + public let Group_ErrorNotMutualContact: String + public let AccessDenied_LocationDisabled: String + public let Login_UnknownError: String + public let Presence_online: String + public let DialogList_Title: String public let Stickers_Install: String + public let SearchImages_NoImagesFound: String private let _Watch_Time_ShortTodayAt: String private let _Watch_Time_ShortTodayAt_r: [(Int, NSRange)] public func Watch_Time_ShortTodayAt(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Watch_Time_ShortTodayAt, self._Watch_Time_ShortTodayAt_r, [_0]) } - public let StickerPack_Add: String - public let ChatSettings_Language: String + public let UserInfo_GroupsInCommon: String public let Message_PinnedContactMessage: String public let AccessDenied_CameraDisabled: String private let _Time_PreciseDate_m3: String @@ -1755,23 +1647,28 @@ public final class PresentationStrings { public func Time_PreciseDate_m3(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Time_PreciseDate_m3, self._Time_PreciseDate_m3_r, [_1, _2, _3]) } - public let UserInfo_GroupsInCommon: String - public let UserInfo_Call: String - public let Conversation_InputTextDisabledPlaceholder: String - public let Map_ForwardViaTelegram: String + private let _LiveLocationUpdated_YesterdayAt: String + private let _LiveLocationUpdated_YesterdayAt_r: [(Int, NSRange)] + public func LiveLocationUpdated_YesterdayAt(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_LiveLocationUpdated_YesterdayAt, self._LiveLocationUpdated_YesterdayAt_r, [_0]) + } public let Month_GenMarch: String public let Watch_UserInfo_Unmute: String - public let PhotoEditor_BlurTool: String + public let CheckoutInfo_ErrorPostcodeInvalid: String public let Common_Delete: String public let Username_Title: String public let Login_PhoneFloodError: String - public let CheckoutInfo_ErrorPostcodeInvalid: String + public let Channel_AdminLog_InfoPanelTitle: String private let _CHANNEL_MESSAGE_PHOTO: String private let _CHANNEL_MESSAGE_PHOTO_r: [(Int, NSRange)] public func CHANNEL_MESSAGE_PHOTO(_ _1: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_CHANNEL_MESSAGE_PHOTO, self._CHANNEL_MESSAGE_PHOTO_r, [_1]) } - public let Channel_AdminLog_InfoPanelTitle: String + private let _Channel_AdminLog_MessageToggleInvitesOff: String + private let _Channel_AdminLog_MessageToggleInvitesOff_r: [(Int, NSRange)] + public func Channel_AdminLog_MessageToggleInvitesOff(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Channel_AdminLog_MessageToggleInvitesOff, self._Channel_AdminLog_MessageToggleInvitesOff_r, [_0]) + } public let Group_ErrorAddTooMuchBots: String private let _Notification_CallFormat: String private let _Notification_CallFormat_r: [(Int, NSRange)] @@ -1783,10 +1680,10 @@ public final class PresentationStrings { public func CHAT_MESSAGE_PHOTO(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_CHAT_MESSAGE_PHOTO, self._CHAT_MESSAGE_PHOTO_r, [_1, _2]) } - private let _Channel_AdminLog_MessageToggleInvitesOff: String - private let _Channel_AdminLog_MessageToggleInvitesOff_r: [(Int, NSRange)] - public func Channel_AdminLog_MessageToggleInvitesOff(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_Channel_AdminLog_MessageToggleInvitesOff, self._Channel_AdminLog_MessageToggleInvitesOff_r, [_0]) + private let _UserInfo_UnblockConfirmation: String + private let _UserInfo_UnblockConfirmation_r: [(Int, NSRange)] + public func UserInfo_UnblockConfirmation(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_UserInfo_UnblockConfirmation, self._UserInfo_UnblockConfirmation_r, [_0]) } public let UserInfo_ShareBot: String public let TwoStepAuth_EmailSkip: String @@ -1797,8 +1694,6 @@ public final class PresentationStrings { public let Camera_FlashAuto: String public let Call_ConnectionErrorMessage: String public let Stickers_FrequentlyUsed: String - public let Compose_NewChannel_AddMember: String - public let Watch_State_Updating: String public let LastSeen_ALongTimeAgo: String public let DialogList_SearchSectionGlobal: String public let ChangePhoneNumberNumber_NumberPlaceholder: String @@ -1806,13 +1701,17 @@ public final class PresentationStrings { public let GroupInfo_GroupType: String public let Watch_Suggestion_OnMyWay: String public let Checkout_NewCard_PaymentCard: String + private let _DialogList_SearchSubtitleFormat: String + private let _DialogList_SearchSubtitleFormat_r: [(Int, NSRange)] + public func DialogList_SearchSubtitleFormat(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_DialogList_SearchSubtitleFormat, self._DialogList_SearchSubtitleFormat_r, [_1, _2]) + } public let PhotoEditor_CropAspectRatioOriginal: String private let _Conversation_RestrictedInlineTimed: String private let _Conversation_RestrictedInlineTimed_r: [(Int, NSRange)] public func Conversation_RestrictedInlineTimed(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Conversation_RestrictedInlineTimed, self._Conversation_RestrictedInlineTimed_r, [_0]) } - public let MediaPicker_MomentsDateRangeFormat: String public let UserInfo_NotificationsDisabled: String private let _CONTACT_JOINED: String private let _CONTACT_JOINED_r: [(Int, NSRange)] @@ -1820,8 +1719,14 @@ public final class PresentationStrings { return formatWithArgumentRanges(_CONTACT_JOINED, self._CONTACT_JOINED_r, [_1]) } public let PrivacyLastSeenSettings_AlwaysShareWith_Title: String + private let _Channel_AdminLog_MessageGroupPreHistoryVisible: String + private let _Channel_AdminLog_MessageGroupPreHistoryVisible_r: [(Int, NSRange)] + public func Channel_AdminLog_MessageGroupPreHistoryVisible(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Channel_AdminLog_MessageGroupPreHistoryVisible, self._Channel_AdminLog_MessageGroupPreHistoryVisible_r, [_0]) + } public let BlockedUsers_LeavePrefix: String public let NetworkUsageSettings_ResetStatsConfirmation: String + public let Group_Setup_HistoryHeader: String public let Channel_EditAdmin_PermissionPostMessages: String private let _Contacts_AddPhoneNumber: String private let _Contacts_AddPhoneNumber_r: [(Int, NSRange)] @@ -1834,11 +1739,12 @@ public final class PresentationStrings { return formatWithArgumentRanges(_MESSAGE_SCREENSHOT, self._MESSAGE_SCREENSHOT_r, [_1]) } public let DialogList_EncryptionProcessing: String + public let GroupInfo_GroupHistory: String public let Conversation_ApplyLocalization: String + public let FastTwoStepSetup_Title: String public let Conversation_DeleteManyMessages: String public let CancelResetAccount_Title: String public let Notification_CallOutgoingShort: String - public let Channel_Moderator_AccessLevelHeader: String public let SharedMedia_TitleAll: String public let Conversation_SlideToCancel: String public let AuthSessions_TerminateSession: String @@ -1861,10 +1767,15 @@ public final class PresentationStrings { return formatWithArgumentRanges(_Conversation_Moderate_DeleteAllMessages, self._Conversation_Moderate_DeleteAllMessages_r, [_0]) } public let SharedMedia_CategoryOther: String - public let GoogleDrive_LogoutMessage: String + public let DialogList_SavedMessagesTooltip: String public let Preview_DeletePhoto: String - public let PasscodeSettings_TurnPasscodeOn: String public let GroupInfo_ChannelListNamePlaceholder: String + public let PasscodeSettings_TurnPasscodeOn: String + private let _Channel_AdminLog_MessageChangedGroupStickerPack: String + private let _Channel_AdminLog_MessageChangedGroupStickerPack_r: [(Int, NSRange)] + public func Channel_AdminLog_MessageChangedGroupStickerPack(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Channel_AdminLog_MessageChangedGroupStickerPack, self._Channel_AdminLog_MessageChangedGroupStickerPack_r, [_0]) + } public let DialogList_Unpin: String public let GroupInfo_SetGroupPhoto: String public let StickerPacksSettings_ArchivedPacks_Info: String @@ -1874,14 +1785,18 @@ public final class PresentationStrings { public func CHAT_MESSAGE_NOTEXT(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_CHAT_MESSAGE_NOTEXT, self._CHAT_MESSAGE_NOTEXT_r, [_1, _2]) } + public let Notification_CallCanceledShort: String public let Channel_Setup_TypeHeader: String private let _Notification_NewAuthDetected: String private let _Notification_NewAuthDetected_r: [(Int, NSRange)] public func Notification_NewAuthDetected(_ _1: String, _ _2: String, _ _3: String, _ _4: String, _ _5: String, _ _6: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Notification_NewAuthDetected, self._Notification_NewAuthDetected_r, [_1, _2, _3, _4, _5, _6]) } - public let Notification_CallCanceledShort: String - public let PhotoEditor_RevertMessage: String + private let _Channel_AdminLog_MessageRemovedGroupStickerPack: String + private let _Channel_AdminLog_MessageRemovedGroupStickerPack_r: [(Int, NSRange)] + public func Channel_AdminLog_MessageRemovedGroupStickerPack(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Channel_AdminLog_MessageRemovedGroupStickerPack, self._Channel_AdminLog_MessageRemovedGroupStickerPack_r, [_0]) + } public let AccessDenied_VideoMessageCamera: String public let Conversation_Search: String private let _Channel_Management_PromotedBy: String @@ -1900,7 +1815,6 @@ public final class PresentationStrings { return formatWithArgumentRanges(_Time_MonthOfYear_m4, self._Time_MonthOfYear_m4_r, [_0]) } public let SecretImage_Title: String - public let Preview_ForwardViaTelegram: String public let Notifications_InAppNotificationsSounds: String public let Call_StatusRequesting: String private let _Channel_AdminLog_MessageRestrictedUntil: String @@ -1920,6 +1834,7 @@ public final class PresentationStrings { public func Channel_AdminLog_MessageChangedChannelAbout(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Channel_AdminLog_MessageChangedChannelAbout, self._Channel_AdminLog_MessageChangedChannelAbout_r, [_0]) } + public let Channel_Stickers_CreateYourOwn: String private let _Call_EmojiDescription: String private let _Call_EmojiDescription_r: [(Int, NSRange)] public func Call_EmojiDescription(_ _0: String) -> (String, [(Int, NSRange)]) { @@ -1944,14 +1859,7 @@ public final class PresentationStrings { return formatWithArgumentRanges(_Notification_MessageLifetimeChanged, self._Notification_MessageLifetimeChanged_r, [_1, _2]) } public let Message_Contact: String - private let _Watch_LastSeen_TodayAt: String - private let _Watch_LastSeen_TodayAt_r: [(Int, NSRange)] - public func Watch_LastSeen_TodayAt(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_Watch_LastSeen_TodayAt, self._Watch_LastSeen_TodayAt_r, [_0]) - } - public let Channel_Moderator_AccessLevelModerator: String - public let GoogleDrive_Logout: String - public let PhotoEditor_RevertToOriginal: String + public let PasscodeSettings_AutoLock_IfAwayFor_1minute: String public let Common_More: String public let Preview_OpenInInstagram: String public let PhotoEditor_HighlightsTool: String @@ -1967,20 +1875,20 @@ public final class PresentationStrings { } public let Invite_LargeRecipientsCountWarning: String public let GroupInfo_BroadcastListNamePlaceholder: String + public let Activity_UploadingVideoMessage: String public let Conversation_ShareBotContactConfirmation: String - public let GroupInfo_ActionBan: String public let Login_CodeSentSms: String public let Conversation_ReportSpamConfirmation: String public let ChannelMembers_ChannelAdminsTitle: String public let SocksProxySetup_Credentials: String public let CallSettings_UseLessData: String + public let MediaPicker_GroupDescription: String private let _TwoStepAuth_EnterPasswordHint: String private let _TwoStepAuth_EnterPasswordHint_r: [(Int, NSRange)] public func TwoStepAuth_EnterPasswordHint(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_TwoStepAuth_EnterPasswordHint, self._TwoStepAuth_EnterPasswordHint_r, [_0]) } public let CallSettings_TabIcon: String - public let Conversation_EditForward: String public let ConversationProfile_UnknownAddMemberError: String private let _Conversation_FileHowToText: String private let _Conversation_FileHowToText_r: [(Int, NSRange)] @@ -1988,11 +1896,7 @@ public final class PresentationStrings { return formatWithArgumentRanges(_Conversation_FileHowToText, self._Conversation_FileHowToText_r, [_0]) } public let Channel_AdminLog_BanSendMedia: String - public let Tour_Text7: String - public let Contacts_contactsvar: String public let Watch_UserInfo_Unblock: String - public let Conversation_EditDelete: String - public let Conversation_ViewPhoto: String public let StickerPacksSettings_ArchivedMasks: String public let Message_Animation: String public let Checkout_PaymentMethod: String @@ -2001,13 +1905,8 @@ public final class PresentationStrings { public let Cache_Music: String private let _Login_CallRequestState1: String private let _Login_CallRequestState1_r: [(Int, NSRange)] - public func Login_CallRequestState1(_ _0: Int) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_Login_CallRequestState1, self._Login_CallRequestState1_r, ["\(_0)"]) - } - private let _SearchImages_ImageNofM: String - private let _SearchImages_ImageNofM_r: [(Int, NSRange)] - public func SearchImages_ImageNofM(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_SearchImages_ImageNofM, self._SearchImages_ImageNofM_r, [_1, _2]) + public func Login_CallRequestState1(_ _0: Int, _ _1: Int) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Login_CallRequestState1, self._Login_CallRequestState1_r, ["\(_0)", String(format: "%.2d", _1)]) } public let Channel_Username_CreatePrivateLinkHelp: String private let _Time_PreciseDate_m2: String @@ -2020,16 +1919,14 @@ public final class PresentationStrings { public func FileSize_B(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_FileSize_B, self._FileSize_B_r, [_0]) } - public let PhotoEditor_SaturationTool: String - public let ImagePicker_NoPhotos: String private let _Target_ShareGameConfirmationGroup: String private let _Target_ShareGameConfirmationGroup_r: [(Int, NSRange)] public func Target_ShareGameConfirmationGroup(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Target_ShareGameConfirmationGroup, self._Target_ShareGameConfirmationGroup_r, [_0]) } - public let Call_StatusConnecting: String + public let PhotoEditor_SaturationTool: String public let Channel_BanUser_BlockFor: String - public let Preview_DeleteVideo: String + public let Call_StatusConnecting: String public let Bot_Start: String private let _Channel_AdminLog_MessageChangedGroupAbout: String private let _Channel_AdminLog_MessageChangedGroupAbout_r: [(Int, NSRange)] @@ -2037,7 +1934,7 @@ public final class PresentationStrings { return formatWithArgumentRanges(_Channel_AdminLog_MessageChangedGroupAbout, self._Channel_AdminLog_MessageChangedGroupAbout_r, [_0]) } public let Notifications_TextTone: String - public let DialogList_Draft: String + public let Settings_CallSettings: String private let _Watch_Time_ShortYesterdayAt: String private let _Watch_Time_ShortYesterdayAt_r: [(Int, NSRange)] public func Watch_Time_ShortYesterdayAt(_ _0: String) -> (String, [(Int, NSRange)]) { @@ -2049,27 +1946,15 @@ public final class PresentationStrings { public func PINNED_DOC(_ _1: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_PINNED_DOC, self._PINNED_DOC_r, [_1]) } - private let _ConversationProfile_UserLeftChatError: String - private let _ConversationProfile_UserLeftChatError_r: [(Int, NSRange)] - public func ConversationProfile_UserLeftChatError(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_ConversationProfile_UserLeftChatError, self._ConversationProfile_UserLeftChatError_r, [_0]) - } public let ChatSettings_PrivateChats: String - public let Settings_CallSettings: String + public let DialogList_Draft: String public let Channel_EditAdmin_PermissionDeleteMessages: String - public let Conversation_CloudStorageInfo_Title: String public let Channel_BanUser_PermissionSendStickersAndGifs: String - public let Channel_AdminLog_Status: String + public let Conversation_CloudStorageInfo_Title: String public let Notification_RenamedChannel: String public let BlockedUsers_BlockUser: String public let ChatSettings_TextSize: String - public let MediaPicker_AccessDeniedError: String public let ChannelInfo_DeleteGroup: String - private let _BlockedUsers_BlockFormat: String - private let _BlockedUsers_BlockFormat_r: [(Int, NSRange)] - public func BlockedUsers_BlockFormat(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_BlockedUsers_BlockFormat, self._BlockedUsers_BlockFormat_r, [_0]) - } public let PhoneNumberHelp_Alert: String private let _PINNED_TEXT: String private let _PINNED_TEXT_r: [(Int, NSRange)] @@ -2079,7 +1964,6 @@ public final class PresentationStrings { public let Watch_ChannelInfo_Title: String public let WebSearch_RecentSectionClear: String public let Channel_AdminLogFilter_AdminsAll: String - public let StickerPack_AddStickers: String public let Channel_Setup_TypePrivate: String public let PhotoEditor_TintTool: String public let Watch_Suggestion_CantTalk: String @@ -2090,10 +1974,8 @@ public final class PresentationStrings { return formatWithArgumentRanges(_CHAT_MESSAGE_STICKER, self._CHAT_MESSAGE_STICKER_r, [_1, _2, _3]) } public let Map_ChooseAPlace: String - public let Tour_Title7: String + public let Map_ShareLiveLocationHelp: String public let Watch_Bot_Restart: String - public let StickerPack_ShareStickers: String - public let ChannelMembers_AllMembersMayInvite: String public let Channel_About_Help: String public let Web_OpenExternal: String public let UserInfo_AddContact: String @@ -2107,13 +1989,11 @@ public final class PresentationStrings { public func Call_StatusBar(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Call_StatusBar, self._Call_StatusBar_r, [_0]) } + public let EditProfile_NameAndPhotoHelp: String public let Month_ShortJuly: String - public let Watch_MessageView_ViewOnPhone: String public let CheckoutInfo_ShippingInfoAddress1Placeholder: String - public let Stickers_Favorited: String + public let Watch_MessageView_ViewOnPhone: String public let CallSettings_Never: String - public let DialogList_SelectContacts: String - public let Conversation_DownloadProgressMegabytes: String public let TwoStepAuth_EmailSent: String private let _Notification_PinnedAnimationMessage: String private let _Notification_PinnedAnimationMessage_r: [(Int, NSRange)] @@ -2123,13 +2003,14 @@ public final class PresentationStrings { public let TwoStepAuth_RecoveryTitle: String public let WatchRemote_AlertOpen: String public let ExplicitContent_AlertChannel: String + public let Widget_AuthRequired: String private let _ForwardedAuthors2: String private let _ForwardedAuthors2_r: [(Int, NSRange)] public func ForwardedAuthors2(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_ForwardedAuthors2, self._ForwardedAuthors2_r, [_0, _1]) } - public let TwoStepAuth_ConfirmationText: String public let ChannelInfo_DeleteGroupConfirmation: String + public let TwoStepAuth_ConfirmationText: String public let Login_SmsRequestState3: String public let Notifications_AlertTones: String private let _Time_MonthOfYear_m10: String @@ -2138,39 +2019,34 @@ public final class PresentationStrings { return formatWithArgumentRanges(_Time_MonthOfYear_m10, self._Time_MonthOfYear_m10_r, [_0]) } public let Login_InfoAvatarPhoto: String - public let Widget_AuthRequired: String public let Calls_TabTitle: String - public let Contacts_MemberSearchSectionTitleChannel: String + public let Map_YouAreHere: String public let PhotoEditor_CurvesTool: String - public let Preview_LoadingVideo: String - public let State_updating: String + public let Map_LiveLocationFor1Hour: String private let _Notification_JoinedChannel: String private let _Notification_JoinedChannel_r: [(Int, NSRange)] public func Notification_JoinedChannel(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Notification_JoinedChannel, self._Notification_JoinedChannel_r, [_0]) } - public let TwoStepAuth_ResetAccount: String public let GroupInfo_ActionRestrict: String public let Checkout_ShippingOption_Title: String - public let Weekday_Tuesday: String - public let Preview_Tooltip: String - public let Conversation_EncryptionProcessing: String - public let Weekday_ShortSunday: String - private let _CHAT_ADD_MEMBER: String - private let _CHAT_ADD_MEMBER_r: [(Int, NSRange)] - public func CHAT_ADD_MEMBER(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_CHAT_ADD_MEMBER, self._CHAT_ADD_MEMBER_r, [_1, _2, _3]) - } private let _Channel_AdminLog_MessageKickedName: String private let _Channel_AdminLog_MessageKickedName_r: [(Int, NSRange)] public func Channel_AdminLog_MessageKickedName(_ _1: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Channel_AdminLog_MessageKickedName, self._Channel_AdminLog_MessageKickedName_r, [_1]) } + public let Conversation_EncryptionProcessing: String + private let _CHAT_ADD_MEMBER: String + private let _CHAT_ADD_MEMBER_r: [(Int, NSRange)] + public func CHAT_ADD_MEMBER(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_CHAT_ADD_MEMBER, self._CHAT_ADD_MEMBER_r, [_1, _2, _3]) + } + public let Weekday_ShortSunday: String public let Month_ShortJune: String public let Privacy_Calls_Integration: String + public let Channel_TypeSetup_Title: String public let Month_GenApril: String public let StickerPacksSettings_ShowStickersButton: String - public let MediaPicker_MomentsDateRangeSameMonthFormat: String public let CheckoutInfo_ShippingInfoTitle: String public let StickerPacksSettings_ShowStickersButtonHelp: String private let _Compatibility_SecretMediaVersionTooLow: String @@ -2179,11 +2055,15 @@ public final class PresentationStrings { return formatWithArgumentRanges(_Compatibility_SecretMediaVersionTooLow, self._Compatibility_SecretMediaVersionTooLow_r, [_0, _1]) } public let CallSettings_RecentCalls: String - public let Conversation_Megabytes: String + private let _Conversation_Megabytes: String + private let _Conversation_Megabytes_r: [(Int, NSRange)] + public func Conversation_Megabytes(_ _0: Float) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Conversation_Megabytes, self._Conversation_Megabytes_r, ["\(_0)"]) + } public let Conversation_SearchByName_Prefix: String public let TwoStepAuth_FloodError: String - public let Login_InvalidCountryCode: String public let Paint_Stickers: String + public let Login_InvalidCountryCode: String public let Privacy_Calls_AlwaysAllow_Title: String public let Username_InvalidTooShort: String private let _Settings_ApplyProxyAlert: String @@ -2192,8 +2072,12 @@ public final class PresentationStrings { return formatWithArgumentRanges(_Settings_ApplyProxyAlert, self._Settings_ApplyProxyAlert_r, [_1, _2]) } public let Weekday_ShortFriday: String + private let _Login_BannedPhoneBody: String + private let _Login_BannedPhoneBody_r: [(Int, NSRange)] + public func Login_BannedPhoneBody(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Login_BannedPhoneBody, self._Login_BannedPhoneBody_r, [_0]) + } public let Conversation_ClearAll: String - public let MediaPicker_Moments: String public let Call_ReportIncludeLog: String private let _Time_MonthOfYear_m3: String private let _Time_MonthOfYear_m3_r: [(Int, NSRange)] @@ -2202,20 +2086,20 @@ public final class PresentationStrings { } public let SharedMedia_EmptyTitle: String public let Call_PhoneCallInProgressMessage: String + public let Notification_GroupActivated: String public let Checkout_Name: String - public let Preview_GroupPhotoTitle: String private let _AUTH_REGION: String private let _AUTH_REGION_r: [(Int, NSRange)] public func AUTH_REGION(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_AUTH_REGION, self._AUTH_REGION_r, [_1, _2]) } public let Settings_NotificationsAndSounds: String + public let Conversation_EncryptionCanceled: String private let _GroupInfo_InvitationLinkAcceptChannel: String private let _GroupInfo_InvitationLinkAcceptChannel_r: [(Int, NSRange)] public func GroupInfo_InvitationLinkAcceptChannel(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_GroupInfo_InvitationLinkAcceptChannel, self._GroupInfo_InvitationLinkAcceptChannel_r, [_0]) } - public let Conversation_EncryptionCanceled: String public let AccessDenied_SaveMedia: String public let InviteText_URL: String private let _Channel_AdminLog_MessageInvitedNameUsername: String @@ -2223,35 +2107,30 @@ public final class PresentationStrings { public func Channel_AdminLog_MessageInvitedNameUsername(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Channel_AdminLog_MessageInvitedNameUsername, self._Channel_AdminLog_MessageInvitedNameUsername_r, [_1, _2]) } - public let Channel_Username_InvalidTooManyUsernames: String public let Compose_GroupTokenListPlaceholder: String - public let Profile_ImageUploadError: String public let Conversation_MessageDeliveryFailed: String public let Privacy_PaymentsClear_PaymentInfo: String public let Notifications_GroupNotifications: String - public let Notification_Mute1hMin: String public let CheckoutInfo_SaveInfoHelp: String + public let Notification_Mute1hMin: String public let StickerPacksSettings_ArchivedMasks_Info: String public let ChannelMembers_WhoCanAddMembers_AllMembers: String public let Channel_Edit_PrivatePublicLinkAlert: String public let Watch_Conversation_UserInfo: String + public let Application_Name: String + public let Conversation_AddToReadingList: String public let Conversation_FileDropbox: String public let Login_PhonePlaceholder: String - public let ExplicitContent_AlertUser: String - public let Conversation_AddToReadingList: String - public let Application_Name: String public let Profile_MessageLifetime1d: String - public let Calls_CallTabDescription: String public let CheckoutInfo_ShippingInfoCityPlaceholder: String + public let Calls_CallTabDescription: String public let Resolve_ErrorNotFound: String public let PhotoEditor_FadeTool: String - public let Channel_TitleShowDiscussion: String public let Channel_Setup_TypePublicHelp: String public let GroupInfo_InviteLink_RevokeAlert_Success: String public let Channel_Setup_PublicNoLink: String public let Privacy_Calls_P2PHelp: String public let Conversation_Info: String - public let ChannelInfo_InvitationLinkDoesNotExist: String private let _Time_TodayAt: String private let _Time_TodayAt_r: [(Int, NSRange)] public func Time_TodayAt(_ _0: String) -> (String, [(Int, NSRange)]) { @@ -2259,6 +2138,11 @@ public final class PresentationStrings { } public let Conversation_Processing: String public let Conversation_RestrictedInline: String + private let _InstantPage_AuthorAndDateTitle: String + private let _InstantPage_AuthorAndDateTitle_r: [(Int, NSRange)] + public func InstantPage_AuthorAndDateTitle(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_InstantPage_AuthorAndDateTitle, self._InstantPage_AuthorAndDateTitle_r, [_1, _2]) + } private let _Watch_LastSeen_AtDate: String private let _Watch_LastSeen_AtDate_r: [(Int, NSRange)] public func Watch_LastSeen_AtDate(_ _0: String) -> (String, [(Int, NSRange)]) { @@ -2266,38 +2150,29 @@ public final class PresentationStrings { } public let Conversation_Location: String public let DialogList_PasscodeLockHelp: String - private let _InstantPage_AuthorAndDateTitle: String - private let _InstantPage_AuthorAndDateTitle_r: [(Int, NSRange)] - public func InstantPage_AuthorAndDateTitle(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_InstantPage_AuthorAndDateTitle, self._InstantPage_AuthorAndDateTitle_r, [_1, _2]) - } public let Channel_Management_Title: String public let Notifications_InAppNotificationsPreview: String - public let PrivacySettings_FloodControlError: String public let EnterPasscode_EnterTitle: String public let ReportPeer_ReasonOther_Title: String public let Month_GenJanuary: String public let Conversation_ForwardChats: String - public let SharedMedia_TitlePhoto: String public let Channel_UpdatePhotoItem: String - public let GroupInfo_InvitationLinkAlreadyAccepted: String public let UserInfo_StartSecretChat: String - public let Watch_State_Connecting: String public let PrivacySettings_LastSeenNobody: String private let _FileSize_MB: String private let _FileSize_MB_r: [(Int, NSRange)] public func FileSize_MB(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_FileSize_MB, self._FileSize_MB_r, [_0]) } - public let TwoStepAuth_ConfirmationAbort: String public let ChatSearch_SearchPlaceholder: String - public let GroupInfo_KickedStatus: String + public let TwoStepAuth_ConfirmationAbort: String public let TwoStepAuth_SetupPasswordConfirmFailed: String private let _LastSeen_YesterdayAt: String private let _LastSeen_YesterdayAt_r: [(Int, NSRange)] public func LastSeen_YesterdayAt(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_LastSeen_YesterdayAt, self._LastSeen_YesterdayAt_r, [_0]) } + public let GroupInfo_GroupHistoryVisible: String public let AppleWatch_ReplyPresetsHelp: String public let Localization_LanguageName: String public let Map_OpenIn: String @@ -2319,22 +2194,10 @@ public final class PresentationStrings { return formatWithArgumentRanges(_Time_PreciseDate_m1, self._Time_PreciseDate_m1_r, [_1, _2, _3]) } public let Month_ShortMay: String - private let _WelcomeScreen_Greeting: String - private let _WelcomeScreen_Greeting_r: [(Int, NSRange)] - public func WelcomeScreen_Greeting(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_WelcomeScreen_Greeting, self._WelcomeScreen_Greeting_r, [_0]) - } public let Tour_Text3: String public let Contacts_GlobalSearch: String - public let Watch_Suggestion_CallSoon: String public let DialogList_LanguageTooltip: String public let Map_LoadError: String - public let WelcomeScreen_Logout: String - private let _Service_ApplyLocalizationWithWarnings: String - private let _Service_ApplyLocalizationWithWarnings_r: [(Int, NSRange)] - public func Service_ApplyLocalizationWithWarnings(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_Service_ApplyLocalizationWithWarnings, self._Service_ApplyLocalizationWithWarnings_r, [_0]) - } public let AccessDenied_VoiceMicrophone: String private let _CHANNEL_MESSAGE_STICKER: String private let _CHANNEL_MESSAGE_STICKER_r: [(Int, NSRange)] @@ -2345,22 +2208,25 @@ public final class PresentationStrings { public let PasscodeSettings_TurnPasscodeOff: String public let MediaPicker_AddCaption: String public let Channel_AdminLog_BanReadMessages: String - public let SharedMedia_Outgoing: String - public let Channel_About_Error: String public let Channel_Status: String public let Map_ChooseLocationTitle: String public let Map_OpenInYandexNavigator: String - public let SearchImages_SkipImage: String public let State_WaitingForNetwork: String public let TwoStepAuth_EmailHelp: String + public let Conversation_StopLiveLocation: String public let PhotoEditor_SharpenTool: String public let Common_of: String public let AuthSessions_Title: String + public let Message_PinnedLiveLocationMessage: String public let PrivacyLastSeenSettings_AlwaysShareWith: String public let EnterPasscode_EnterPasscode: String public let Notifications_Reset: String + private let _Map_LiveLocationPrivateDescription: String + private let _Map_LiveLocationPrivateDescription_r: [(Int, NSRange)] + public func Map_LiveLocationPrivateDescription(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Map_LiveLocationPrivateDescription, self._Map_LiveLocationPrivateDescription_r, [_0]) + } public let GroupInfo_InvitationLinkGroupFull: String - public let GoogleDrive_LogoutLogout: String private let _Channel_AdminLog_MessageChangedChannelUsername: String private let _Channel_AdminLog_MessageChangedChannelUsername_r: [(Int, NSRange)] public func Channel_AdminLog_MessageChangedChannelUsername(_ _0: String) -> (String, [(Int, NSRange)]) { @@ -2372,14 +2238,7 @@ public final class PresentationStrings { return formatWithArgumentRanges(_CHAT_MESSAGE_DOC, self._CHAT_MESSAGE_DOC_r, [_1, _2]) } public let Watch_AppName: String - private let _Channel_NotificationSelfAdded: String - private let _Channel_NotificationSelfAdded_r: [(Int, NSRange)] - public func Channel_NotificationSelfAdded(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_Channel_NotificationSelfAdded, self._Channel_NotificationSelfAdded_r, [_0]) - } public let ConvertToSupergroup_HelpTitle: String - public let Conversation_TapAndHoldToRecord: String - public let Channel_ShareNoLink: String private let _MESSAGE_GIF: String private let _MESSAGE_GIF_r: [(Int, NSRange)] public func MESSAGE_GIF(_ _1: String) -> (String, [(Int, NSRange)]) { @@ -2391,29 +2250,26 @@ public final class PresentationStrings { return formatWithArgumentRanges(_DialogList_EncryptedChatStartedOutgoing, self._DialogList_EncryptedChatStartedOutgoing_r, [_0]) } public let Checkout_PayWithTouchId: String - private let _Notification_InvitedMany: String - private let _Notification_InvitedMany_r: [(Int, NSRange)] - public func Notification_InvitedMany(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_Notification_InvitedMany, self._Notification_InvitedMany_r, [_0, _1]) - } + public let Conversation_DiscardVoiceMessageTitle: String private let _CHAT_ADD_YOU: String private let _CHAT_ADD_YOU_r: [(Int, NSRange)] public func CHAT_ADD_YOU(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_CHAT_ADD_YOU, self._CHAT_ADD_YOU_r, [_1, _2]) } public let CheckoutInfo_ShippingInfoCity: String - public let Conversation_DiscardVoiceMessageTitle: String public let Conversation_ClousStorageInfo_Description3: String - public let Profile_MessageLifetime: String - public let GoogleDrive_LogoutTitle: String public let Conversation_PinMessageAlertGroup: String public let Settings_FAQ_Intro: String public let PrivacySettings_AuthSessions: String + private let _CHAT_MESSAGE_GEOLIVE: String + private let _CHAT_MESSAGE_GEOLIVE_r: [(Int, NSRange)] + public func CHAT_MESSAGE_GEOLIVE(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_CHAT_MESSAGE_GEOLIVE, self._CHAT_MESSAGE_GEOLIVE_r, [_1, _2]) + } public let Tour_Title5: String public let ChatAdmins_AllMembersAreAdmins: String public let Group_Management_AddModeratorHelp: String public let Channel_Username_CheckingUsername: String - public let Activity_UploadingAudio: String private let _DialogList_SingleRecordingVideoMessageSuffix: String private let _DialogList_SingleRecordingVideoMessageSuffix_r: [(Int, NSRange)] public func DialogList_SingleRecordingVideoMessageSuffix(_ _0: String) -> (String, [(Int, NSRange)]) { @@ -2429,14 +2285,13 @@ public final class PresentationStrings { public func Checkout_LiabilityAlert(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Checkout_LiabilityAlert, self._Checkout_LiabilityAlert_r, [_1, _1, _1, _2]) } - public let Profile_BotInfo: String public let Channel_Info_BlackList: String - public let StickerPack_RemoveStickers: String + public let Profile_BotInfo: String public let Compose_NewChannel_Members: String public let Notification_Reply: String public let Watch_Stickers_Recents: String public let GroupInfo_SetGroupPhotoStop: String - public let Conversation_PinMessageAlertChannel: String + public let Channel_Stickers_Placeholder: String public let AttachmentMenu_File: String private let _MESSAGE_STICKER: String private let _MESSAGE_STICKER_r: [(Int, NSRange)] @@ -2449,11 +2304,10 @@ public final class PresentationStrings { public func PINNED_PHOTO(_ _1: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_PINNED_PHOTO, self._PINNED_PHOTO_r, [_1]) } - public let Channel_EditAdmin_PermissionChangeInviteLink: String public let Channel_AdminLog_CanAddAdmins: String - public let WelcomeScreen_Title: String public let TwoStepAuth_SetupHint: String public let Conversation_StatusLeftGroup: String + public let MediaPicker_TapToUngroupDescription: String public let Conversation_ShareBotLocationConfirmation: String public let Conversation_DeleteMessagesForMe: String public let Message_PinnedAnimationMessage: String @@ -2465,6 +2319,7 @@ public final class PresentationStrings { return formatWithArgumentRanges(_Time_MonthOfYear_m2, self._Time_MonthOfYear_m2_r, [_0]) } public let Channel_About_Placeholder: String + public let Map_Directions: String public let Channel_About_Title: String private let _MESSAGE_PHOTO: String private let _MESSAGE_PHOTO_r: [(Int, NSRange)] @@ -2473,14 +2328,12 @@ public final class PresentationStrings { } public let Calls_RatingTitle: String public let SharedMedia_EmptyText: String - public let Channel_Username_CreateCommentsHelp: String + public let Channel_Stickers_Searching: String public let Login_PadPhoneHelp: String public let StickerPacksSettings_ArchivedPacks: String public let Channel_ErrorAccessDenied: String public let Generic_ErrorMoreInfo: String - public let Notification_GroupDeactivated: String public let Channel_AdminLog_TitleAllEvents: String - public let PrivacySettings_TouchIdTitle: String public let ChannelMembers_WhoCanAddMembersAllHelp: String public let ChangePhoneNumberCode_CodePlaceholder: String public let Camera_SquareMode: String @@ -2496,33 +2349,25 @@ public final class PresentationStrings { public let PhotoEditor_VignetteTool: String public let LastSeen_WithinAWeek: String public let Widget_NoUsers: String - public let Channel_Edit_EnableComments: String - public let DialogList_NoMessagesText: String + public let Calls_NewCall: String private let _CHANNEL_MESSAGE_AUDIO: String private let _CHANNEL_MESSAGE_AUDIO_r: [(Int, NSRange)] public func CHANNEL_MESSAGE_AUDIO(_ _1: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_CHANNEL_MESSAGE_AUDIO, self._CHANNEL_MESSAGE_AUDIO_r, [_1]) } - public let Calls_NewCall: String - public let SharedMedia_TitleFile: String + public let DialogList_NoMessagesText: String public let MaskStickerSettings_Info: String public let Conversation_FilePhotoOrVideo: String - private let _Watch_LastSeen_AtWeekday: String - private let _Watch_LastSeen_AtWeekday_r: [(Int, NSRange)] - public func Watch_LastSeen_AtWeekday(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_Watch_LastSeen_AtWeekday, self._Watch_LastSeen_AtWeekday_r, [_0]) - } public let Channel_AdminLog_BanSendStickers: String public let Common_Next: String + public let Stickers_RemoveFromFavorites: String public let Watch_Notification_Joined: String private let _Channel_AdminLog_MessageRestrictedNewSetting: String private let _Channel_AdminLog_MessageRestrictedNewSetting_r: [(Int, NSRange)] public func Channel_AdminLog_MessageRestrictedNewSetting(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Channel_AdminLog_MessageRestrictedNewSetting, self._Channel_AdminLog_MessageRestrictedNewSetting_r, [_0]) } - public let ImagePicker_NoVideos: String public let GroupInfo_DeleteAndExitConfirmation: String - public let ChatSettings_Cache: String public let TwoStepAuth_EmailInvalid: String private let _CHAT_MESSAGE_VIDEO: String private let _CHAT_MESSAGE_VIDEO_r: [(Int, NSRange)] @@ -2530,6 +2375,7 @@ public final class PresentationStrings { return formatWithArgumentRanges(_CHAT_MESSAGE_VIDEO, self._CHAT_MESSAGE_VIDEO_r, [_1, _2]) } public let Month_GenJune: String + public let Map_LiveLocationFor15Minutes: String private let _Login_EmailCodeSubject: String private let _Login_EmailCodeSubject_r: [(Int, NSRange)] public func Login_EmailCodeSubject(_ _0: String) -> (String, [(Int, NSRange)]) { @@ -2540,7 +2386,6 @@ public final class PresentationStrings { public func CHAT_TITLE_EDITED(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_CHAT_TITLE_EDITED, self._CHAT_TITLE_EDITED_r, [_1, _2]) } - public let Watch_UnlockRequired: String private let _NetworkUsageSettings_WifiUsageSince: String private let _NetworkUsageSettings_WifiUsageSince_r: [(Int, NSRange)] public func NetworkUsageSettings_WifiUsageSince(_ _0: String) -> (String, [(Int, NSRange)]) { @@ -2548,13 +2393,7 @@ public final class PresentationStrings { } public let Watch_LastSeen_Lately: String public let Watch_Compose_CurrentLocation: String - private let _CHANNEL_MESSAGE_FWDS: String - private let _CHANNEL_MESSAGE_FWDS_r: [(Int, NSRange)] - public func CHANNEL_MESSAGE_FWDS(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_CHANNEL_MESSAGE_FWDS, self._CHANNEL_MESSAGE_FWDS_r, [_1, _2]) - } public let DialogList_RecentTitlePeople: String - public let Conversation_ViewLocation: String public let GroupInfo_Notifications: String public let Call_ReportPlaceholder: String private let _MESSAGE_DOC: String @@ -2572,8 +2411,12 @@ public final class PresentationStrings { return formatWithArgumentRanges(_MediaPicker_Nof, self._MediaPicker_Nof_r, [_0]) } public let Common_Create: String - public let Message_InvoiceShipmentLabel: String public let Contacts_TopSection: String + private let _Map_DirectionsDriveEta: String + private let _Map_DirectionsDriveEta_r: [(Int, NSRange)] + public func Map_DirectionsDriveEta(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Map_DirectionsDriveEta, self._Map_DirectionsDriveEta_r, [_0]) + } public let Your_cards_number_is_invalid: String private let _MESSAGE_INVOICE: String private let _MESSAGE_INVOICE_r: [(Int, NSRange)] @@ -2595,26 +2438,24 @@ public final class PresentationStrings { } public let Conversation_MessageDialogRetry: String public let Watch_ChatList_NoConversationsTitle: String + public let Stickers_GroupStickers: String public let BlockedUsers_Title: String + private let _LiveLocationUpdated_TodayAt: String + private let _LiveLocationUpdated_TodayAt_r: [(Int, NSRange)] + public func LiveLocationUpdated_TodayAt(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_LiveLocationUpdated_TodayAt, self._LiveLocationUpdated_TodayAt_r, [_0]) + } public let ChatSettings_ConnectionType_UseSocks5: String - public let MediaPicker_MomentsDateRangeYearFormat: String public let Cache_ClearNone: String public let SecretTimer_VideoDescription: String public let Login_InvalidCodeError: String - public let Contacts_contacts: String public let Channel_BanList_BlockedTitle: String public let NetworkUsageSettings_Cellular: String public let Watch_Location_Access: String - private let _CONTACT_ACTIVATED: String - private let _CONTACT_ACTIVATED_r: [(Int, NSRange)] - public func CONTACT_ACTIVATED(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_CONTACT_ACTIVATED, self._CONTACT_ACTIVATED_r, [_1]) - } - public let BlockedUsers_AlreadyBlocked: String public let PrivacySettings_DeleteAccountIfAwayFor: String - public let PrivacySettings_DeleteAccountTitle: String - public let Channel_AdminLog_EmptyText: String public let Channel_AdminLog_EmptyFilterText: String + public let Channel_AdminLog_EmptyText: String + public let PrivacySettings_DeleteAccountTitle: String public let PrivacyLastSeenSettings_CustomShareSettings_Delete: String private let _ENCRYPTED_MESSAGE: String private let _ENCRYPTED_MESSAGE_r: [(Int, NSRange)] @@ -2626,11 +2467,6 @@ public final class PresentationStrings { public let TwoStepAuth_EnterPasswordHelp: String public let Bot_Stop: String public let Privacy_GroupsAndChannels_AlwaysAllow_Placeholder: String - private let _AUTH_UNKNOWN: String - private let _AUTH_UNKNOWN_r: [(Int, NSRange)] - public func AUTH_UNKNOWN(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_AUTH_UNKNOWN, self._AUTH_UNKNOWN_r, [_1]) - } public let UserInfo_BotSettings: String public let Your_cards_expiration_month_is_invalid: String public let PrivacyLastSeenSettings_EmpryUsersPlaceholder: String @@ -2639,27 +2475,19 @@ public final class PresentationStrings { public func CHANNEL_MESSAGE_ROUND(_ _1: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_CHANNEL_MESSAGE_ROUND, self._CHANNEL_MESSAGE_ROUND_r, [_1]) } - public let GoogleDrive_FolderLoadError: String public let SocksProxySetup_Port: String public let Message_VideoMessage: String public let Conversation_ContextMenuStickerPackInfo: String - public let Watch_Suggestion_TextInABit: String + public let Login_ResetAccountProtected_LimitExceeded: String private let _CHAT_DELETE_MEMBER: String private let _CHAT_DELETE_MEMBER_r: [(Int, NSRange)] public func CHAT_DELETE_MEMBER(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_CHAT_DELETE_MEMBER, self._CHAT_DELETE_MEMBER_r, [_1, _2, _3]) } - public let Login_ResetAccountProtected_LimitExceeded: String - public let Conversation_EncryptedForwardingAlert: String public let Conversation_DiscardVoiceMessageAction: String public let Camera_Title: String public let PhotoEditor_CurvesBlue: String public let Message_PinnedVideoMessage: String - private let _Settings_OpenSystemPrivacySettings: String - private let _Settings_OpenSystemPrivacySettings_r: [(Int, NSRange)] - public func Settings_OpenSystemPrivacySettings(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_Settings_OpenSystemPrivacySettings, self._Settings_OpenSystemPrivacySettings_r, [_0]) - } private let _Login_EmailPhoneSubject: String private let _Login_EmailPhoneSubject_r: [(Int, NSRange)] public func Login_EmailPhoneSubject(_ _0: String) -> (String, [(Int, NSRange)]) { @@ -2678,14 +2506,13 @@ public final class PresentationStrings { public let AccessDenied_Title: String public let SharedMedia_CategoryLinks: String public let Localization_LanguageOther: String - public let Conversation_ClearAllConfirmation: String public let TwoStepAuth_EmailSkipAlert: String public let ChatSettings_Stickers: String public let Camera_FlashOff: String public let TwoStepAuth_Title: String + public let Checkout_ErrorProviderAccountTimeout: String public let TwoStepAuth_SetupPasswordEnterPasswordChange: String public let WebSearch_Images: String - public let Checkout_ErrorProviderAccountTimeout: String public let Conversation_typing: String public let Common_Back: String public let Common_Search: String @@ -2715,12 +2542,15 @@ public final class PresentationStrings { return formatWithArgumentRanges(_EncryptionKey_Description, self._EncryptionKey_Description_r, [_1, _2]) } public let Conversation_UnreadMessages: String + private let _DialogList_LiveLocationSharingTo: String + private let _DialogList_LiveLocationSharingTo_r: [(Int, NSRange)] + public func DialogList_LiveLocationSharingTo(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_DialogList_LiveLocationSharingTo, self._DialogList_LiveLocationSharingTo_r, [_0]) + } public let Tour_Title3: String public let PrivacyLastSeenSettings_GroupsAndChannelsHelp: String public let Watch_Contacts_NoResults: String public let Watch_UserInfo_MuteTitle: String - public let MediaPicker_Choose: String - public let Conversation_DownloadMegabytes: String private let _Privacy_GroupsAndChannels_InviteToGroupError: String private let _Privacy_GroupsAndChannels_InviteToGroupError_r: [(Int, NSRange)] public func Privacy_GroupsAndChannels_InviteToGroupError(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { @@ -2741,7 +2571,7 @@ public final class PresentationStrings { public let Map_LocatingError: String public let MediaPicker_Send: String public let ChannelIntro_Title: String - public let SearchImages_ErrorDownloadingImage: String + public let AccessDenied_LocationAlwaysDenied: String private let _PINNED_GIF: String private let _PINNED_GIF_r: [(Int, NSRange)] public func PINNED_GIF(_ _1: String) -> (String, [(Int, NSRange)]) { @@ -2753,7 +2583,6 @@ public final class PresentationStrings { return formatWithArgumentRanges(_InviteText_SingleContact, self._InviteText_SingleContact_r, [_0]) } public let Channel_EditAdmin_CannotEdit: String - public let Profile_PhonebookAccessDisabled: String public let LoginPassword_PasswordHelp: String public let BlockedUsers_Unblock: String private let _Time_MonthOfYear_m1: String @@ -2761,7 +2590,6 @@ public final class PresentationStrings { public func Time_MonthOfYear_m1(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Time_MonthOfYear_m1, self._Time_MonthOfYear_m1_r, [_0]) } - public let Conversation_ViewFile: String public let Notifications_GroupNotificationsAlert: String public let Paint_Masks: String public let StickerPack_ErrorNotFound: String @@ -2771,19 +2599,16 @@ public final class PresentationStrings { public func PINNED_CONTACT(_ _1: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_PINNED_CONTACT, self._PINNED_CONTACT_r, [_1]) } - private let _Conversation_ForwardToGroupFormat: String - private let _Conversation_ForwardToGroupFormat_r: [(Int, NSRange)] - public func Conversation_ForwardToGroupFormat(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_Conversation_ForwardToGroupFormat, self._Conversation_ForwardToGroupFormat_r, [_0]) - } private let _FileSize_KB: String private let _FileSize_KB_r: [(Int, NSRange)] public func FileSize_KB(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_FileSize_KB, self._FileSize_KB_r, [_0]) } + public let Map_LiveLocationTitle: String public let Watch_GroupInfo_Title: String public let Channel_AdminLog_EmptyTitle: String public let PhotoEditor_Set: String + public let LiveLocation_MenuStopAll: String private let _Notification_Invited: String private let _Notification_Invited_r: [(Int, NSRange)] public func Notification_Invited(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { @@ -2794,24 +2619,16 @@ public final class PresentationStrings { public let AppleWatch_ReplyPresets: String public let Channel_Members_AddAdminErrorNotAMember: String public let Conversation_EncryptedDescription2: String - public let Paint_Edit: String public let NetworkUsageSettings_MediaVideoDataSection: String + public let Paint_Edit: String public let Conversation_EncryptedDescription3: String public let Login_CodeFloodError: String - private let _Call_EncryptionKey_Description: String - private let _Call_EncryptionKey_Description_r: [(Int, NSRange)] - public func Call_EncryptionKey_Description(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_Call_EncryptionKey_Description, self._Call_EncryptionKey_Description_r, [_1, _2]) - } public let Conversation_EncryptedDescription4: String public let AppleWatch_Title: String public let Contacts_AccessDeniedError: String public let Conversation_StatusTyping: String - public let GoogleDrive_LoadErrorTitle: String public let Share_Title: String - public let Map_Send: String public let TwoStepAuth_ConfirmationTitle: String - public let Conversation_SupportPlaceholder: String public let ChatSettings_Title: String public let AuthSessions_CurrentSession: String public let Watch_Microphone_Access: String @@ -2820,15 +2637,15 @@ public final class PresentationStrings { public func Notification_RenamedChat(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Notification_RenamedChat, self._Notification_RenamedChat_r, [_0]) } + public let Conversation_LiveLocation: String public let Watch_Conversation_GroupInfo: String public let UserInfo_Title: String - public let Service_LocalizationDownloadError: String + public let Map_LiveLocationGroupDescription: String public let Login_InfoHelp: String public let ShareMenu_ShareTo: String public let Message_PinnedGame: String public let Channel_AdminLog_CanSendMessages: String public let Notification_RenamedGroup: String - public let Weekday_Thursday: String private let _Call_PrivacyErrorMessage: String private let _Call_PrivacyErrorMessage_r: [(Int, NSRange)] public func Call_PrivacyErrorMessage(_ _0: String) -> (String, [(Int, NSRange)]) { @@ -2837,22 +2654,15 @@ public final class PresentationStrings { public let ChangePhoneNumberNumber_Title: String public let TwoStepAuth_EnterPasswordInvalid: String public let DialogList_SearchSectionMessages: String - private let _Profile_ShareBotGroupFormat: String - private let _Profile_ShareBotGroupFormat_r: [(Int, NSRange)] - public func Profile_ShareBotGroupFormat(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_Profile_ShareBotGroupFormat, self._Profile_ShareBotGroupFormat_r, [_0]) - } + public let Media_ShareThisVideo: String public let Call_ReportIncludeLogDescription: String public let Preview_DeleteGif: String - public let Weekday_Saturday: String public let UserInfo_DeleteContact: String public let Notifications_ResetAllNotifications: String public let Notification_MessageLifetimeRemovedOutgoing: String - public let Map_More: String public let Login_ContinueWithLocalization: String public let GroupInfo_AddParticipant: String public let Watch_Location_Current: String - public let Map_MapTitle: String public let Checkout_NewCard_SaveInfoHelp: String private let _Settings_ApplyProxyAlertCredentials: String private let _Settings_ApplyProxyAlertCredentials_r: [(Int, NSRange)] @@ -2860,14 +2670,8 @@ public final class PresentationStrings { return formatWithArgumentRanges(_Settings_ApplyProxyAlertCredentials, self._Settings_ApplyProxyAlertCredentials_r, [_1, _2, _3, _4]) } public let MediaPicker_CameraRoll: String - private let _TwoStepAuth_RecoverySent: String - private let _TwoStepAuth_RecoverySent_r: [(Int, NSRange)] - public func TwoStepAuth_RecoverySent(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_TwoStepAuth_RecoverySent, self._TwoStepAuth_RecoverySent_r, [_0]) - } public let Channel_AdminLog_CanPinMessages: String public let KeyCommand_NewMessage: String - public let Compose_NewBroadcastButton: String private let _Time_PreciseDate_m12: String private let _Time_PreciseDate_m12_r: [(Int, NSRange)] public func Time_PreciseDate_m12(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { @@ -2888,7 +2692,6 @@ public final class PresentationStrings { } public let TwoStepAuth_RemovePassword: String public let Privacy_GroupsAndChannels_CustomHelp: String - public let Notification_GroupMigratedToChannel: String public let UserInfo_NotificationsDisable: String public let Watch_UserInfo_Service: String public let Privacy_Calls_CustomHelp: String @@ -2898,7 +2701,6 @@ public final class PresentationStrings { public let DialogList_ClearHistoryConfirmation: String public let CheckoutInfo_ErrorEmailInvalid: String public let Month_GenNovember: String - public let PhotoEditor_TintIntensity: String public let UserInfo_NotificationsEnable: String private let _Target_InviteToGroupConfirmation: String private let _Target_InviteToGroupConfirmation_r: [(Int, NSRange)] @@ -2909,7 +2711,6 @@ public final class PresentationStrings { public let Map_OpenInMaps: String public let Common_OK: String public let TwoStepAuth_SetupHintTitle: String - public let Watch_Suggestion_Nope: String public let GroupInfo_LeftStatus: String public let Cache_ClearProgress: String public let Login_InvalidPhoneError: String @@ -2922,19 +2723,16 @@ public final class PresentationStrings { return formatWithArgumentRanges(_Channel_AdminLog_MessageRemovedGroupUsername, self._Channel_AdminLog_MessageRemovedGroupUsername_r, [_0]) } public let ChatSettings_AutomaticPhotoDownload: String - public let Update_Update: String public let Group_ErrorAddTooMuchAdmins: String + public let SocksProxySetup_Password: String public let Login_SelectCountry_Title: String - public let Notification_EncryptedChatAccepted: String public let Notifications_GroupNotificationsHelp: String public let PhotoEditor_CropAspectRatioSquare: String public let Notification_CallOutgoing: String - public let SocksProxySetup_Password: String public let Weekday_ShortMonday: String - public let Channel_Edit_AboutItem: String public let Checkout_Receipt_Title: String + public let Channel_Edit_AboutItem: String public let Login_InfoLastNamePlaceholder: String - public let Contacts_InvitationText: String public let Channel_Members_AddMembersHelp: String private let _MESSAGE_VIDEO_SECRET: String private let _MESSAGE_VIDEO_SECRET_r: [(Int, NSRange)] @@ -2944,8 +2742,6 @@ public final class PresentationStrings { public let ReportPeer_Report: String public let Channel_EditMessageErrorGeneric: String public let LoginPassword_FloodError: String - public let EncryptionKey_TapToEmojify: String - public let Conversation_InfoChannel: String public let TwoStepAuth_SetupPasswordTitle: String public let PhotoEditor_DiscardChanges: String public let Group_UpgradeNoticeText2: String @@ -2960,11 +2756,6 @@ public final class PresentationStrings { return formatWithArgumentRanges(_ChannelInfo_ChannelForbidden, self._ChannelInfo_ChannelForbidden_r, [_0]) } public let Conversation_ShareMyContactInfo: String - private let _Profile_ShareContactPersonFormat: String - private let _Profile_ShareContactPersonFormat_r: [(Int, NSRange)] - public func Profile_ShareContactPersonFormat(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_Profile_ShareContactPersonFormat, self._Profile_ShareContactPersonFormat_r, [_0]) - } private let _CHANNEL_MESSAGE_GEO: String private let _CHANNEL_MESSAGE_GEO_r: [(Int, NSRange)] public func CHANNEL_MESSAGE_GEO(_ _1: String) -> (String, [(Int, NSRange)]) { @@ -2975,23 +2766,22 @@ public final class PresentationStrings { public let Channel_AdminLogFilter_ChannelEventsInfo: String public let StickerPacksSettings_FeaturedPacks: String public let Month_GenAugust: String + public let Notification_CallCanceled: String public let Channel_Username_CreatePublicLinkHelp: String public let StickerPack_Send: String + public let StickerSettings_MaskContextInfo: String public let Watch_Suggestion_HoldOn: String - public let AttachmentMenu_ImageSearch: String - public let PasscodeSettings_EncryptData: String private let _PINNED_GEO: String private let _PINNED_GEO_r: [(Int, NSRange)] public func PINNED_GEO(_ _1: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_PINNED_GEO, self._PINNED_GEO_r, [_1]) } - public let StickerSettings_MaskContextInfo: String - public let Notification_CallCanceled: String + public let PasscodeSettings_EncryptData: String public let Common_NotNow: String + public let FastTwoStepSetup_PasswordConfirmationPlaceholder: String public let PasscodeSettings_Title: String public let StickerPack_BuiltinPackName: String public let Watch_Suggestion_BRB: String - public let Login_CodeTitle: String private let _CHAT_MESSAGE_ROUND: String private let _CHAT_MESSAGE_ROUND_r: [(Int, NSRange)] public func CHAT_MESSAGE_ROUND(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { @@ -3011,6 +2801,11 @@ public final class PresentationStrings { return formatWithArgumentRanges(_CHAT_LEFT, self._CHAT_LEFT_r, [_1, _2]) } public let LoginPassword_ForgotPassword: String + private let _Map_LiveLocationShortHour: String + private let _Map_LiveLocationShortHour_r: [(Int, NSRange)] + public func Map_LiveLocationShortHour(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Map_LiveLocationShortHour, self._Map_LiveLocationShortHour_r, [_0]) + } private let _DialogList_AwaitingEncryption: String private let _DialogList_AwaitingEncryption_r: [(Int, NSRange)] public func DialogList_AwaitingEncryption(_ _0: String) -> (String, [(Int, NSRange)]) { @@ -3018,11 +2813,6 @@ public final class PresentationStrings { } public let ChatSettings_Appearance: String public let Tour_Title1: String - private let _Notification_ChangedUserPhoto: String - private let _Notification_ChangedUserPhoto_r: [(Int, NSRange)] - public func Notification_ChangedUserPhoto(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_Notification_ChangedUserPhoto, self._Notification_ChangedUserPhoto_r, [_0]) - } public let Conversation_LinkDialogCopy: String private let _Notification_PinnedLocationMessage: String private let _Notification_PinnedLocationMessage_r: [(Int, NSRange)] @@ -3059,6 +2849,7 @@ public final class PresentationStrings { public func Channel_Username_LinkHint(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Channel_Username_LinkHint, self._Channel_Username_LinkHint_r, [_0]) } + public let Settings_ViewPhoto: String public let Paint_RecentStickers: String public let Login_CallRequestState3: String public let Channel_Edit_LinkItem: String @@ -3068,18 +2859,17 @@ public final class PresentationStrings { public let Channel_Moderator_Title: String public let Message_PinnedPhotoMessage: String public let Notification_SecretChatScreenshot: String - public let Activity_UploadingDocument: String - public let AccessDenied_LocationTracking: String - public let Watch_ChatList_NoConversationsText: String - public let ReportPeer_AlertSuccess: String - public let Tour_Text4: String - public let Channel_Info_Description: String private let _Conversation_DeleteMessagesFor: String private let _Conversation_DeleteMessagesFor_r: [(Int, NSRange)] public func Conversation_DeleteMessagesFor(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Conversation_DeleteMessagesFor, self._Conversation_DeleteMessagesFor_r, [_0]) } - public let MessageTimer_Title: String + public let Activity_UploadingDocument: String + public let Watch_ChatList_NoConversationsText: String + public let ReportPeer_AlertSuccess: String + public let Tour_Text4: String + public let Channel_Info_Description: String + public let AccessDenied_LocationTracking: String public let Watch_Compose_Send: String public let SocksProxySetup_UseForCallsHelp: String public let Preview_CopyAddress: String @@ -3092,23 +2882,19 @@ public final class PresentationStrings { public let Target_InviteToGroupErrorAlreadyInvited: String public let AccessDenied_CameraRestricted: String public let Watch_Message_ForwardedFrom: String + public let CheckoutInfo_ShippingInfoCountryPlaceholder: String public let Channel_AboutItem: String public let PhotoEditor_CurvesGreen: String - public let CheckoutInfo_ShippingInfoCountryPlaceholder: String public let Month_GenJuly: String - public let Conversation_DeleteChat: String private let _DialogList_SingleUploadingFileSuffix: String private let _DialogList_SingleUploadingFileSuffix_r: [(Int, NSRange)] public func DialogList_SingleUploadingFileSuffix(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_DialogList_SingleUploadingFileSuffix, self._DialogList_SingleUploadingFileSuffix_r, [_0]) } public let ChannelIntro_CreateChannel: String - public let WelcomeScreen_ContactsAccessDisabled: String public let Channel_Management_AddModerator: String public let Common_ChoosePhoto: String - public let Group_Username_Help: String public let Conversation_Pin: String - public let Channel_AdminLog_CanStartCalls: String private let _Login_ResetAccountProtected_Text: String private let _Login_ResetAccountProtected_Text_r: [(Int, NSRange)] public func Login_ResetAccountProtected_Text(_ _0: String) -> (String, [(Int, NSRange)]) { @@ -3124,7 +2910,6 @@ public final class PresentationStrings { public let FeaturedStickerPacks_Title: String public let Map_OpenInGoogleMaps: String public let Notification_MessageLifetime5s: String - public let EnterPasscode_SetupTitle: String public let Contacts_Title: String public let Channel_Management_AddModeratorHelp: String private let _CHAT_MESSAGE_FWDS: String @@ -3132,17 +2917,11 @@ public final class PresentationStrings { public func CHAT_MESSAGE_FWDS(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_CHAT_MESSAGE_FWDS, self._CHAT_MESSAGE_FWDS_r, [_1, _2, _3]) } - public let WelcomeScreen_UpdatingTitle: String - private let _Login_CodeHelp: String - private let _Login_CodeHelp_r: [(Int, NSRange)] - public func Login_CodeHelp(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_Login_CodeHelp, self._Login_CodeHelp_r, [_0]) - } public let Conversation_MessageDialogEdit: String public let PrivacyLastSeenSettings_Title: String public let Notifications_ClassicTones: String - public let GoogleDrive_Title: String public let Conversation_LinkDialogOpen: String + public let Channel_Info_Subscribers: String public let Conversation_ClousStorageInfo_Description4: String public let Privacy_Calls_AlwaysAllow: String public let Privacy_PaymentsClearInfoHelp: String @@ -3161,13 +2940,13 @@ public final class PresentationStrings { public func PHONE_CALL_MISSED(_ _1: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_PHONE_CALL_MISSED, self._PHONE_CALL_MISSED_r, [_1]) } - public let Map_AccessDeniedError: String private let _Conversation_Kilobytes: String private let _Conversation_Kilobytes_r: [(Int, NSRange)] public func Conversation_Kilobytes(_ _0: Int) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Conversation_Kilobytes, self._Conversation_Kilobytes_r, ["\(_0)"]) } public let Group_ErrorAddBlocked: String + public let TwoStepAuth_AdditionalPassword: String public let MediaPicker_Videos: String public let BlockedUsers_AddNew: String public let StickerPacksSettings_StickerPacksSection: String @@ -3180,29 +2959,19 @@ public final class PresentationStrings { public let PhotoEditor_ShadowsTint: String public let ExplicitContent_AlertTitle: String public let Channel_AdminLogFilter_EventsLeaving: String - public let StickerPack_HideStickers: String - private let _Group_MessageTitleUpdated: String - private let _Group_MessageTitleUpdated_r: [(Int, NSRange)] - public func Group_MessageTitleUpdated(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_Group_MessageTitleUpdated, self._Group_MessageTitleUpdated_r, [_0]) - } + public let Map_LiveLocationFor8Hours: String public let Checkout_EnterPassword: String + public let StickerPack_HideStickers: String public let UserInfo_NotificationsEnabled: String public let Weekday_ShortTuesday: String public let Notification_CallIncomingShort: String public let ConvertToSupergroup_Note: String public let Conversation_EmptyPlaceholder: String - public let Conversation_BroadcastTitle: String public let Username_Help: String public let StickerSettings_ContextHide: String - public let Preview_LoadingImage: String - public let Weekday_Sunday: String - private let _Conversation_DownloadProgressKilobytes: String - private let _Conversation_DownloadProgressKilobytes_r: [(Int, NSRange)] - public func Conversation_DownloadProgressKilobytes(_ _1: Int, _ _2: Int) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_Conversation_DownloadProgressKilobytes, self._Conversation_DownloadProgressKilobytes_r, ["\(_1)", "\(_2)"]) - } + public let Media_ShareThisPhoto: String public let Contacts_ShareTelegram: String + public let PrivacySettings_PasscodeAndFaceId: String public let Settings_ChatBackground: String private let _MessageTimer_Seconds_zero: String private let _MessageTimer_Seconds_one: String @@ -3358,6 +3127,28 @@ public final class PresentationStrings { return String(format: self._MuteFor_Hours_other, "\(value)") } } + private let _Media_ShareVideo_zero: String + private let _Media_ShareVideo_one: String + private let _Media_ShareVideo_two: String + private let _Media_ShareVideo_few: String + private let _Media_ShareVideo_many: String + private let _Media_ShareVideo_other: String + public func Media_ShareVideo(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Media_ShareVideo_zero, "\(value)") + case .one: + return String(format: self._Media_ShareVideo_one, "\(value)") + case .two: + return String(format: self._Media_ShareVideo_two, "\(value)") + case .few: + return String(format: self._Media_ShareVideo_few, "\(value)") + case .many: + return String(format: self._Media_ShareVideo_many, "\(value)") + case .other: + return String(format: self._Media_ShareVideo_other, "\(value)") + } + } private let _MessageTimer_ShortMinutes_zero: String private let _MessageTimer_ShortMinutes_one: String private let _MessageTimer_ShortMinutes_two: String @@ -3512,6 +3303,28 @@ public final class PresentationStrings { return String(format: self._Call_ShortSeconds_other, "\(value)") } } + private let _Conversation_StatusSubscribers_zero: String + private let _Conversation_StatusSubscribers_one: String + private let _Conversation_StatusSubscribers_two: String + private let _Conversation_StatusSubscribers_few: String + private let _Conversation_StatusSubscribers_many: String + private let _Conversation_StatusSubscribers_other: String + public func Conversation_StatusSubscribers(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Conversation_StatusSubscribers_zero, "\(value)") + case .one: + return String(format: self._Conversation_StatusSubscribers_one, "\(value)") + case .two: + return String(format: self._Conversation_StatusSubscribers_two, "\(value)") + case .few: + return String(format: self._Conversation_StatusSubscribers_few, "\(value)") + case .many: + return String(format: self._Conversation_StatusSubscribers_many, "\(value)") + case .other: + return String(format: self._Conversation_StatusSubscribers_other, "\(value)") + } + } private let _SharedMedia_File_zero: String private let _SharedMedia_File_one: String private let _SharedMedia_File_two: String @@ -3534,28 +3347,6 @@ public final class PresentationStrings { return String(format: self._SharedMedia_File_other, "\(value)") } } - private let _PasscodeSettings_AutoLock_IfAwayFor_zero: String - private let _PasscodeSettings_AutoLock_IfAwayFor_one: String - private let _PasscodeSettings_AutoLock_IfAwayFor_two: String - private let _PasscodeSettings_AutoLock_IfAwayFor_few: String - private let _PasscodeSettings_AutoLock_IfAwayFor_many: String - private let _PasscodeSettings_AutoLock_IfAwayFor_other: String - public func PasscodeSettings_AutoLock_IfAwayFor(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._PasscodeSettings_AutoLock_IfAwayFor_zero, "\(value)") - case .one: - return String(format: self._PasscodeSettings_AutoLock_IfAwayFor_one, "\(value)") - case .two: - return String(format: self._PasscodeSettings_AutoLock_IfAwayFor_two, "\(value)") - case .few: - return String(format: self._PasscodeSettings_AutoLock_IfAwayFor_few, "\(value)") - case .many: - return String(format: self._PasscodeSettings_AutoLock_IfAwayFor_many, "\(value)") - case .other: - return String(format: self._PasscodeSettings_AutoLock_IfAwayFor_other, "\(value)") - } - } private let _ForwardedAudios_zero: String private let _ForwardedAudios_one: String private let _ForwardedAudios_two: String @@ -3600,28 +3391,6 @@ public final class PresentationStrings { return String(format: self._PrivacyLastSeenSettings_AddUsers_other, "\(value)") } } - private let _MuteFor_Weeks_zero: String - private let _MuteFor_Weeks_one: String - private let _MuteFor_Weeks_two: String - private let _MuteFor_Weeks_few: String - private let _MuteFor_Weeks_many: String - private let _MuteFor_Weeks_other: String - public func MuteFor_Weeks(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._MuteFor_Weeks_zero, "\(value)") - case .one: - return String(format: self._MuteFor_Weeks_one, "\(value)") - case .two: - return String(format: self._MuteFor_Weeks_two, "\(value)") - case .few: - return String(format: self._MuteFor_Weeks_few, "\(value)") - case .many: - return String(format: self._MuteFor_Weeks_many, "\(value)") - case .other: - return String(format: self._MuteFor_Weeks_other, "\(value)") - } - } private let _ForwardedVideoMessages_zero: String private let _ForwardedVideoMessages_one: String private let _ForwardedVideoMessages_two: String @@ -3710,6 +3479,72 @@ public final class PresentationStrings { return String(format: self._Conversation_StatusMembers_other, "\(value)") } } + private let _Conversation_LiveLocationMembersCount_zero: String + private let _Conversation_LiveLocationMembersCount_one: String + private let _Conversation_LiveLocationMembersCount_two: String + private let _Conversation_LiveLocationMembersCount_few: String + private let _Conversation_LiveLocationMembersCount_many: String + private let _Conversation_LiveLocationMembersCount_other: String + public func Conversation_LiveLocationMembersCount(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Conversation_LiveLocationMembersCount_zero, "\(value)") + case .one: + return String(format: self._Conversation_LiveLocationMembersCount_one, "\(value)") + case .two: + return String(format: self._Conversation_LiveLocationMembersCount_two, "\(value)") + case .few: + return String(format: self._Conversation_LiveLocationMembersCount_few, "\(value)") + case .many: + return String(format: self._Conversation_LiveLocationMembersCount_many, "\(value)") + case .other: + return String(format: self._Conversation_LiveLocationMembersCount_other, "\(value)") + } + } + private let _Media_SharePhoto_zero: String + private let _Media_SharePhoto_one: String + private let _Media_SharePhoto_two: String + private let _Media_SharePhoto_few: String + private let _Media_SharePhoto_many: String + private let _Media_SharePhoto_other: String + public func Media_SharePhoto(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Media_SharePhoto_zero, "\(value)") + case .one: + return String(format: self._Media_SharePhoto_one, "\(value)") + case .two: + return String(format: self._Media_SharePhoto_two, "\(value)") + case .few: + return String(format: self._Media_SharePhoto_few, "\(value)") + case .many: + return String(format: self._Media_SharePhoto_many, "\(value)") + case .other: + return String(format: self._Media_SharePhoto_other, "\(value)") + } + } + private let _LiveLocation_MenuChatsCount_zero: String + private let _LiveLocation_MenuChatsCount_one: String + private let _LiveLocation_MenuChatsCount_two: String + private let _LiveLocation_MenuChatsCount_few: String + private let _LiveLocation_MenuChatsCount_many: String + private let _LiveLocation_MenuChatsCount_other: String + public func LiveLocation_MenuChatsCount(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._LiveLocation_MenuChatsCount_zero, "\(value)") + case .one: + return String(format: self._LiveLocation_MenuChatsCount_one, "\(value)") + case .two: + return String(format: self._LiveLocation_MenuChatsCount_two, "\(value)") + case .few: + return String(format: self._LiveLocation_MenuChatsCount_few, "\(value)") + case .many: + return String(format: self._LiveLocation_MenuChatsCount_many, "\(value)") + case .other: + return String(format: self._LiveLocation_MenuChatsCount_other, "\(value)") + } + } private let _Invitation_Members_zero: String private let _Invitation_Members_one: String private let _Invitation_Members_two: String @@ -3842,28 +3677,6 @@ public final class PresentationStrings { return String(format: self._SharedMedia_Video_other, "\(value)") } } - private let _MuteFor_Minutes_zero: String - private let _MuteFor_Minutes_one: String - private let _MuteFor_Minutes_two: String - private let _MuteFor_Minutes_few: String - private let _MuteFor_Minutes_many: String - private let _MuteFor_Minutes_other: String - public func MuteFor_Minutes(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._MuteFor_Minutes_zero, "\(value)") - case .one: - return String(format: self._MuteFor_Minutes_one, "\(value)") - case .two: - return String(format: self._MuteFor_Minutes_two, "\(value)") - case .few: - return String(format: self._MuteFor_Minutes_few, "\(value)") - case .many: - return String(format: self._MuteFor_Minutes_many, "\(value)") - case .other: - return String(format: self._MuteFor_Minutes_other, "\(value)") - } - } private let _AttachmentMenu_SendVideo_zero: String private let _AttachmentMenu_SendVideo_one: String private let _AttachmentMenu_SendVideo_two: String @@ -3930,28 +3743,6 @@ public final class PresentationStrings { return String(format: self._ForwardedContacts_other, "\(value)") } } - private let _Channel_NotificationComments_zero: String - private let _Channel_NotificationComments_one: String - private let _Channel_NotificationComments_two: String - private let _Channel_NotificationComments_few: String - private let _Channel_NotificationComments_many: String - private let _Channel_NotificationComments_other: String - public func Channel_NotificationComments(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._Channel_NotificationComments_zero, "\(value)") - case .one: - return String(format: self._Channel_NotificationComments_one, "\(value)") - case .two: - return String(format: self._Channel_NotificationComments_two, "\(value)") - case .few: - return String(format: self._Channel_NotificationComments_few, "\(value)") - case .many: - return String(format: self._Channel_NotificationComments_many, "\(value)") - case .other: - return String(format: self._Channel_NotificationComments_other, "\(value)") - } - } private let _ForwardedGifs_zero: String private let _ForwardedGifs_one: String private let _ForwardedGifs_two: String @@ -4106,28 +3897,6 @@ public final class PresentationStrings { return String(format: self._LastSeen_MinutesAgo_other, "\(value)") } } - private let _Conversation_StatusRecipients_zero: String - private let _Conversation_StatusRecipients_one: String - private let _Conversation_StatusRecipients_two: String - private let _Conversation_StatusRecipients_few: String - private let _Conversation_StatusRecipients_many: String - private let _Conversation_StatusRecipients_other: String - public func Conversation_StatusRecipients(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._Conversation_StatusRecipients_zero, "\(value)") - case .one: - return String(format: self._Conversation_StatusRecipients_one, "\(value)") - case .two: - return String(format: self._Conversation_StatusRecipients_two, "\(value)") - case .few: - return String(format: self._Conversation_StatusRecipients_few, "\(value)") - case .many: - return String(format: self._Conversation_StatusRecipients_many, "\(value)") - case .other: - return String(format: self._Conversation_StatusRecipients_other, "\(value)") - } - } private let _ServiceMessage_GameScoreSelfSimple_zero: String private let _ServiceMessage_GameScoreSelfSimple_one: String private let _ServiceMessage_GameScoreSelfSimple_two: String @@ -4216,26 +3985,26 @@ public final class PresentationStrings { return String(format: self._StickerPack_AddMaskCount_other, "\(value)") } } - private let _Channel_Management_LabelRights_zero: String - private let _Channel_Management_LabelRights_one: String - private let _Channel_Management_LabelRights_two: String - private let _Channel_Management_LabelRights_few: String - private let _Channel_Management_LabelRights_many: String - private let _Channel_Management_LabelRights_other: String - public func Channel_Management_LabelRights(_ value: Int32) -> String { + private let _MuteExpires_Days_zero: String + private let _MuteExpires_Days_one: String + private let _MuteExpires_Days_two: String + private let _MuteExpires_Days_few: String + private let _MuteExpires_Days_many: String + private let _MuteExpires_Days_other: String + public func MuteExpires_Days(_ value: Int32) -> String { switch presentationStringsPluralizationForm(self.lc, value) { case .zero: - return String(format: self._Channel_Management_LabelRights_zero, "\(value)") + return String(format: self._MuteExpires_Days_zero, "\(value)") case .one: - return String(format: self._Channel_Management_LabelRights_one, "\(value)") + return String(format: self._MuteExpires_Days_one, "\(value)") case .two: - return String(format: self._Channel_Management_LabelRights_two, "\(value)") + return String(format: self._MuteExpires_Days_two, "\(value)") case .few: - return String(format: self._Channel_Management_LabelRights_few, "\(value)") + return String(format: self._MuteExpires_Days_few, "\(value)") case .many: - return String(format: self._Channel_Management_LabelRights_many, "\(value)") + return String(format: self._MuteExpires_Days_many, "\(value)") case .other: - return String(format: self._Channel_Management_LabelRights_other, "\(value)") + return String(format: self._MuteExpires_Days_other, "\(value)") } } private let _LastSeen_HoursAgo_zero: String @@ -4260,26 +4029,26 @@ public final class PresentationStrings { return String(format: self._LastSeen_HoursAgo_other, "\(value)") } } - private let _MuteExpires_Days_zero: String - private let _MuteExpires_Days_one: String - private let _MuteExpires_Days_two: String - private let _MuteExpires_Days_few: String - private let _MuteExpires_Days_many: String - private let _MuteExpires_Days_other: String - public func MuteExpires_Days(_ value: Int32) -> String { + private let _MessageTimer_Hours_zero: String + private let _MessageTimer_Hours_one: String + private let _MessageTimer_Hours_two: String + private let _MessageTimer_Hours_few: String + private let _MessageTimer_Hours_many: String + private let _MessageTimer_Hours_other: String + public func MessageTimer_Hours(_ value: Int32) -> String { switch presentationStringsPluralizationForm(self.lc, value) { case .zero: - return String(format: self._MuteExpires_Days_zero, "\(value)") + return String(format: self._MessageTimer_Hours_zero, "\(value)") case .one: - return String(format: self._MuteExpires_Days_one, "\(value)") + return String(format: self._MessageTimer_Hours_one, "\(value)") case .two: - return String(format: self._MuteExpires_Days_two, "\(value)") + return String(format: self._MessageTimer_Hours_two, "\(value)") case .few: - return String(format: self._MuteExpires_Days_few, "\(value)") + return String(format: self._MessageTimer_Hours_few, "\(value)") case .many: - return String(format: self._MuteExpires_Days_many, "\(value)") + return String(format: self._MessageTimer_Hours_many, "\(value)") case .other: - return String(format: self._MuteExpires_Days_other, "\(value)") + return String(format: self._MessageTimer_Hours_other, "\(value)") } } private let _MuteExpires_Hours_zero: String @@ -4414,26 +4183,26 @@ public final class PresentationStrings { return String(format: self._SharedMedia_Link_other, "\(value)") } } - private let _Map_ETAHours_zero: String - private let _Map_ETAHours_one: String - private let _Map_ETAHours_two: String - private let _Map_ETAHours_few: String - private let _Map_ETAHours_many: String - private let _Map_ETAHours_other: String - public func Map_ETAHours(_ value: Int32) -> String { + private let _DialogList_LiveLocationChatsCount_zero: String + private let _DialogList_LiveLocationChatsCount_one: String + private let _DialogList_LiveLocationChatsCount_two: String + private let _DialogList_LiveLocationChatsCount_few: String + private let _DialogList_LiveLocationChatsCount_many: String + private let _DialogList_LiveLocationChatsCount_other: String + public func DialogList_LiveLocationChatsCount(_ value: Int32) -> String { switch presentationStringsPluralizationForm(self.lc, value) { case .zero: - return String(format: self._Map_ETAHours_zero, "\(value)") + return String(format: self._DialogList_LiveLocationChatsCount_zero, "\(value)") case .one: - return String(format: self._Map_ETAHours_one, "\(value)") + return String(format: self._DialogList_LiveLocationChatsCount_one, "\(value)") case .two: - return String(format: self._Map_ETAHours_two, "\(value)") + return String(format: self._DialogList_LiveLocationChatsCount_two, "\(value)") case .few: - return String(format: self._Map_ETAHours_few, "\(value)") + return String(format: self._DialogList_LiveLocationChatsCount_few, "\(value)") case .many: - return String(format: self._Map_ETAHours_many, "\(value)") + return String(format: self._DialogList_LiveLocationChatsCount_many, "\(value)") case .other: - return String(format: self._Map_ETAHours_other, "\(value)") + return String(format: self._DialogList_LiveLocationChatsCount_other, "\(value)") } } private let _SharedMedia_DeleteItemsConfirmation_zero: String @@ -4502,26 +4271,26 @@ public final class PresentationStrings { return String(format: self._ForwardedMessages_other, "\(value)") } } - private let _SharedMedia_ItemsSelected_zero: String - private let _SharedMedia_ItemsSelected_one: String - private let _SharedMedia_ItemsSelected_two: String - private let _SharedMedia_ItemsSelected_few: String - private let _SharedMedia_ItemsSelected_many: String - private let _SharedMedia_ItemsSelected_other: String - public func SharedMedia_ItemsSelected(_ value: Int32) -> String { + private let _Map_ETAHours_zero: String + private let _Map_ETAHours_one: String + private let _Map_ETAHours_two: String + private let _Map_ETAHours_few: String + private let _Map_ETAHours_many: String + private let _Map_ETAHours_other: String + public func Map_ETAHours(_ value: Int32) -> String { switch presentationStringsPluralizationForm(self.lc, value) { case .zero: - return String(format: self._SharedMedia_ItemsSelected_zero, "\(value)") + return String(format: self._Map_ETAHours_zero, "\(value)") case .one: - return String(format: self._SharedMedia_ItemsSelected_one, "\(value)") + return String(format: self._Map_ETAHours_one, "\(value)") case .two: - return String(format: self._SharedMedia_ItemsSelected_two, "\(value)") + return String(format: self._Map_ETAHours_two, "\(value)") case .few: - return String(format: self._SharedMedia_ItemsSelected_few, "\(value)") + return String(format: self._Map_ETAHours_few, "\(value)") case .many: - return String(format: self._SharedMedia_ItemsSelected_many, "\(value)") + return String(format: self._Map_ETAHours_many, "\(value)") case .other: - return String(format: self._SharedMedia_ItemsSelected_other, "\(value)") + return String(format: self._Map_ETAHours_other, "\(value)") } } private let _Watch_LastSeen_MinutesAgo_zero: String @@ -4590,28 +4359,6 @@ public final class PresentationStrings { return String(format: self._Map_ETAMinutes_other, "\(value)") } } - private let _MessageTimer_Hours_zero: String - private let _MessageTimer_Hours_one: String - private let _MessageTimer_Hours_two: String - private let _MessageTimer_Hours_few: String - private let _MessageTimer_Hours_many: String - private let _MessageTimer_Hours_other: String - public func MessageTimer_Hours(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._MessageTimer_Hours_zero, "\(value)") - case .one: - return String(format: self._MessageTimer_Hours_one, "\(value)") - case .two: - return String(format: self._MessageTimer_Hours_two, "\(value)") - case .few: - return String(format: self._MessageTimer_Hours_few, "\(value)") - case .many: - return String(format: self._MessageTimer_Hours_many, "\(value)") - case .other: - return String(format: self._MessageTimer_Hours_other, "\(value)") - } - } private let _Notification_GameScoreSelfSimple_zero: String private let _Notification_GameScoreSelfSimple_one: String private let _Notification_GameScoreSelfSimple_two: String @@ -4788,26 +4535,26 @@ public final class PresentationStrings { return String(format: self._Watch_UserInfo_Mute_other, "\(value)") } } - private let _StickerPack_MaskCount_zero: String - private let _StickerPack_MaskCount_one: String - private let _StickerPack_MaskCount_two: String - private let _StickerPack_MaskCount_few: String - private let _StickerPack_MaskCount_many: String - private let _StickerPack_MaskCount_other: String - public func StickerPack_MaskCount(_ value: Int32) -> String { + private let _LiveLocationUpdated_MinutesAgo_zero: String + private let _LiveLocationUpdated_MinutesAgo_one: String + private let _LiveLocationUpdated_MinutesAgo_two: String + private let _LiveLocationUpdated_MinutesAgo_few: String + private let _LiveLocationUpdated_MinutesAgo_many: String + private let _LiveLocationUpdated_MinutesAgo_other: String + public func LiveLocationUpdated_MinutesAgo(_ value: Int32) -> String { switch presentationStringsPluralizationForm(self.lc, value) { case .zero: - return String(format: self._StickerPack_MaskCount_zero, "\(value)") + return String(format: self._LiveLocationUpdated_MinutesAgo_zero, "\(value)") case .one: - return String(format: self._StickerPack_MaskCount_one, "\(value)") + return String(format: self._LiveLocationUpdated_MinutesAgo_one, "\(value)") case .two: - return String(format: self._StickerPack_MaskCount_two, "\(value)") + return String(format: self._LiveLocationUpdated_MinutesAgo_two, "\(value)") case .few: - return String(format: self._StickerPack_MaskCount_few, "\(value)") + return String(format: self._LiveLocationUpdated_MinutesAgo_few, "\(value)") case .many: - return String(format: self._StickerPack_MaskCount_many, "\(value)") + return String(format: self._LiveLocationUpdated_MinutesAgo_many, "\(value)") case .other: - return String(format: self._StickerPack_MaskCount_other, "\(value)") + return String(format: self._LiveLocationUpdated_MinutesAgo_other, "\(value)") } } private let _Call_ShortMinutes_zero: String @@ -4854,6 +4601,28 @@ public final class PresentationStrings { return String(format: self._StickerPack_RemoveMaskCount_other, "\(value)") } } + private let _Media_ShareItem_zero: String + private let _Media_ShareItem_one: String + private let _Media_ShareItem_two: String + private let _Media_ShareItem_few: String + private let _Media_ShareItem_many: String + private let _Media_ShareItem_other: String + public func Media_ShareItem(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Media_ShareItem_zero, "\(value)") + case .one: + return String(format: self._Media_ShareItem_one, "\(value)") + case .two: + return String(format: self._Media_ShareItem_two, "\(value)") + case .few: + return String(format: self._Media_ShareItem_few, "\(value)") + case .many: + return String(format: self._Media_ShareItem_many, "\(value)") + case .other: + return String(format: self._Media_ShareItem_other, "\(value)") + } + } private let _ForwardedLocations_zero: String private let _ForwardedLocations_one: String private let _ForwardedLocations_two: String @@ -4992,7 +4761,11 @@ public final class PresentationStrings { self.languageCode = languageCode self.dict = dict var rawCode = languageCode as NSString - let range = rawCode.range(of: "_") + var range = rawCode.range(of: "_") + if range.location != NSNotFound { + rawCode = rawCode.substring(to: range.location) as NSString + } + range = rawCode.range(of: "-") if range.location != NSNotFound { rawCode = rawCode.substring(to: range.location) as NSString } @@ -5008,13 +4781,12 @@ public final class PresentationStrings { self.EnterPasscode_EnterNewPasscodeNew = getValue(dict, "EnterPasscode.EnterNewPasscodeNew") self.Privacy_Calls_WhoCanCallMe = getValue(dict, "Privacy.Calls.WhoCanCallMe") self.Watch_NoConnection = getValue(dict, "Watch.NoConnection") - self._Group_Username_LinkHint = getValue(dict, "Group.Username.LinkHint") - self._Group_Username_LinkHint_r = extractArgumentRanges(self._Group_Username_LinkHint) self.Activity_UploadingPhoto = getValue(dict, "Activity.UploadingPhoto") self.PrivacySettings_PrivacyTitle = getValue(dict, "PrivacySettings.PrivacyTitle") - self.Settings_LogoutError = getValue(dict, "Settings.LogoutError") self._DialogList_PinLimitError = getValue(dict, "DialogList.PinLimitError") self._DialogList_PinLimitError_r = extractArgumentRanges(self._DialogList_PinLimitError) + self.FastTwoStepSetup_PasswordSection = getValue(dict, "FastTwoStepSetup.PasswordSection") + self.FastTwoStepSetup_EmailSection = getValue(dict, "FastTwoStepSetup.EmailSection") self.Cache_ClearCache = getValue(dict, "Cache.ClearCache") self.Common_Close = getValue(dict, "Common.Close") self.ChangePhoneNumberCode_Called = getValue(dict, "ChangePhoneNumberCode.Called") @@ -5028,15 +4800,15 @@ public final class PresentationStrings { self.TwoStepAuth_SetupPasswordConfirmPassword = getValue(dict, "TwoStepAuth.SetupPasswordConfirmPassword") self.ChannelIntro_Text = getValue(dict, "ChannelIntro.Text") self.PrivacySettings_SecurityTitle = getValue(dict, "PrivacySettings.SecurityTitle") + self.DialogList_SavedMessages = getValue(dict, "DialogList.SavedMessages") self._Login_SmsRequestState1 = getValue(dict, "Login.SmsRequestState1") self._Login_SmsRequestState1_r = extractArgumentRanges(self._Login_SmsRequestState1) - self.Conversation_Download = getValue(dict, "Conversation.Download") self._Call_StatusOngoing = getValue(dict, "Call.StatusOngoing") self._Call_StatusOngoing_r = extractArgumentRanges(self._Call_StatusOngoing) self.Settings_LogoutConfirmationText = getValue(dict, "Settings.LogoutConfirmationText") self.BlockedUsers_Info = getValue(dict, "BlockedUsers.Info") self.ChatSettings_AutomaticAudioDownload = getValue(dict, "ChatSettings.AutomaticAudioDownload") - self.Map_OpenInFoursquare = getValue(dict, "Map.OpenInFoursquare") + self.Settings_SetUsername = getValue(dict, "Settings.SetUsername") self.Privacy_Calls_CustomShareHelp = getValue(dict, "Privacy.Calls.CustomShareHelp") self.Group_MessagePhotoUpdated = getValue(dict, "Group.MessagePhotoUpdated") self.Message_PinnedInvoice = getValue(dict, "Message.PinnedInvoice") @@ -5046,22 +4818,20 @@ public final class PresentationStrings { self._CHAT_MESSAGE_TEXT = getValue(dict, "CHAT_MESSAGE_TEXT") self._CHAT_MESSAGE_TEXT_r = extractArgumentRanges(self._CHAT_MESSAGE_TEXT) self.Message_Sticker = getValue(dict, "Message.Sticker") - self.Channel_Management_Remove = getValue(dict, "Channel.Management.Remove") - self.Channel_Username_Help = getValue(dict, "Channel.Username.Help") self.Paint_Regular = getValue(dict, "Paint.Regular") + self.Channel_Username_Help = getValue(dict, "Channel.Username.Help") self._Profile_CreateEncryptedChatOutdatedError = getValue(dict, "Profile.CreateEncryptedChatOutdatedError") self._Profile_CreateEncryptedChatOutdatedError_r = extractArgumentRanges(self._Profile_CreateEncryptedChatOutdatedError) - self.Login_InactiveHelp = getValue(dict, "Login.InactiveHelp") - self.ChatSettings_Security = getValue(dict, "ChatSettings.Security") self._PINNED_STICKER = getValue(dict, "PINNED_STICKER") self._PINNED_STICKER_r = extractArgumentRanges(self._PINNED_STICKER) self.Conversation_ShareInlineBotLocationConfirmation = getValue(dict, "Conversation.ShareInlineBotLocationConfirmation") self._Channel_AdminLog_MessageEdited = getValue(dict, "Channel.AdminLog.MessageEdited") self._Channel_AdminLog_MessageEdited_r = extractArgumentRanges(self._Channel_AdminLog_MessageEdited) + self.Group_Setup_HistoryHidden = getValue(dict, "Group.Setup.HistoryHidden") + self.Your_cards_expiration_year_is_invalid = getValue(dict, "Your_cards_expiration_year_is_invalid") + self.AccessDenied_MicrophoneRestricted = getValue(dict, "AccessDenied.MicrophoneRestricted") self._PHONE_CALL_REQUEST = getValue(dict, "PHONE_CALL_REQUEST") self._PHONE_CALL_REQUEST_r = extractArgumentRanges(self._PHONE_CALL_REQUEST) - self.AccessDenied_MicrophoneRestricted = getValue(dict, "AccessDenied.MicrophoneRestricted") - self.Your_cards_expiration_year_is_invalid = getValue(dict, "Your_cards_expiration_year_is_invalid") self.GroupInfo_InviteByLink = getValue(dict, "GroupInfo.InviteByLink") self._Notification_LeftChat = getValue(dict, "Notification.LeftChat") self._Notification_LeftChat_r = extractArgumentRanges(self._Notification_LeftChat) @@ -5069,13 +4839,12 @@ public final class PresentationStrings { self._Channel_AdminLog_MessageAdmin_r = extractArgumentRanges(self._Channel_AdminLog_MessageAdmin) self.PrivacyLastSeenSettings_NeverShareWith_Placeholder = getValue(dict, "PrivacyLastSeenSettings.NeverShareWith.Placeholder") self.TwoStepAuth_SetupEmail = getValue(dict, "TwoStepAuth.SetupEmail") + self.Checkout_PayWithFaceId = getValue(dict, "Checkout.PayWithFaceId") self.Login_ResetAccountProtected_Reset = getValue(dict, "Login.ResetAccountProtected.Reset") self.SocksProxySetup_Hostname = getValue(dict, "SocksProxySetup.Hostname") self.Channel_AdminLog_CanEditMessages = getValue(dict, "Channel.AdminLog.CanEditMessages") self._MESSAGE_CONTACT = getValue(dict, "MESSAGE_CONTACT") self._MESSAGE_CONTACT_r = extractArgumentRanges(self._MESSAGE_CONTACT) - self._Group_Management_ErrorNotMember = getValue(dict, "Group.Management.ErrorNotMember") - self._Group_Management_ErrorNotMember_r = extractArgumentRanges(self._Group_Management_ErrorNotMember) self.MediaPicker_MomentsDateRangeSameMonthYearFormat = getValue(dict, "MediaPicker.MomentsDateRangeSameMonthYearFormat") self.Notification_MessageLifetime1w = getValue(dict, "Notification.MessageLifetime1w") self.PasscodeSettings_AutoLock_IfAwayFor_5minutes = getValue(dict, "PasscodeSettings.AutoLock.IfAwayFor_5minutes") @@ -5088,7 +4857,6 @@ public final class PresentationStrings { self._Notification_CallTimeFormat_r = extractArgumentRanges(self._Notification_CallTimeFormat) self.Paint_Delete = getValue(dict, "Paint.Delete") self.Channel_MessagePhotoUpdated = getValue(dict, "Channel.MessagePhotoUpdated") - self.SharedMedia_All = getValue(dict, "SharedMedia.All") self.Cache_Help = getValue(dict, "Cache.Help") self._Login_EmailPhoneBody = getValue(dict, "Login.EmailPhoneBody") self._Login_EmailPhoneBody_r = extractArgumentRanges(self._Login_EmailPhoneBody) @@ -5097,55 +4865,48 @@ public final class PresentationStrings { self.Checkout_TotalAmount = getValue(dict, "Checkout.TotalAmount") self.Conversation_MessageEditedLabel = getValue(dict, "Conversation.MessageEditedLabel") self.SharedMedia_EmptyLinksText = getValue(dict, "SharedMedia.EmptyLinksText") - self.Channel_Members_Kick = getValue(dict, "Channel.Members.Kick") - self.GoogleDrive_FolderIsEmpty = getValue(dict, "GoogleDrive.FolderIsEmpty") self._Conversation_RestrictedTextTimed = getValue(dict, "Conversation.RestrictedTextTimed") self._Conversation_RestrictedTextTimed_r = extractArgumentRanges(self._Conversation_RestrictedTextTimed) self.Calls_NoCallsPlaceholder = getValue(dict, "Calls.NoCallsPlaceholder") - self.Message_PinnedDeletedMessage = getValue(dict, "Message.PinnedDeletedMessage") self.Conversation_PinMessageAlert_OnlyPin = getValue(dict, "Conversation.PinMessageAlert.OnlyPin") + self.PasscodeSettings_UnlockWithFaceId = getValue(dict, "PasscodeSettings.UnlockWithFaceId") self.ReportPeer_ReasonOther_Send = getValue(dict, "ReportPeer.ReasonOther.Send") self.Conversation_InstantPagePreview = getValue(dict, "Conversation.InstantPagePreview") self.PasscodeSettings_SimplePasscodeHelp = getValue(dict, "PasscodeSettings.SimplePasscodeHelp") self._Time_PreciseDate_m9 = getValue(dict, "Time.PreciseDate_m9") self._Time_PreciseDate_m9_r = extractArgumentRanges(self._Time_PreciseDate_m9) - self.Group_ErrorAddTooMuch = getValue(dict, "Group.ErrorAddTooMuch") self.GroupInfo_Title = getValue(dict, "GroupInfo.Title") self.State_Updating = getValue(dict, "State.Updating") - self.StickerSettings_ContextShow = getValue(dict, "StickerSettings.ContextShow") self.Map_GetDirections = getValue(dict, "Map.GetDirections") self._TwoStepAuth_PendingEmailHelp = getValue(dict, "TwoStepAuth.PendingEmailHelp") self._TwoStepAuth_PendingEmailHelp_r = extractArgumentRanges(self._TwoStepAuth_PendingEmailHelp) self.UserInfo_PhoneCall = getValue(dict, "UserInfo.PhoneCall") self.MusicPlayer_VoiceNote = getValue(dict, "MusicPlayer.VoiceNote") - self.Channel_Username_InvalidTaken = getValue(dict, "Channel.Username.InvalidTaken") self.Paint_Duplicate = getValue(dict, "Paint.Duplicate") - self._Profile_ShareContactGroupFormat = getValue(dict, "Profile.ShareContactGroupFormat") - self._Profile_ShareContactGroupFormat_r = extractArgumentRanges(self._Profile_ShareContactGroupFormat) + self.Channel_Username_InvalidTaken = getValue(dict, "Channel.Username.InvalidTaken") + self.Stickers_GroupStickersHelp = getValue(dict, "Stickers.GroupStickersHelp") self.SecretChat_Title = getValue(dict, "SecretChat.Title") self.Group_UpgradeConfirmation = getValue(dict, "Group.UpgradeConfirmation") self.Checkout_LiabilityAlertTitle = getValue(dict, "Checkout.LiabilityAlertTitle") self.GroupInfo_GroupNamePlaceholder = getValue(dict, "GroupInfo.GroupNamePlaceholder") - self.Conversation_InfoBroadcastList = getValue(dict, "Conversation.InfoBroadcastList") self._Time_PreciseDate_m11 = getValue(dict, "Time.PreciseDate_m11") self._Time_PreciseDate_m11_r = extractArgumentRanges(self._Time_PreciseDate_m11) + self._MESSAGE_GEOLIVE = getValue(dict, "MESSAGE_GEOLIVE") + self._MESSAGE_GEOLIVE_r = extractArgumentRanges(self._MESSAGE_GEOLIVE) self._Notification_JoinedGroupByLink = getValue(dict, "Notification.JoinedGroupByLink") self._Notification_JoinedGroupByLink_r = extractArgumentRanges(self._Notification_JoinedGroupByLink) self.LoginPassword_Title = getValue(dict, "LoginPassword.Title") self.Login_HaveNotReceivedCodeInternal = getValue(dict, "Login.HaveNotReceivedCodeInternal") - self.Conversation_PlayVideo = getValue(dict, "Conversation.PlayVideo") self.PasscodeSettings_SimplePasscode = getValue(dict, "PasscodeSettings.SimplePasscode") - self.Conversation_MicrophoneAccessDisabled = getValue(dict, "Conversation.MicrophoneAccessDisabled") self.NewContact_Title = getValue(dict, "NewContact.Title") self.Username_CheckingUsername = getValue(dict, "Username.CheckingUsername") - self.UserInfo_InviteBotToGroup = getValue(dict, "UserInfo.InviteBotToGroup") self.Login_ResetAccountProtected_TimerTitle = getValue(dict, "Login.ResetAccountProtected.TimerTitle") self.Checkout_Email = getValue(dict, "Checkout.Email") self.CheckoutInfo_SaveInfo = getValue(dict, "CheckoutInfo.SaveInfo") + self.UserInfo_InviteBotToGroup = getValue(dict, "UserInfo.InviteBotToGroup") self._ChangePhoneNumberCode_CallTimer = getValue(dict, "ChangePhoneNumberCode.CallTimer") self._ChangePhoneNumberCode_CallTimer_r = extractArgumentRanges(self._ChangePhoneNumberCode_CallTimer) self.TwoStepAuth_SetupPasswordEnterPasswordNew = getValue(dict, "TwoStepAuth.SetupPasswordEnterPasswordNew") - self.Weekday_Wednesday = getValue(dict, "Weekday.Wednesday") self._Channel_AdminLog_MessageToggleSignaturesOff = getValue(dict, "Channel.AdminLog.MessageToggleSignaturesOff") self._Channel_AdminLog_MessageToggleSignaturesOff_r = extractArgumentRanges(self._Channel_AdminLog_MessageToggleSignaturesOff) self.Month_ShortDecember = getValue(dict, "Month.ShortDecember") @@ -5156,13 +4917,14 @@ public final class PresentationStrings { self.Privacy_GroupsAndChannels_NeverAllow_Placeholder = getValue(dict, "Privacy.GroupsAndChannels.NeverAllow.Placeholder") self.Message_Video = getValue(dict, "Message.Video") self.Notification_ChannelInviterSelf = getValue(dict, "Notification.ChannelInviterSelf") - self._VideoPreview_OptionSD = getValue(dict, "VideoPreview.OptionSD") - self._VideoPreview_OptionSD_r = extractArgumentRanges(self._VideoPreview_OptionSD) - self.Conversation_SecretLinkPreviewAlert = getValue(dict, "Conversation.SecretLinkPreviewAlert") self.Channel_AdminLog_BanEmbedLinks = getValue(dict, "Channel.AdminLog.BanEmbedLinks") + self.Conversation_SecretLinkPreviewAlert = getValue(dict, "Conversation.SecretLinkPreviewAlert") + self._CHANNEL_MESSAGE_GEOLIVE = getValue(dict, "CHANNEL_MESSAGE_GEOLIVE") + self._CHANNEL_MESSAGE_GEOLIVE_r = extractArgumentRanges(self._CHANNEL_MESSAGE_GEOLIVE) self.Cache_Videos = getValue(dict, "Cache.Videos") self.Call_ReportSkip = getValue(dict, "Call.ReportSkip") self.NetworkUsageSettings_MediaImageDataSection = getValue(dict, "NetworkUsageSettings.MediaImageDataSection") + self.Group_Setup_HistoryTitle = getValue(dict, "Group.Setup.HistoryTitle") self.TwoStepAuth_GenericHelp = getValue(dict, "TwoStepAuth.GenericHelp") self._DialogList_SingleRecordingAudioSuffix = getValue(dict, "DialogList.SingleRecordingAudioSuffix") self._DialogList_SingleRecordingAudioSuffix_r = extractArgumentRanges(self._DialogList_SingleRecordingAudioSuffix) @@ -5170,25 +4932,22 @@ public final class PresentationStrings { self.Settings_FAQ_Button = getValue(dict, "Settings.FAQ_Button") self._GroupInfo_AddParticipantConfirmation = getValue(dict, "GroupInfo.AddParticipantConfirmation") self._GroupInfo_AddParticipantConfirmation_r = extractArgumentRanges(self._GroupInfo_AddParticipantConfirmation) + self._Notification_PinnedLiveLocationMessage = getValue(dict, "Notification.PinnedLiveLocationMessage") + self._Notification_PinnedLiveLocationMessage_r = extractArgumentRanges(self._Notification_PinnedLiveLocationMessage) self.AccessDenied_PhotosRestricted = getValue(dict, "AccessDenied.PhotosRestricted") self.Map_Locating = getValue(dict, "Map.Locating") - self._SearchImages_Downloading_Kb = getValue(dict, "SearchImages.Downloading#Kb") - self._SearchImages_Downloading_Kb_r = extractArgumentRanges(self._SearchImages_Downloading_Kb) - self._Profile_ShareBotPersonFormat = getValue(dict, "Profile.ShareBotPersonFormat") - self._Profile_ShareBotPersonFormat_r = extractArgumentRanges(self._Profile_ShareBotPersonFormat) - self.SearchImages_SearchImages = getValue(dict, "SearchImages.SearchImages") self.SocksProxySetup_Title = getValue(dict, "SocksProxySetup.Title") self.SharedMedia_EmptyMusicText = getValue(dict, "SharedMedia.EmptyMusicText") self.Cache_ByPeerHeader = getValue(dict, "Cache.ByPeerHeader") self.Bot_GroupStatusReadsHistory = getValue(dict, "Bot.GroupStatusReadsHistory") self.TwoStepAuth_ResetAccountConfirmation = getValue(dict, "TwoStepAuth.ResetAccountConfirmation") self.CallSettings_Always = getValue(dict, "CallSettings.Always") - self.SearchImages_DownloadCancelled = getValue(dict, "SearchImages.DownloadCancelled") - self.Channel_BanUser_Unban = getValue(dict, "Channel.BanUser.Unban") self.Message_ImageExpired = getValue(dict, "Message.ImageExpired") + self.Channel_BanUser_Unban = getValue(dict, "Channel.BanUser.Unban") + self.Stickers_GroupChooseStickerPack = getValue(dict, "Stickers.GroupChooseStickerPack") + self.Group_Setup_TypePrivate = getValue(dict, "Group.Setup.TypePrivate") self.Settings_LogoutConfirmationTitle = getValue(dict, "Settings.LogoutConfirmationTitle") self.UserInfo_FirstNamePlaceholder = getValue(dict, "UserInfo.FirstNamePlaceholder") - self.ChatSettings_AutoPlayAudio = getValue(dict, "ChatSettings.AutoPlayAudio") self.LoginPassword_ResetAccount = getValue(dict, "LoginPassword.ResetAccount") self.Privacy_GroupsAndChannels_AlwaysAllow = getValue(dict, "Privacy.GroupsAndChannels.AlwaysAllow") self._Notification_JoinedChat = getValue(dict, "Notification.JoinedChat") @@ -5200,6 +4959,7 @@ public final class PresentationStrings { self.Channel_Username_Title = getValue(dict, "Channel.Username.Title") self._Channel_AdminLog_MessageToggleSignaturesOn = getValue(dict, "Channel.AdminLog.MessageToggleSignaturesOn") self._Channel_AdminLog_MessageToggleSignaturesOn_r = extractArgumentRanges(self._Channel_AdminLog_MessageToggleSignaturesOn) + self.Map_PullUpForPlaces = getValue(dict, "Map.PullUpForPlaces") self._Conversation_EncryptionWaiting = getValue(dict, "Conversation.EncryptionWaiting") self._Conversation_EncryptionWaiting_r = extractArgumentRanges(self._Conversation_EncryptionWaiting) self.Calls_NotNow = getValue(dict, "Calls.NotNow") @@ -5222,7 +4982,7 @@ public final class PresentationStrings { self.Watch_MessageView_Title = getValue(dict, "Watch.MessageView.Title") self._Notification_PinnedRoundMessage = getValue(dict, "Notification.PinnedRoundMessage") self._Notification_PinnedRoundMessage_r = extractArgumentRanges(self._Notification_PinnedRoundMessage) - self.Conversation_DeleteGroup = getValue(dict, "Conversation.DeleteGroup") + self.Conversation_ViewMessage = getValue(dict, "Conversation.ViewMessage") self.Settings_SaveEditedPhotos = getValue(dict, "Settings.SaveEditedPhotos") self.Channel_Management_LabelCreator = getValue(dict, "Channel.Management.LabelCreator") self._Notification_PinnedStickerMessage = getValue(dict, "Notification.PinnedStickerMessage") @@ -5238,11 +4998,12 @@ public final class PresentationStrings { self.CheckoutInfo_ReceiverInfoPhone = getValue(dict, "CheckoutInfo.ReceiverInfoPhone") self.SocksProxySetup_TypeNone = getValue(dict, "SocksProxySetup.TypeNone") self.GroupInfo_AddParticipantTitle = getValue(dict, "GroupInfo.AddParticipantTitle") + self.Map_LiveLocationShowAll = getValue(dict, "Map.LiveLocationShowAll") + self.Settings_SavedMessages = getValue(dict, "Settings.SavedMessages") self._CHANNEL_MESSAGE_TEXT = getValue(dict, "CHANNEL_MESSAGE_TEXT") self._CHANNEL_MESSAGE_TEXT_r = extractArgumentRanges(self._CHANNEL_MESSAGE_TEXT) self.Checkout_PayNone = getValue(dict, "Checkout.PayNone") self.CheckoutInfo_ErrorNameInvalid = getValue(dict, "CheckoutInfo.ErrorNameInvalid") - self.Channel_Share = getValue(dict, "Channel.Share") self.Notification_PaymentSent = getValue(dict, "Notification.PaymentSent") self.Settings_Username = getValue(dict, "Settings.Username") self.Notification_CallMissedShort = getValue(dict, "Notification.CallMissedShort") @@ -5257,29 +5018,24 @@ public final class PresentationStrings { self.StickerPack_Share = getValue(dict, "StickerPack.Share") self.Watch_MessageView_Reply = getValue(dict, "Watch.MessageView.Reply") self.Call_AudioRouteSpeaker = getValue(dict, "Call.AudioRouteSpeaker") - self.PrivacySettings_DeleteAccountNever = getValue(dict, "PrivacySettings.DeleteAccountNever") - self._WelcomeScreen_ContactsAccessHelp = getValue(dict, "WelcomeScreen.ContactsAccessHelp") - self._WelcomeScreen_ContactsAccessHelp_r = extractArgumentRanges(self._WelcomeScreen_ContactsAccessHelp) + self.Checkout_Title = getValue(dict, "Checkout.Title") self._MESSAGE_GEO = getValue(dict, "MESSAGE_GEO") self._MESSAGE_GEO_r = extractArgumentRanges(self._MESSAGE_GEO) - self.Checkout_Title = getValue(dict, "Checkout.Title") self.Privacy_Calls = getValue(dict, "Privacy.Calls") self.Channel_AdminLogFilter_EventsInfo = getValue(dict, "Channel.AdminLogFilter.EventsInfo") self._Channel_AdminLog_MessagePinned = getValue(dict, "Channel.AdminLog.MessagePinned") self._Channel_AdminLog_MessagePinned_r = extractArgumentRanges(self._Channel_AdminLog_MessagePinned) self._Channel_AdminLog_MessageToggleInvitesOn = getValue(dict, "Channel.AdminLog.MessageToggleInvitesOn") self._Channel_AdminLog_MessageToggleInvitesOn_r = extractArgumentRanges(self._Channel_AdminLog_MessageToggleInvitesOn) - self.Conversation_SearchWebImages = getValue(dict, "Conversation.SearchWebImages") self.KeyCommand_ScrollDown = getValue(dict, "KeyCommand.ScrollDown") self.Conversation_LinkDialogSave = getValue(dict, "Conversation.LinkDialogSave") - self.Presence_offline = getValue(dict, "Presence.offline") - self.Conversation_SendMessageErrorFlood = getValue(dict, "Conversation.SendMessageErrorFlood") - self._Conversation_ForwardToPersonFormat = getValue(dict, "Conversation.ForwardToPersonFormat") - self._Conversation_ForwardToPersonFormat_r = extractArgumentRanges(self._Conversation_ForwardToPersonFormat) self.CheckoutInfo_ErrorShippingNotAvailable = getValue(dict, "CheckoutInfo.ErrorShippingNotAvailable") - self.SharedMedia_Incoming = getValue(dict, "SharedMedia.Incoming") + self.Conversation_SendMessageErrorFlood = getValue(dict, "Conversation.SendMessageErrorFlood") self._Checkout_SavePasswordTimeoutAndTouchId = getValue(dict, "Checkout.SavePasswordTimeoutAndTouchId") self._Checkout_SavePasswordTimeoutAndTouchId_r = extractArgumentRanges(self._Checkout_SavePasswordTimeoutAndTouchId) + self.HashtagSearch_AllChats = getValue(dict, "HashtagSearch.AllChats") + self._Date_ChatDateHeaderYear = getValue(dict, "Date.ChatDateHeaderYear") + self._Date_ChatDateHeaderYear_r = extractArgumentRanges(self._Date_ChatDateHeaderYear) self.CheckoutInfo_ShippingInfoCountry = getValue(dict, "CheckoutInfo.ShippingInfoCountry") self.Map_ShowPlaces = getValue(dict, "Map.ShowPlaces") self.Camera_VideoMode = getValue(dict, "Camera.VideoMode") @@ -5288,10 +5044,9 @@ public final class PresentationStrings { self.UserInfo_TelegramCall = getValue(dict, "UserInfo.TelegramCall") self.PrivacyLastSeenSettings_CustomShareSettingsHelp = getValue(dict, "PrivacyLastSeenSettings.CustomShareSettingsHelp") self.Channel_AdminLog_InfoPanelAlertText = getValue(dict, "Channel.AdminLog.InfoPanelAlertText") - self.Watch_State_WaitingForNetwork = getValue(dict, "Watch.State.WaitingForNetwork") - self.Cache_Photos = getValue(dict, "Cache.Photos") self._Channel_AdminLog_MessageUnpinned = getValue(dict, "Channel.AdminLog.MessageUnpinned") self._Channel_AdminLog_MessageUnpinned_r = extractArgumentRanges(self._Channel_AdminLog_MessageUnpinned) + self.Cache_Photos = getValue(dict, "Cache.Photos") self.Message_PinnedStickerMessage = getValue(dict, "Message.PinnedStickerMessage") self.PhotoEditor_QualityMedium = getValue(dict, "PhotoEditor.QualityMedium") self.Privacy_PaymentsClearInfo = getValue(dict, "Privacy.PaymentsClearInfo") @@ -5320,13 +5075,11 @@ public final class PresentationStrings { self.Login_Code = getValue(dict, "Login.Code") self.Channel_Username_InvalidCharacters = getValue(dict, "Channel.Username.InvalidCharacters") self.FeatureDisabled_Oops = getValue(dict, "FeatureDisabled.Oops") - self.Login_InviteButton = getValue(dict, "Login.InviteButton") - self.ShareMenu_Send = getValue(dict, "ShareMenu.Send") - self.Conversation_InfoGroup = getValue(dict, "Conversation.InfoGroup") - self.WatchRemote_AlertTitle = getValue(dict, "WatchRemote.AlertTitle") - self.Preview_ProfilePhotoTitle = getValue(dict, "Preview.ProfilePhotoTitle") self.Calls_CallTabTitle = getValue(dict, "Calls.CallTabTitle") + self.ShareMenu_Send = getValue(dict, "ShareMenu.Send") + self.WatchRemote_AlertTitle = getValue(dict, "WatchRemote.AlertTitle") self.Channel_Members_AddBannedErrorAdmin = getValue(dict, "Channel.Members.AddBannedErrorAdmin") + self.Conversation_InfoGroup = getValue(dict, "Conversation.InfoGroup") self.Checkout_Phone = getValue(dict, "Checkout.Phone") self.Channel_SignMessages_Help = getValue(dict, "Channel.SignMessages.Help") self.Calls_SubmitRating = getValue(dict, "Calls.SubmitRating") @@ -5334,33 +5087,25 @@ public final class PresentationStrings { self.Watch_MessageView_Forward = getValue(dict, "Watch.MessageView.Forward") self.GroupInfo_ActionPromote = getValue(dict, "GroupInfo.ActionPromote") self.DialogList_You = getValue(dict, "DialogList.You") - self.Weekday_Monday = getValue(dict, "Weekday.Monday") - self.Watch_Suggestion_Yes = getValue(dict, "Watch.Suggestion.Yes") self.AccessDenied_Camera = getValue(dict, "AccessDenied.Camera") self.WatchRemote_NotificationText = getValue(dict, "WatchRemote.NotificationText") - self.Activity_Location = getValue(dict, "Activity.Location") self.SharedMedia_ViewInChat = getValue(dict, "SharedMedia.ViewInChat") self.Activity_RecordingAudio = getValue(dict, "Activity.RecordingAudio") self.Watch_Stickers_StickerPacks = getValue(dict, "Watch.Stickers.StickerPacks") self._Target_ShareGameConfirmationPrivate = getValue(dict, "Target.ShareGameConfirmationPrivate") self._Target_ShareGameConfirmationPrivate_r = extractArgumentRanges(self._Target_ShareGameConfirmationPrivate) self.Checkout_NewCard_PostcodePlaceholder = getValue(dict, "Checkout.NewCard.PostcodePlaceholder") - self.Conversation_SearchImages = getValue(dict, "Conversation.SearchImages") self.DialogList_DeleteConversationConfirmation = getValue(dict, "DialogList.DeleteConversationConfirmation") self.AttachmentMenu_SendAsFile = getValue(dict, "AttachmentMenu.SendAsFile") - self.Message_GamePreviewLabel = getValue(dict, "Message.GamePreviewLabel") - self.Checkout_ShippingOption_Header = getValue(dict, "Checkout.ShippingOption.Header") self.Watch_Conversation_Unblock = getValue(dict, "Watch.Conversation.Unblock") self.Channel_AdminLog_MessagePreviousLink = getValue(dict, "Channel.AdminLog.MessagePreviousLink") - self.CallSettings_PrivacyDescription = getValue(dict, "CallSettings.PrivacyDescription") self.Conversation_ContextMenuCopy = getValue(dict, "Conversation.ContextMenuCopy") self.GroupInfo_UpgradeButton = getValue(dict, "GroupInfo.UpgradeButton") self.PrivacyLastSeenSettings_NeverShareWith = getValue(dict, "PrivacyLastSeenSettings.NeverShareWith") self.ConvertToSupergroup_HelpText = getValue(dict, "ConvertToSupergroup.HelpText") self.MediaPicker_VideoMuteDescription = getValue(dict, "MediaPicker.VideoMuteDescription") - self._SearchImages_Downloading_Mb = getValue(dict, "SearchImages.Downloading#Mb") - self._SearchImages_Downloading_Mb_r = extractArgumentRanges(self._SearchImages_Downloading_Mb) self.UserInfo_ShareMyContactInfo = getValue(dict, "UserInfo.ShareMyContactInfo") + self.Channel_Info_Stickers = getValue(dict, "Channel.Info.Stickers") self._FileSize_GB = getValue(dict, "FileSize.GB") self._FileSize_GB_r = extractArgumentRanges(self._FileSize_GB) self.Month_ShortJanuary = getValue(dict, "Month.ShortJanuary") @@ -5375,14 +5120,13 @@ public final class PresentationStrings { self.Contacts_InviteSearchLabel = getValue(dict, "Contacts.InviteSearchLabel") self.Tour_StartButton = getValue(dict, "Tour.StartButton") self.CheckoutInfo_Title = getValue(dict, "CheckoutInfo.Title") + self.Conversation_Admin = getValue(dict, "Conversation.Admin") self._Channel_AdminLog_MessageRestrictedNameUsername = getValue(dict, "Channel.AdminLog.MessageRestrictedNameUsername") self._Channel_AdminLog_MessageRestrictedNameUsername_r = extractArgumentRanges(self._Channel_AdminLog_MessageRestrictedNameUsername) self.ChangePhoneNumberCode_Help = getValue(dict, "ChangePhoneNumberCode.Help") self.Web_Error = getValue(dict, "Web.Error") self.ShareFileTip_Title = getValue(dict, "ShareFileTip.Title") self.Username_InvalidStartsWithNumber = getValue(dict, "Username.InvalidStartsWithNumber") - self.ChatSettings_RevertLanguage = getValue(dict, "ChatSettings.RevertLanguage") - self.Conversation_ReportSpamAndLeave = getValue(dict, "Conversation.ReportSpamAndLeave") self._DialogList_EncryptedChatStartedIncoming = getValue(dict, "DialogList.EncryptedChatStartedIncoming") self._DialogList_EncryptedChatStartedIncoming_r = extractArgumentRanges(self._DialogList_EncryptedChatStartedIncoming) self.Calls_AddTab = getValue(dict, "Calls.AddTab") @@ -5397,38 +5141,39 @@ public final class PresentationStrings { self._PINNED_INVOICE_r = extractArgumentRanges(self._PINNED_INVOICE) self.Month_GenFebruary = getValue(dict, "Month.GenFebruary") self.Contacts_SelectAll = getValue(dict, "Contacts.SelectAll") + self.FastTwoStepSetup_EmailHelp = getValue(dict, "FastTwoStepSetup.EmailHelp") self.Month_GenOctober = getValue(dict, "Month.GenOctober") self.CheckoutInfo_ErrorPhoneInvalid = getValue(dict, "CheckoutInfo.ErrorPhoneInvalid") - self.SharedMedia_TitleVideo = getValue(dict, "SharedMedia.TitleVideo") + self.Group_Setup_TypePublic = getValue(dict, "Group.Setup.TypePublic") self.Checkout_PaymentMethod_New = getValue(dict, "Checkout.PaymentMethod.New") self.ShareMenu_Comment = getValue(dict, "ShareMenu.Comment") self.Channel_Management_LabelEditor = getValue(dict, "Channel.Management.LabelEditor") self.TwoStepAuth_SetPasswordHelp = getValue(dict, "TwoStepAuth.SetPasswordHelp") self.Channel_AdminLogFilter_EventsTitle = getValue(dict, "Channel.AdminLogFilter.EventsTitle") + self.NotificationSettings_ContactJoined = getValue(dict, "NotificationSettings.ContactJoined") self.Username_LinkCopied = getValue(dict, "Username.LinkCopied") self._Time_MonthOfYear_m9 = getValue(dict, "Time.MonthOfYear_m9") self._Time_MonthOfYear_m9_r = extractArgumentRanges(self._Time_MonthOfYear_m9) - self.DialogList_Conversations = getValue(dict, "DialogList.Conversations") self.Channel_EditAdmin_PermissionAddAdmins = getValue(dict, "Channel.EditAdmin.PermissionAddAdmins") self.Conversation_SendMessage = getValue(dict, "Conversation.SendMessage") self.Notification_CallIncoming = getValue(dict, "Notification.CallIncoming") self._MESSAGE_FWDS = getValue(dict, "MESSAGE_FWDS") self._MESSAGE_FWDS_r = extractArgumentRanges(self._MESSAGE_FWDS) - self.Conversation_InputTextCommentPlaceholder = getValue(dict, "Conversation.InputTextCommentPlaceholder") self.Map_OpenInYandexMaps = getValue(dict, "Map.OpenInYandexMaps") + self.FastTwoStepSetup_PasswordHelp = getValue(dict, "FastTwoStepSetup.PasswordHelp") + self.GroupInfo_GroupHistoryHidden = getValue(dict, "GroupInfo.GroupHistoryHidden") self.Month_ShortNovember = getValue(dict, "Month.ShortNovember") self.AccessDenied_Settings = getValue(dict, "AccessDenied.Settings") self.EncryptionKey_Title = getValue(dict, "EncryptionKey.Title") self.Profile_MessageLifetime1h = getValue(dict, "Profile.MessageLifetime1h") self._Map_DistanceAway = getValue(dict, "Map.DistanceAway") self._Map_DistanceAway_r = extractArgumentRanges(self._Map_DistanceAway) - self.Compose_NewMessage = getValue(dict, "Compose.NewMessage") self.Checkout_ErrorPaymentFailed = getValue(dict, "Checkout.ErrorPaymentFailed") + self.Compose_NewMessage = getValue(dict, "Compose.NewMessage") + self.Conversation_LiveLocationYou = getValue(dict, "Conversation.LiveLocationYou") self.Map_OpenInWaze = getValue(dict, "Map.OpenInWaze") - self.Common_ChooseVideo = getValue(dict, "Common.ChooseVideo") self.Checkout_ShippingMethod = getValue(dict, "Checkout.ShippingMethod") self.Login_InfoFirstNamePlaceholder = getValue(dict, "Login.InfoFirstNamePlaceholder") - self.DialogList_Broadcast = getValue(dict, "DialogList.Broadcast") self.Checkout_ErrorProviderAccountInvalid = getValue(dict, "Checkout.ErrorProviderAccountInvalid") self.CallSettings_TabIconDescription = getValue(dict, "CallSettings.TabIconDescription") self.Checkout_WebConfirmation_Title = getValue(dict, "Checkout.WebConfirmation.Title") @@ -5438,7 +5183,6 @@ public final class PresentationStrings { self.MessageTimer_Custom = getValue(dict, "MessageTimer.Custom") self.Conversation_SilentBroadcastTooltipOff = getValue(dict, "Conversation.SilentBroadcastTooltipOff") self.Conversation_Mute = getValue(dict, "Conversation.Mute") - self.Call_CallBack = getValue(dict, "Call.CallBack") self.CreateGroup_SoftUserLimitAlert = getValue(dict, "CreateGroup.SoftUserLimitAlert") self.AccessDenied_LocationDenied = getValue(dict, "AccessDenied.LocationDenied") self.Tour_Title6 = getValue(dict, "Tour.Title6") @@ -5449,23 +5193,22 @@ public final class PresentationStrings { self._Channel_AdminLog_MessageDeleted = getValue(dict, "Channel.AdminLog.MessageDeleted") self._Channel_AdminLog_MessageDeleted_r = extractArgumentRanges(self._Channel_AdminLog_MessageDeleted) self.DialogList_DeleteBotConfirmation = getValue(dict, "DialogList.DeleteBotConfirmation") + self.EditProfile_Title = getValue(dict, "EditProfile.Title") + self.PasscodeSettings_HelpTop = getValue(dict, "PasscodeSettings.HelpTop") self.Common_TakePhotoOrVideo = getValue(dict, "Common.TakePhotoOrVideo") self.Notification_MessageLifetime2s = getValue(dict, "Notification.MessageLifetime2s") - self.Conversation_FileGoogleDrive = getValue(dict, "Conversation.FileGoogleDrive") - self._MediaPicker_Processing = getValue(dict, "MediaPicker.Processing") - self._MediaPicker_Processing_r = extractArgumentRanges(self._MediaPicker_Processing) self.Checkout_ErrorGeneric = getValue(dict, "Checkout.ErrorGeneric") self.Channel_AdminLog_CanBanUsers = getValue(dict, "Channel.AdminLog.CanBanUsers") self.Cache_Indexing = getValue(dict, "Cache.Indexing") self._ENCRYPTION_REQUEST = getValue(dict, "ENCRYPTION_REQUEST") self._ENCRYPTION_REQUEST_r = extractArgumentRanges(self._ENCRYPTION_REQUEST) self.StickerSettings_ContextInfo = getValue(dict, "StickerSettings.ContextInfo") - self.Message_SharedContact = getValue(dict, "Message.SharedContact") self.Channel_BanUser_PermissionEmbedLinks = getValue(dict, "Channel.BanUser.PermissionEmbedLinks") - self.Channel_Username_CreateCommentsEnabled = getValue(dict, "Channel.Username.CreateCommentsEnabled") + self.Map_Location = getValue(dict, "Map.Location") self.GroupInfo_InviteLink_LinkSection = getValue(dict, "GroupInfo.InviteLink.LinkSection") self.Privacy_Calls_AlwaysAllow_Placeholder = getValue(dict, "Privacy.Calls.AlwaysAllow.Placeholder") self.CheckoutInfo_ShippingInfoPostcode = getValue(dict, "CheckoutInfo.ShippingInfoPostcode") + self.Group_Setup_HistoryVisibleHelp = getValue(dict, "Group.Setup.HistoryVisibleHelp") self._Time_PreciseDate_m7 = getValue(dict, "Time.PreciseDate_m7") self._Time_PreciseDate_m7_r = extractArgumentRanges(self._Time_PreciseDate_m7) self.PasscodeSettings_EncryptDataHelp = getValue(dict, "PasscodeSettings.EncryptDataHelp") @@ -5474,7 +5217,6 @@ public final class PresentationStrings { self.Cache_KeepMedia = getValue(dict, "Cache.KeepMedia") self.WebPreview_GettingLinkInfo = getValue(dict, "WebPreview.GettingLinkInfo") self.Group_Setup_TypePublicHelp = getValue(dict, "Group.Setup.TypePublicHelp") - self.Channel_Moderator_AccessLevelModeratorHelp = getValue(dict, "Channel.Moderator.AccessLevelModeratorHelp") self.Map_Satellite = getValue(dict, "Map.Satellite") self.Username_InvalidTaken = getValue(dict, "Username.InvalidTaken") self._Notification_PinnedAudioMessage = getValue(dict, "Notification.PinnedAudioMessage") @@ -5500,21 +5242,20 @@ public final class PresentationStrings { self.Privacy_GroupsAndChannels_WhoCanAddMe = getValue(dict, "Privacy.GroupsAndChannels.WhoCanAddMe") self.Login_CodeExpiredError = getValue(dict, "Login.CodeExpiredError") self.Settings_PhoneNumber = getValue(dict, "Settings.PhoneNumber") + self.FastTwoStepSetup_EmailPlaceholder = getValue(dict, "FastTwoStepSetup.EmailPlaceholder") self._DialogList_MultipleTypingSuffix = getValue(dict, "DialogList.MultipleTypingSuffix") self._DialogList_MultipleTypingSuffix_r = extractArgumentRanges(self._DialogList_MultipleTypingSuffix) - self.ChannelMembers_Blacklist_EmptyText = getValue(dict, "ChannelMembers.Blacklist.EmptyText") self.Bot_GenericBotStatus = getValue(dict, "Bot.GenericBotStatus") + self.PrivacySettings_PasscodeAndTouchId = getValue(dict, "PrivacySettings.PasscodeAndTouchId") self.Common_edit = getValue(dict, "Common.edit") self.Settings_AppLanguage = getValue(dict, "Settings.AppLanguage") self.PrivacyLastSeenSettings_WhoCanSeeMyTimestamp = getValue(dict, "PrivacyLastSeenSettings.WhoCanSeeMyTimestamp") self._Notification_Kicked = getValue(dict, "Notification.Kicked") self._Notification_Kicked_r = extractArgumentRanges(self._Notification_Kicked) - self.Conversation_Send = getValue(dict, "Conversation.Send") self.Channel_AdminLog_MessageRestrictedForever = getValue(dict, "Channel.AdminLog.MessageRestrictedForever") self.ChannelInfo_DeleteChannelConfirmation = getValue(dict, "ChannelInfo.DeleteChannelConfirmation") self.Weekday_ShortSaturday = getValue(dict, "Weekday.ShortSaturday") self.Map_SendThisLocation = getValue(dict, "Map.SendThisLocation") - self.DialogList_RecentTitleBots = getValue(dict, "DialogList.RecentTitleBots") self._Notification_PinnedDocumentMessage = getValue(dict, "Notification.PinnedDocumentMessage") self._Notification_PinnedDocumentMessage_r = extractArgumentRanges(self._Notification_PinnedDocumentMessage) self.Conversation_ContextMenuReply = getValue(dict, "Conversation.ContextMenuReply") @@ -5522,16 +5263,12 @@ public final class PresentationStrings { self.NetworkUsageSettings_Wifi = getValue(dict, "NetworkUsageSettings.Wifi") self.Call_Accept = getValue(dict, "Call.Accept") self.GroupInfo_SetGroupPhotoDelete = getValue(dict, "GroupInfo.SetGroupPhotoDelete") + self.Login_PhoneBannedError = getValue(dict, "Login.PhoneBannedError") self.PhotoEditor_CropAuto = getValue(dict, "PhotoEditor.CropAuto") self.PhotoEditor_ContrastTool = getValue(dict, "PhotoEditor.ContrastTool") - self.MediaPicker_MomentsDateYearFormat = getValue(dict, "MediaPicker.MomentsDateYearFormat") self.CheckoutInfo_ReceiverInfoNamePlaceholder = getValue(dict, "CheckoutInfo.ReceiverInfoNamePlaceholder") self.Channel_AdminLog_MessagePreviousCaption = getValue(dict, "Channel.AdminLog.MessagePreviousCaption") self.Privacy_PaymentsClear_ShippingInfo = getValue(dict, "Privacy.PaymentsClear.ShippingInfo") - self.TwoStepAuth_GenericError = getValue(dict, "TwoStepAuth.GenericError") - self.Channel_Moderator_AccessLevelEditorHelp = getValue(dict, "Channel.Moderator.AccessLevelEditorHelp") - self.Compose_NewChannelButton = getValue(dict, "Compose.NewChannelButton") - self.ConversationMedia_EmptyTitle = getValue(dict, "ConversationMedia.EmptyTitle") self.Date_DialogDateFormat = getValue(dict, "Date.DialogDateFormat") self.ReportPeer_ReasonSpam = getValue(dict, "ReportPeer.ReasonSpam") self.Privacy_Calls_P2P = getValue(dict, "Privacy.Calls.P2P") @@ -5539,27 +5276,23 @@ public final class PresentationStrings { self._PINNED_VIDEO = getValue(dict, "PINNED_VIDEO") self._PINNED_VIDEO_r = extractArgumentRanges(self._PINNED_VIDEO) self.StickerPacksSettings_Title = getValue(dict, "StickerPacksSettings.Title") + self.Privacy_PaymentsClearInfoDoneHelp = getValue(dict, "Privacy.PaymentsClearInfoDoneHelp") self.Privacy_Calls_NeverAllow_Placeholder = getValue(dict, "Privacy.Calls.NeverAllow.Placeholder") self.Settings_Support = getValue(dict, "Settings.Support") self.Notification_GroupInviterSelf = getValue(dict, "Notification.GroupInviterSelf") self._SecretImage_NotViewedYet = getValue(dict, "SecretImage.NotViewedYet") self._SecretImage_NotViewedYet_r = extractArgumentRanges(self._SecretImage_NotViewedYet) self.MaskStickerSettings_Title = getValue(dict, "MaskStickerSettings.Title") - self.Watch_Suggestion_ThankYou = getValue(dict, "Watch.Suggestion.ThankYou") self.TwoStepAuth_SetPassword = getValue(dict, "TwoStepAuth.SetPassword") - self.GoogleDrive_LoadErrorMessage = getValue(dict, "GoogleDrive.LoadErrorMessage") self.GroupInfo_InviteLink_ShareLink = getValue(dict, "GroupInfo.InviteLink.ShareLink") - self.ChannelMembers_AllMembersMayInviteOnHelp = getValue(dict, "ChannelMembers.AllMembersMayInviteOnHelp") self.Common_Cancel = getValue(dict, "Common.Cancel") self.UserInfo_About_Placeholder = getValue(dict, "UserInfo.About.Placeholder") - self.Preview_LoadingImages = getValue(dict, "Preview.LoadingImages") self.ChangePhoneNumberCode_RequestingACall = getValue(dict, "ChangePhoneNumberCode.RequestingACall") self.PrivacyLastSeenSettings_NeverShareWith_Title = getValue(dict, "PrivacyLastSeenSettings.NeverShareWith.Title") self.KeyCommand_JumpToNextChat = getValue(dict, "KeyCommand.JumpToNextChat") self._Time_MonthOfYear_m8 = getValue(dict, "Time.MonthOfYear_m8") self._Time_MonthOfYear_m8_r = extractArgumentRanges(self._Time_MonthOfYear_m8) self.Tour_Text1 = getValue(dict, "Tour.Text1") - self.StickerPack_Remove = getValue(dict, "StickerPack.Remove") self.Conversation_HoldForVideo = getValue(dict, "Conversation.HoldForVideo") self.Checkout_NewCard_Title = getValue(dict, "Checkout.NewCard.Title") self.Channel_TitleInfo = getValue(dict, "Channel.TitleInfo") @@ -5574,8 +5307,8 @@ public final class PresentationStrings { self.TwoStepAuth_ChangeEmail = getValue(dict, "TwoStepAuth.ChangeEmail") self._ENCRYPTION_ACCEPT = getValue(dict, "ENCRYPTION_ACCEPT") self._ENCRYPTION_ACCEPT_r = extractArgumentRanges(self._ENCRYPTION_ACCEPT) - self.Conversation_ShareBotLocationConfirmationTitle = getValue(dict, "Conversation.ShareBotLocationConfirmationTitle") self.NetworkUsageSettings_BytesSent = getValue(dict, "NetworkUsageSettings.BytesSent") + self.Conversation_ShareBotLocationConfirmationTitle = getValue(dict, "Conversation.ShareBotLocationConfirmationTitle") self.Conversation_ForwardContacts = getValue(dict, "Conversation.ForwardContacts") self._Notification_ChangedGroupName = getValue(dict, "Notification.ChangedGroupName") self._Notification_ChangedGroupName_r = extractArgumentRanges(self._Notification_ChangedGroupName) @@ -5592,17 +5325,13 @@ public final class PresentationStrings { self.Watch_Compose_CreateMessage = getValue(dict, "Watch.Compose.CreateMessage") self.ChatSettings_ConnectionType_UseProxy = getValue(dict, "ChatSettings.ConnectionType.UseProxy") self.Message_Audio = getValue(dict, "Message.Audio") - self.Notification_CreatedGroup = getValue(dict, "Notification.CreatedGroup") self.Conversation_SearchNoResults = getValue(dict, "Conversation.SearchNoResults") - self.ChannelMembers_BanList_EmptyText = getValue(dict, "ChannelMembers.BanList.EmptyText") self.ReportPeer_ReasonViolence = getValue(dict, "ReportPeer.ReasonViolence") self.Group_Username_RemoveExistingUsernamesInfo = getValue(dict, "Group.Username.RemoveExistingUsernamesInfo") self.Message_InvoiceLabel = getValue(dict, "Message.InvoiceLabel") - self._LastSeen_AtWeekday = getValue(dict, "LastSeen.AtWeekday") - self._LastSeen_AtWeekday_r = extractArgumentRanges(self._LastSeen_AtWeekday) + self.Channel_AdminLogFilter_Title = getValue(dict, "Channel.AdminLogFilter.Title") self.Contacts_SearchLabel = getValue(dict, "Contacts.SearchLabel") self.Group_Username_InvalidStartsWithNumber = getValue(dict, "Group.Username.InvalidStartsWithNumber") - self.Channel_AdminLogFilter_Title = getValue(dict, "Channel.AdminLogFilter.Title") self.ChatAdmins_AllMembersAreAdminsOnHelp = getValue(dict, "ChatAdmins.AllMembersAreAdminsOnHelp") self.Month_ShortSeptember = getValue(dict, "Month.ShortSeptember") self.Group_Username_CreatePublicLinkHelp = getValue(dict, "Group.Username.CreatePublicLinkHelp") @@ -5611,21 +5340,22 @@ public final class PresentationStrings { self.Bot_Unblock = getValue(dict, "Bot.Unblock") self.SharedMedia_CategoryMedia = getValue(dict, "SharedMedia.CategoryMedia") self.Conversation_HoldForAudio = getValue(dict, "Conversation.HoldForAudio") + self.Conversation_ClousStorageInfo_Description1 = getValue(dict, "Conversation.ClousStorageInfo.Description1") self.Channel_Members_InviteLink = getValue(dict, "Channel.Members.InviteLink") self.Core_ServiceUserStatus = getValue(dict, "Core.ServiceUserStatus") self.WebSearch_RecentClearConfirmation = getValue(dict, "WebSearch.RecentClearConfirmation") - self.Conversation_ClousStorageInfo_Description1 = getValue(dict, "Conversation.ClousStorageInfo.Description1") self.Notification_ChannelMigratedFrom = getValue(dict, "Notification.ChannelMigratedFrom") self.Settings_Title = getValue(dict, "Settings.Title") self.Call_StatusBusy = getValue(dict, "Call.StatusBusy") - self.ConversationMedia_Title = getValue(dict, "ConversationMedia.Title") self.ArchivedPacksAlert_Title = getValue(dict, "ArchivedPacksAlert.Title") + self.ConversationMedia_Title = getValue(dict, "ConversationMedia.Title") self._Conversation_MessageViaUser = getValue(dict, "Conversation.MessageViaUser") self._Conversation_MessageViaUser_r = extractArgumentRanges(self._Conversation_MessageViaUser) - self.Presence_invisible = getValue(dict, "Presence.invisible") - self.DialogList_Create = getValue(dict, "DialogList.Create") self.Tour_Title4 = getValue(dict, "Tour.Title4") self.Call_StatusEnded = getValue(dict, "Call.StatusEnded") + self.LiveLocationUpdated_JustNow = getValue(dict, "LiveLocationUpdated.JustNow") + self._Login_BannedPhoneSubject = getValue(dict, "Login.BannedPhoneSubject") + self._Login_BannedPhoneSubject_r = extractArgumentRanges(self._Login_BannedPhoneSubject) self._Channel_Management_RestrictedBy = getValue(dict, "Channel.Management.RestrictedBy") self._Channel_Management_RestrictedBy_r = extractArgumentRanges(self._Channel_Management_RestrictedBy) self.Conversation_UnpinMessageAlert = getValue(dict, "Conversation.UnpinMessageAlert") @@ -5652,10 +5382,7 @@ public final class PresentationStrings { self._DialogList_SinglePlayingGameSuffix_r = extractArgumentRanges(self._DialogList_SinglePlayingGameSuffix) self.AttachmentMenu_SendAsFiles = getValue(dict, "AttachmentMenu.SendAsFiles") self.Profile_MessageLifetime1m = getValue(dict, "Profile.MessageLifetime1m") - self.DialogList_SelectContact = getValue(dict, "DialogList.SelectContact") self.Settings_AppleWatch = getValue(dict, "Settings.AppleWatch") - self.Conversation_View = getValue(dict, "Conversation.View") - self.Contacts_Invite = getValue(dict, "Contacts.Invite") self.Channel_AdminLog_MessagePreviousDescription = getValue(dict, "Channel.AdminLog.MessagePreviousDescription") self.Your_card_was_declined = getValue(dict, "Your_card_was_declined") self.PhoneNumberHelp_ChangeNumber = getValue(dict, "PhoneNumberHelp.ChangeNumber") @@ -5667,42 +5394,40 @@ public final class PresentationStrings { self.Notifications_GroupNotificationsPreview = getValue(dict, "Notifications.GroupNotificationsPreview") self.Message_PinnedLocationMessage = getValue(dict, "Message.PinnedLocationMessage") self.Settings_Logout = getValue(dict, "Settings.Logout") + self._UserInfo_BlockConfirmation = getValue(dict, "UserInfo.BlockConfirmation") + self._UserInfo_BlockConfirmation_r = extractArgumentRanges(self._UserInfo_BlockConfirmation) self.Profile_Username = getValue(dict, "Profile.Username") self.Group_Username_InvalidTooShort = getValue(dict, "Group.Username.InvalidTooShort") self.AuthSessions_TerminateOtherSessions = getValue(dict, "AuthSessions.TerminateOtherSessions") self.PasscodeSettings_TryAgainIn1Minute = getValue(dict, "PasscodeSettings.TryAgainIn1Minute") self.Notifications_InAppNotifications = getValue(dict, "Notifications.InAppNotifications") - self.Channels_Title = getValue(dict, "Channels.Title") self.StickerPack_ViewPack = getValue(dict, "StickerPack.ViewPack") self.EnterPasscode_ChangeTitle = getValue(dict, "EnterPasscode.ChangeTitle") self.Call_Decline = getValue(dict, "Call.Decline") self.UserInfo_AddPhone = getValue(dict, "UserInfo.AddPhone") - self.Web_CopyLink = getValue(dict, "Web.CopyLink") self.Activity_PlayingGame = getValue(dict, "Activity.PlayingGame") self.CheckoutInfo_ShippingInfoStatePlaceholder = getValue(dict, "CheckoutInfo.ShippingInfoStatePlaceholder") self.Notifications_MessageNotificationsSound = getValue(dict, "Notifications.MessageNotificationsSound") self.Call_StatusWaiting = getValue(dict, "Call.StatusWaiting") self.Weekday_ShortWednesday = getValue(dict, "Weekday.ShortWednesday") - self.DC_UPDATE = getValue(dict, "DC_UPDATE") - self.PasscodeSettings_AutoLock_IfAwayFor_5hours = getValue(dict, "PasscodeSettings.AutoLock.IfAwayFor_5hours") self.Notifications_Title = getValue(dict, "Notifications.Title") + self.PasscodeSettings_AutoLock_IfAwayFor_5hours = getValue(dict, "PasscodeSettings.AutoLock.IfAwayFor_5hours") self.Conversation_PinnedMessage = getValue(dict, "Conversation.PinnedMessage") + self.Channel_AdminLog_MessagePreviousMessage = getValue(dict, "Channel.AdminLog.MessagePreviousMessage") self._Time_MonthOfYear_m12 = getValue(dict, "Time.MonthOfYear_m12") self._Time_MonthOfYear_m12_r = extractArgumentRanges(self._Time_MonthOfYear_m12) self.ConversationProfile_LeaveDeleteAndExit = getValue(dict, "ConversationProfile.LeaveDeleteAndExit") self.State_connecting = getValue(dict, "State.connecting") - self.Channel_AdminLog_MessagePreviousMessage = getValue(dict, "Channel.AdminLog.MessagePreviousMessage") - self.WebPreview_LinkPreview = getValue(dict, "WebPreview.LinkPreview") self.Map_OpenInHereMaps = getValue(dict, "Map.OpenInHereMaps") + self.Stickers_FavoriteStickers = getValue(dict, "Stickers.FavoriteStickers") self.CheckoutInfo_Pay = getValue(dict, "CheckoutInfo.Pay") - self.DialogList_Messages = getValue(dict, "DialogList.Messages") self.Login_CountryCode = getValue(dict, "Login.CountryCode") + self.PasscodeSettings_AutoLock_IfAwayFor_1hour = getValue(dict, "PasscodeSettings.AutoLock.IfAwayFor_1hour") self.CheckoutInfo_ShippingInfoState = getValue(dict, "CheckoutInfo.ShippingInfoState") - self.Map_OpenInGooglePlus = getValue(dict, "Map.OpenInGooglePlus") self._CHAT_MESSAGE_AUDIO = getValue(dict, "CHAT_MESSAGE_AUDIO") self._CHAT_MESSAGE_AUDIO_r = extractArgumentRanges(self._CHAT_MESSAGE_AUDIO) - self.Preview_SaveToCameraRoll = getValue(dict, "Preview.SaveToCameraRoll") self.Login_SmsRequestState2 = getValue(dict, "Login.SmsRequestState2") + self.Preview_SaveToCameraRoll = getValue(dict, "Preview.SaveToCameraRoll") self.PasscodeSettings_ChangePasscode = getValue(dict, "PasscodeSettings.ChangePasscode") self.TwoStepAuth_RecoveryCodeInvalid = getValue(dict, "TwoStepAuth.RecoveryCodeInvalid") self._Message_PaymentSent = getValue(dict, "Message.PaymentSent") @@ -5712,8 +5437,6 @@ public final class PresentationStrings { self._Conversation_RestrictedMediaTimed = getValue(dict, "Conversation.RestrictedMediaTimed") self._Conversation_RestrictedMediaTimed_r = extractArgumentRanges(self._Conversation_RestrictedMediaTimed) self.Login_InfoDeletePhoto = getValue(dict, "Login.InfoDeletePhoto") - self.Group_Members_AddMemberErrorNotAllowed = getValue(dict, "Group.Members.AddMemberErrorNotAllowed") - self.Settings_SaveIncomingPhotosHelp = getValue(dict, "Settings.SaveIncomingPhotosHelp") self.TwoStepAuth_RecoveryCodeExpired = getValue(dict, "TwoStepAuth.RecoveryCodeExpired") self.TwoStepAuth_EmailTitle = getValue(dict, "TwoStepAuth.EmailTitle") self.Privacy_GroupsAndChannels_NeverAllow = getValue(dict, "Privacy.GroupsAndChannels.NeverAllow") @@ -5722,37 +5445,34 @@ public final class PresentationStrings { self._Time_MonthOfYear_m7 = getValue(dict, "Time.MonthOfYear_m7") self._Time_MonthOfYear_m7_r = extractArgumentRanges(self._Time_MonthOfYear_m7) self.PhotoEditor_QualityLow = getValue(dict, "PhotoEditor.QualityLow") - self.State_ConnectingToProxyInfo = getValue(dict, "State.ConnectingToProxyInfo") self.Paint_Outlined = getValue(dict, "Paint.Outlined") + self.State_ConnectingToProxyInfo = getValue(dict, "State.ConnectingToProxyInfo") self.Checkout_PasswordEntry_Title = getValue(dict, "Checkout.PasswordEntry.Title") self.Common_Done = getValue(dict, "Common.Done") self.PrivacySettings_LastSeenContacts = getValue(dict, "PrivacySettings.LastSeenContacts") self.CheckoutInfo_ShippingInfoAddress1 = getValue(dict, "CheckoutInfo.ShippingInfoAddress1") self.UserInfo_LastNamePlaceholder = getValue(dict, "UserInfo.LastNamePlaceholder") - self.GroupInfo_InviteLink_RevokeAlert_Text = getValue(dict, "GroupInfo.InviteLink.RevokeAlert.Text") self.Conversation_StatusKickedFromChannel = getValue(dict, "Conversation.StatusKickedFromChannel") + self.CheckoutInfo_ShippingInfoAddress2 = getValue(dict, "CheckoutInfo.ShippingInfoAddress2") self._DialogList_SingleTypingSuffix = getValue(dict, "DialogList.SingleTypingSuffix") self._DialogList_SingleTypingSuffix_r = extractArgumentRanges(self._DialogList_SingleTypingSuffix) self.LastSeen_JustNow = getValue(dict, "LastSeen.JustNow") - self.CheckoutInfo_ShippingInfoAddress2 = getValue(dict, "CheckoutInfo.ShippingInfoAddress2") - self.Watch_Suggestion_No = getValue(dict, "Watch.Suggestion.No") + self.GroupInfo_InviteLink_RevokeAlert_Text = getValue(dict, "GroupInfo.InviteLink.RevokeAlert.Text") self.BroadcastListInfo_AddRecipient = getValue(dict, "BroadcastListInfo.AddRecipient") self._Channel_Management_ErrorNotMember = getValue(dict, "Channel.Management.ErrorNotMember") self._Channel_Management_ErrorNotMember_r = extractArgumentRanges(self._Channel_Management_ErrorNotMember) self.Privacy_Calls_NeverAllow = getValue(dict, "Privacy.Calls.NeverAllow") self.Settings_About_Title = getValue(dict, "Settings.About.Title") self.PhoneNumberHelp_Help = getValue(dict, "PhoneNumberHelp.Help") - self.Service_NetworkConfigurationUpdatedMessage = getValue(dict, "Service.NetworkConfigurationUpdatedMessage") self.Channel_LinkItem = getValue(dict, "Channel.LinkItem") self.Camera_Retake = getValue(dict, "Camera.Retake") - self.StickerPack_ShowStickers = getValue(dict, "StickerPack.ShowStickers") self.Conversation_RestrictedText = getValue(dict, "Conversation.RestrictedText") + self.Channel_Stickers_YourStickers = getValue(dict, "Channel.Stickers.YourStickers") self._CHAT_CREATED = getValue(dict, "CHAT_CREATED") self._CHAT_CREATED_r = extractArgumentRanges(self._CHAT_CREATED) self.LastSeen_WithinAMonth = getValue(dict, "LastSeen.WithinAMonth") self._PrivacySettings_LastSeenContactsPlus = getValue(dict, "PrivacySettings.LastSeenContactsPlus") self._PrivacySettings_LastSeenContactsPlus_r = extractArgumentRanges(self._PrivacySettings_LastSeenContactsPlus) - self.Conversation_FileHowTo = getValue(dict, "Conversation.FileHowTo") self.ChangePhoneNumberNumber_NewNumber = getValue(dict, "ChangePhoneNumberNumber.NewNumber") self.Compose_NewChannel = getValue(dict, "Compose.NewChannel") self.Channel_AdminLog_CanChangeInviteLink = getValue(dict, "Channel.AdminLog.CanChangeInviteLink") @@ -5764,13 +5484,10 @@ public final class PresentationStrings { self._CancelResetAccount_TextSMS = getValue(dict, "CancelResetAccount.TextSMS") self._CancelResetAccount_TextSMS_r = extractArgumentRanges(self._CancelResetAccount_TextSMS) self.Channel_EditAdmin_PermissionInviteUsers = getValue(dict, "Channel.EditAdmin.PermissionInviteUsers") - self.Conversation_Document = getValue(dict, "Conversation.Document") - self.SearchImages_RetryDownload = getValue(dict, "SearchImages.RetryDownload") self.GroupInfo_DeleteAndExit = getValue(dict, "GroupInfo.DeleteAndExit") self.GroupInfo_InviteLink_CopyLink = getValue(dict, "GroupInfo.InviteLink.CopyLink") - self.Weekday_Friday = getValue(dict, "Weekday.Friday") - self.Settings_SetProfilePhoto = getValue(dict, "Settings.SetProfilePhoto") self.Login_ResetAccountProtected_Title = getValue(dict, "Login.ResetAccountProtected.Title") + self.Settings_SetProfilePhoto = getValue(dict, "Settings.SetProfilePhoto") self.Compose_ChannelTokenListPlaceholder = getValue(dict, "Compose.ChannelTokenListPlaceholder") self.Channel_EditAdmin_PermissionPinMessages = getValue(dict, "Channel.EditAdmin.PermissionPinMessages") self.Your_card_has_expired = getValue(dict, "Your_card_has_expired") @@ -5782,23 +5499,24 @@ public final class PresentationStrings { self._Username_UsernameIsAvailable = getValue(dict, "Username.UsernameIsAvailable") self._Username_UsernameIsAvailable_r = extractArgumentRanges(self._Username_UsernameIsAvailable) self.KeyCommand_JumpToNextUnreadChat = getValue(dict, "KeyCommand.JumpToNextUnreadChat") + self._Date_ChatDateHeader = getValue(dict, "Date.ChatDateHeader") + self._Date_ChatDateHeader_r = extractArgumentRanges(self._Date_ChatDateHeader) self.Conversation_EncryptedDescriptionTitle = getValue(dict, "Conversation.EncryptedDescriptionTitle") + self.DialogList_Pin = getValue(dict, "DialogList.Pin") self._Notification_RemovedGroupPhoto = getValue(dict, "Notification.RemovedGroupPhoto") self._Notification_RemovedGroupPhoto_r = extractArgumentRanges(self._Notification_RemovedGroupPhoto) - self.GroupInfo_SharedMediaNone = getValue(dict, "GroupInfo.SharedMediaNone") self.Channel_ErrorAddTooMuch = getValue(dict, "Channel.ErrorAddTooMuch") - self.DialogList_Pin = getValue(dict, "DialogList.Pin") + self.GroupInfo_SharedMediaNone = getValue(dict, "GroupInfo.SharedMediaNone") self.ChatSettings_TextSizeUnits = getValue(dict, "ChatSettings.TextSizeUnits") self.ChatSettings_AutoPlayAnimations = getValue(dict, "ChatSettings.AutoPlayAnimations") self.Conversation_FileOpenIn = getValue(dict, "Conversation.FileOpenIn") self.Channel_Setup_TypePublic = getValue(dict, "Channel.Setup.TypePublic") self._ChangePhone_ErrorOccupied = getValue(dict, "ChangePhone.ErrorOccupied") self._ChangePhone_ErrorOccupied_r = extractArgumentRanges(self._ChangePhone_ErrorOccupied) - self.DialogList_RecentTitleGroups = getValue(dict, "DialogList.RecentTitleGroups") + self.Clipboard_SendPhoto = getValue(dict, "Clipboard.SendPhoto") self.Privacy_GroupsAndChannels_CustomShareHelp = getValue(dict, "Privacy.GroupsAndChannels.CustomShareHelp") self.KeyCommand_ChatInfo = getValue(dict, "KeyCommand.ChatInfo") self.Channel_AdminLog_EmptyFilterTitle = getValue(dict, "Channel.AdminLog.EmptyFilterTitle") - self.Notification_CreatedBroadcastList = getValue(dict, "Notification.CreatedBroadcastList") self.PhotoEditor_HighlightsTint = getValue(dict, "PhotoEditor.HighlightsTint") self.Watch_Compose_AddContact = getValue(dict, "Watch.Compose.AddContact") self._Time_PreciseDate_m5 = getValue(dict, "Time.PreciseDate_m5") @@ -5819,12 +5537,9 @@ public final class PresentationStrings { self._Generic_OpenHiddenLinkAlert = getValue(dict, "Generic.OpenHiddenLinkAlert") self._Generic_OpenHiddenLinkAlert_r = extractArgumentRanges(self._Generic_OpenHiddenLinkAlert) self.Conversation_Contact = getValue(dict, "Conversation.Contact") - self.Service_ApplyLocalization = getValue(dict, "Service.ApplyLocalization") self.NetworkUsageSettings_GeneralDataSection = getValue(dict, "NetworkUsageSettings.GeneralDataSection") - self._StickerPack_RemovePrompt = getValue(dict, "StickerPack.RemovePrompt") - self._StickerPack_RemovePrompt_r = extractArgumentRanges(self._StickerPack_RemovePrompt) - self.Channel_NotificationCommentsDisabled = getValue(dict, "Channel.NotificationCommentsDisabled") self.EnterPasscode_RepeatNewPasscode = getValue(dict, "EnterPasscode.RepeatNewPasscode") + self.Conversation_ContextMenuCopyLink = getValue(dict, "Conversation.ContextMenuCopyLink") self.InstantPage_AutoNightTheme = getValue(dict, "InstantPage.AutoNightTheme") self.CloudStorage_Title = getValue(dict, "CloudStorage.Title") self.Month_ShortOctober = getValue(dict, "Month.ShortOctober") @@ -5836,31 +5551,27 @@ public final class PresentationStrings { self.Tour_Text6 = getValue(dict, "Tour.Text6") self.PhotoEditor_WarmthTool = getValue(dict, "PhotoEditor.WarmthTool") self.Common_TakePhoto = getValue(dict, "Common.TakePhoto") - self.PhotoEditor_Current = getValue(dict, "PhotoEditor.Current") self.UserInfo_CreateNewContact = getValue(dict, "UserInfo.CreateNewContact") + self.NetworkUsageSettings_MediaDocumentDataSection = getValue(dict, "NetworkUsageSettings.MediaDocumentDataSection") + self.Login_CodeSentCall = getValue(dict, "Login.CodeSentCall") self.Watch_PhotoView_Title = getValue(dict, "Watch.PhotoView.Title") self._PrivacySettings_LastSeenContactsMinus = getValue(dict, "PrivacySettings.LastSeenContactsMinus") self._PrivacySettings_LastSeenContactsMinus_r = extractArgumentRanges(self._PrivacySettings_LastSeenContactsMinus) - self.Login_InfoUpdatePhoto = getValue(dict, "Login.InfoUpdatePhoto") - self.Login_CodeSentCall = getValue(dict, "Login.CodeSentCall") self.ShareMenu_SelectChats = getValue(dict, "ShareMenu.SelectChats") - self.NetworkUsageSettings_MediaDocumentDataSection = getValue(dict, "NetworkUsageSettings.MediaDocumentDataSection") self.Group_ErrorSendRestrictedMedia = getValue(dict, "Group.ErrorSendRestrictedMedia") + self.Group_Setup_HistoryVisible = getValue(dict, "Group.Setup.HistoryVisible") self.Channel_EditAdmin_PermissinAddAdminOff = getValue(dict, "Channel.EditAdmin.PermissinAddAdminOff") self.Cache_Files = getValue(dict, "Cache.Files") self.PhotoEditor_EnhanceTool = getValue(dict, "PhotoEditor.EnhanceTool") self.Conversation_SearchPlaceholder = getValue(dict, "Conversation.SearchPlaceholder") - self.Calls_Search = getValue(dict, "Calls.Search") - self.BroadcastListInfo_Title = getValue(dict, "BroadcastListInfo.Title") + self.Channel_Stickers_NotFound = getValue(dict, "Channel.Stickers.NotFound") self.WatchRemote_AlertText = getValue(dict, "WatchRemote.AlertText") self.Channel_AdminLog_CanInviteUsers = getValue(dict, "Channel.AdminLog.CanInviteUsers") - self.Conversation_Block = getValue(dict, "Conversation.Block") - self.AttachmentMenu_PhotoOrVideo = getValue(dict, "AttachmentMenu.PhotoOrVideo") self.Channel_BanUser_PermissionReadMessages = getValue(dict, "Channel.BanUser.PermissionReadMessages") + self.AttachmentMenu_PhotoOrVideo = getValue(dict, "AttachmentMenu.PhotoOrVideo") self.Month_ShortMarch = getValue(dict, "Month.ShortMarch") self.GroupInfo_InviteLink_Title = getValue(dict, "GroupInfo.InviteLink.Title") self.Watch_LastSeen_JustNow = getValue(dict, "Watch.LastSeen.JustNow") - self.BroadcastLists_Title = getValue(dict, "BroadcastLists.Title") self.PhoneLabel_Title = getValue(dict, "PhoneLabel.Title") self.PrivacySettings_Passcode = getValue(dict, "PrivacySettings.Passcode") self.Paint_ClearConfirm = getValue(dict, "Paint.ClearConfirm") @@ -5875,8 +5586,6 @@ public final class PresentationStrings { self._MESSAGE_PHOTO_SECRET_r = extractArgumentRanges(self._MESSAGE_PHOTO_SECRET) self.Login_PhoneAndCountryHelp = getValue(dict, "Login.PhoneAndCountryHelp") self.CheckoutInfo_ReceiverInfoName = getValue(dict, "CheckoutInfo.ReceiverInfoName") - self._LastSeen_TodayAt = getValue(dict, "LastSeen.TodayAt") - self._LastSeen_TodayAt_r = extractArgumentRanges(self._LastSeen_TodayAt) self._Time_YesterdayAt = getValue(dict, "Time.YesterdayAt") self._Time_YesterdayAt_r = extractArgumentRanges(self._Time_YesterdayAt) self.Weekday_Yesterday = getValue(dict, "Weekday.Yesterday") @@ -5884,7 +5593,6 @@ public final class PresentationStrings { self.Embed_PlayingInPIP = getValue(dict, "Embed.PlayingInPIP") self.Localization_EnglishLanguageName = getValue(dict, "Localization.EnglishLanguageName") self.Call_StatusIncoming = getValue(dict, "Call.StatusIncoming") - self.Conversation_Play = getValue(dict, "Conversation.Play") self.Settings_PrivacySettings = getValue(dict, "Settings.PrivacySettings") self.Conversation_SilentBroadcastTooltipOn = getValue(dict, "Conversation.SilentBroadcastTooltipOn") self._SecretVideo_NotViewedYet = getValue(dict, "SecretVideo.NotViewedYet") @@ -5896,7 +5604,6 @@ public final class PresentationStrings { self.Channel_AdminLog_BanSendMessages = getValue(dict, "Channel.AdminLog.BanSendMessages") self.Channel_MessagePhotoRemoved = getValue(dict, "Channel.MessagePhotoRemoved") self.Conversation_StatusKickedFromGroup = getValue(dict, "Conversation.StatusKickedFromGroup") - self.Compose_NewChannel_AddMemberHelp = getValue(dict, "Compose.NewChannel.AddMemberHelp") self.GroupInfo_ChatAdmins = getValue(dict, "GroupInfo.ChatAdmins") self.PhotoEditor_CurvesAll = getValue(dict, "PhotoEditor.CurvesAll") self._Notification_LeftChannel = getValue(dict, "Notification.LeftChannel") @@ -5911,20 +5618,16 @@ public final class PresentationStrings { self._Call_GroupFormat_r = extractArgumentRanges(self._Call_GroupFormat) self.Forward_ChannelReadOnly = getValue(dict, "Forward.ChannelReadOnly") self.Privacy_GroupsAndChannels_NeverAllow_Title = getValue(dict, "Privacy.GroupsAndChannels.NeverAllow.Title") - self.Conversation_StatusGroupDeactivated = getValue(dict, "Conversation.StatusGroupDeactivated") - self._CHAT_JOINED = getValue(dict, "CHAT_JOINED") - self._CHAT_JOINED_r = extractArgumentRanges(self._CHAT_JOINED) self._Channel_AdminLog_MessageInvitedName = getValue(dict, "Channel.AdminLog.MessageInvitedName") self._Channel_AdminLog_MessageInvitedName_r = extractArgumentRanges(self._Channel_AdminLog_MessageInvitedName) self.Conversation_Moderate_Ban = getValue(dict, "Conversation.Moderate.Ban") self.Group_Status = getValue(dict, "Group.Status") - self.Watch_Suggestion_Absolutely = getValue(dict, "Watch.Suggestion.Absolutely") self.Conversation_InputTextPlaceholder = getValue(dict, "Conversation.InputTextPlaceholder") - self.SharedMedia_TitleAudio = getValue(dict, "SharedMedia.TitleAudio") self.TwoStepAuth_RecoveryCode = getValue(dict, "TwoStepAuth.RecoveryCode") self.SharedMedia_CategoryDocs = getValue(dict, "SharedMedia.CategoryDocs") self.Channel_AdminLog_CanChangeInfo = getValue(dict, "Channel.AdminLog.CanChangeInfo") self.Channel_AdminLogFilter_EventsAdmins = getValue(dict, "Channel.AdminLogFilter.EventsAdmins") + self.Group_Setup_HistoryHiddenHelp = getValue(dict, "Group.Setup.HistoryHiddenHelp") self._AuthSessions_AppUnofficial = getValue(dict, "AuthSessions.AppUnofficial") self._AuthSessions_AppUnofficial_r = extractArgumentRanges(self._AuthSessions_AppUnofficial) self.Conversation_ContextMenuBan = getValue(dict, "Conversation.ContextMenuBan") @@ -5942,24 +5645,23 @@ public final class PresentationStrings { self.Channel_Info_Members = getValue(dict, "Channel.Info.Members") self.ShareFileTip_CloseTip = getValue(dict, "ShareFileTip.CloseTip") self.KeyCommand_Find = getValue(dict, "KeyCommand.Find") - self.Preview_VideoNotYetDownloaded = getValue(dict, "Preview.VideoNotYetDownloaded") self.SecretVideo_Title = getValue(dict, "SecretVideo.Title") self.Checkout_NewCard_PostcodeTitle = getValue(dict, "Checkout.NewCard.PostcodeTitle") self._Channel_AdminLog_MessageRestricted = getValue(dict, "Channel.AdminLog.MessageRestricted") self._Channel_AdminLog_MessageRestricted_r = extractArgumentRanges(self._Channel_AdminLog_MessageRestricted) self.Channel_EditAdmin_PermissinAddAdminOn = getValue(dict, "Channel.EditAdmin.PermissinAddAdminOn") self.WebSearch_GIFs = getValue(dict, "WebSearch.GIFs") + self.Conversation_SavedMessages = getValue(dict, "Conversation.SavedMessages") self.TwoStepAuth_EnterPasswordTitle = getValue(dict, "TwoStepAuth.EnterPasswordTitle") self._CHANNEL_MESSAGE_GAME = getValue(dict, "CHANNEL_MESSAGE_GAME") self._CHANNEL_MESSAGE_GAME_r = extractArgumentRanges(self._CHANNEL_MESSAGE_GAME) + self.Channel_Subscribers_Title = getValue(dict, "Channel.Subscribers.Title") self.AccessDenied_CallMicrophone = getValue(dict, "AccessDenied.CallMicrophone") self.Conversation_DeleteMessagesForEveryone = getValue(dict, "Conversation.DeleteMessagesForEveryone") self.UserInfo_TapToCall = getValue(dict, "UserInfo.TapToCall") self.Common_Edit = getValue(dict, "Common.Edit") self.Conversation_OpenFile = getValue(dict, "Conversation.OpenFile") self.Message_PinnedDocumentMessage = getValue(dict, "Message.PinnedDocumentMessage") - self.Channel_ShareChannel = getValue(dict, "Channel.ShareChannel") - self.PrivacySettings_DeleteAccountNowConfirmation = getValue(dict, "PrivacySettings.DeleteAccountNowConfirmation") self.Checkout_TotalPaidAmount = getValue(dict, "Checkout.TotalPaidAmount") self.Conversation_UnsupportedMedia = getValue(dict, "Conversation.UnsupportedMedia") self._Message_ForwardedMessage = getValue(dict, "Message.ForwardedMessage") @@ -5974,30 +5676,30 @@ public final class PresentationStrings { self.Profile_CreateEncryptedChatError = getValue(dict, "Profile.CreateEncryptedChatError") self.Map_LocationTitle = getValue(dict, "Map.LocationTitle") self.Call_RateCall = getValue(dict, "Call.RateCall") - self.Compose_Recipients = getValue(dict, "Compose.Recipients") self.Message_ReplyActionButtonShowReceipt = getValue(dict, "Message.ReplyActionButtonShowReceipt") self.PhotoEditor_ShadowsTool = getValue(dict, "PhotoEditor.ShadowsTool") self.Checkout_NewCard_CardholderNamePlaceholder = getValue(dict, "Checkout.NewCard.CardholderNamePlaceholder") self.Cache_Title = getValue(dict, "Cache.Title") self.Month_GenMay = getValue(dict, "Month.GenMay") + self.PasscodeSettings_HelpBottom = getValue(dict, "PasscodeSettings.HelpBottom") self._Notification_CreatedChat = getValue(dict, "Notification.CreatedChat") self._Notification_CreatedChat_r = extractArgumentRanges(self._Notification_CreatedChat) self.Calls_NoMissedCallsPlacehoder = getValue(dict, "Calls.NoMissedCallsPlacehoder") + self.Channel_Stickers_NotFoundHelp = getValue(dict, "Channel.Stickers.NotFoundHelp") self.Watch_UserInfo_Block = getValue(dict, "Watch.UserInfo.Block") self.Watch_LastSeen_ALongTimeAgo = getValue(dict, "Watch.LastSeen.ALongTimeAgo") self.StickerPacksSettings_ManagingHelp = getValue(dict, "StickerPacksSettings.ManagingHelp") self.Privacy_GroupsAndChannels_InviteToChannelMultipleError = getValue(dict, "Privacy.GroupsAndChannels.InviteToChannelMultipleError") - self.PrivacySettings_TouchIdEnable = getValue(dict, "PrivacySettings.TouchIdEnable") self.SearchImages_Title = getValue(dict, "SearchImages.Title") self.Channel_BlackList_Title = getValue(dict, "Channel.BlackList.Title") + self._Conversation_LiveLocationYouAnd = getValue(dict, "Conversation.LiveLocationYouAnd") + self._Conversation_LiveLocationYouAnd_r = extractArgumentRanges(self._Conversation_LiveLocationYouAnd) self.Checkout_NewCard_SaveInfo = getValue(dict, "Checkout.NewCard.SaveInfo") self.Notification_CallMissed = getValue(dict, "Notification.CallMissed") self.Profile_ShareContactButton = getValue(dict, "Profile.ShareContactButton") self.Group_ErrorSendRestrictedStickers = getValue(dict, "Group.ErrorSendRestrictedStickers") self.Bot_GroupStatusDoesNotReadHistory = getValue(dict, "Bot.GroupStatusDoesNotReadHistory") self.Notification_Mute1h = getValue(dict, "Notification.Mute1h") - self.Cache_ClearCacheAlert = getValue(dict, "Cache.ClearCacheAlert") - self.BroadcastLists_NoListsYet = getValue(dict, "BroadcastLists.NoListsYet") self.Settings_TabTitle = getValue(dict, "Settings.TabTitle") self.NetworkUsageSettings_MediaAudioDataSection = getValue(dict, "NetworkUsageSettings.MediaAudioDataSection") self.GroupInfo_DeactivatedStatus = getValue(dict, "GroupInfo.DeactivatedStatus") @@ -6006,27 +5708,27 @@ public final class PresentationStrings { self.Conversation_ContextMenuMore = getValue(dict, "Conversation.ContextMenuMore") self._PrivacySettings_LastSeenEverybodyMinus = getValue(dict, "PrivacySettings.LastSeenEverybodyMinus") self._PrivacySettings_LastSeenEverybodyMinus_r = extractArgumentRanges(self._PrivacySettings_LastSeenEverybodyMinus) + self.Map_ShareLiveLocation = getValue(dict, "Map.ShareLiveLocation") self.Weekday_Today = getValue(dict, "Weekday.Today") + self._PINNED_GEOLIVE = getValue(dict, "PINNED_GEOLIVE") + self._PINNED_GEOLIVE_r = extractArgumentRanges(self._PINNED_GEOLIVE) self._Conversation_RestrictedStickersTimed = getValue(dict, "Conversation.RestrictedStickersTimed") self._Conversation_RestrictedStickersTimed_r = extractArgumentRanges(self._Conversation_RestrictedStickersTimed) self.Login_InvalidFirstNameError = getValue(dict, "Login.InvalidFirstNameError") self._Notification_Joined = getValue(dict, "Notification.Joined") self._Notification_Joined_r = extractArgumentRanges(self._Notification_Joined) - self._VideoPreview_OptionHD = getValue(dict, "VideoPreview.OptionHD") - self._VideoPreview_OptionHD_r = extractArgumentRanges(self._VideoPreview_OptionHD) - self.TwoStepAuth_RecoveryFailed = getValue(dict, "TwoStepAuth.RecoveryFailed") self.Paint_Clear = getValue(dict, "Paint.Clear") + self.TwoStepAuth_RecoveryFailed = getValue(dict, "TwoStepAuth.RecoveryFailed") self._MESSAGE_AUDIO = getValue(dict, "MESSAGE_AUDIO") self._MESSAGE_AUDIO_r = extractArgumentRanges(self._MESSAGE_AUDIO) self.Checkout_PasswordEntry_Pay = getValue(dict, "Checkout.PasswordEntry.Pay") self.Notifications_MessageNotificationsHelp = getValue(dict, "Notifications.MessageNotificationsHelp") - self.Notification_EncryptedChatRequested = getValue(dict, "Notification.EncryptedChatRequested") self.EnterPasscode_EnterCurrentPasscode = getValue(dict, "EnterPasscode.EnterCurrentPasscode") - self.Channel_Management_LabelModerator = getValue(dict, "Channel.Management.LabelModerator") self._MESSAGE_GAME = getValue(dict, "MESSAGE_GAME") self._MESSAGE_GAME_r = extractArgumentRanges(self._MESSAGE_GAME) self.Conversation_Moderate_Report = getValue(dict, "Conversation.Moderate.Report") self.MessageTimer_Forever = getValue(dict, "MessageTimer.Forever") + self.DialogList_SavedMessagesHelp = getValue(dict, "DialogList.SavedMessagesHelp") self._Conversation_EncryptedPlaceholderTitleIncoming = getValue(dict, "Conversation.EncryptedPlaceholderTitleIncoming") self._Conversation_EncryptedPlaceholderTitleIncoming_r = extractArgumentRanges(self._Conversation_EncryptedPlaceholderTitleIncoming) self._Map_AccurateTo = getValue(dict, "Map.AccurateTo") @@ -6034,28 +5736,21 @@ public final class PresentationStrings { self._Call_ParticipantVersionOutdatedError = getValue(dict, "Call.ParticipantVersionOutdatedError") self._Call_ParticipantVersionOutdatedError_r = extractArgumentRanges(self._Call_ParticipantVersionOutdatedError) self.Tour_Text2 = getValue(dict, "Tour.Text2") - self.Preview_ViewStickerPack = getValue(dict, "Preview.ViewStickerPack") self.Call_StatusNoAnswer = getValue(dict, "Call.StatusNoAnswer") self.Conversation_MessageDialogDelete = getValue(dict, "Conversation.MessageDialogDelete") - self.Calls_Clear = getValue(dict, "Calls.Clear") self.Username_Placeholder = getValue(dict, "Username.Placeholder") self._Notification_PinnedDeletedMessage = getValue(dict, "Notification.PinnedDeletedMessage") self._Notification_PinnedDeletedMessage_r = extractArgumentRanges(self._Notification_PinnedDeletedMessage) self._Time_MonthOfYear_m11 = getValue(dict, "Time.MonthOfYear_m11") self._Time_MonthOfYear_m11_r = extractArgumentRanges(self._Time_MonthOfYear_m11) self.UserInfo_BotHelp = getValue(dict, "UserInfo.BotHelp") - self.Contacts_contact = getValue(dict, "Contacts.contact") self.TwoStepAuth_PasswordSet = getValue(dict, "TwoStepAuth.PasswordSet") - self.Channel_Moderator_AccessLevelEditor = getValue(dict, "Channel.Moderator.AccessLevelEditor") - self.EnterPasscode_TouchId = getValue(dict, "EnterPasscode.TouchId") self._CHANNEL_MESSAGE_VIDEO = getValue(dict, "CHANNEL_MESSAGE_VIDEO") self._CHANNEL_MESSAGE_VIDEO_r = extractArgumentRanges(self._CHANNEL_MESSAGE_VIDEO) + self.EnterPasscode_TouchId = getValue(dict, "EnterPasscode.TouchId") self.Checkout_ErrorInvoiceAlreadyPaid = getValue(dict, "Checkout.ErrorInvoiceAlreadyPaid") self.ChatAdmins_Title = getValue(dict, "ChatAdmins.Title") - self.BroadcastLists_NoListsText = getValue(dict, "BroadcastLists.NoListsText") self.ChannelMembers_WhoCanAddMembers = getValue(dict, "ChannelMembers.WhoCanAddMembers") - self.ChannelMembers_AllMembersMayInviteOffHelp = getValue(dict, "ChannelMembers.AllMembersMayInviteOffHelp") - self.Conversation_InfoPrivate = getValue(dict, "Conversation.InfoPrivate") self.PasscodeSettings_Help = getValue(dict, "PasscodeSettings.Help") self.Conversation_EditingMessagePanelTitle = getValue(dict, "Conversation.EditingMessagePanelTitle") self.Settings_AboutEmpty = getValue(dict, "Settings.AboutEmpty") @@ -6065,42 +5760,36 @@ public final class PresentationStrings { self._Notification_PinnedContactMessage = getValue(dict, "Notification.PinnedContactMessage") self._Notification_PinnedContactMessage_r = extractArgumentRanges(self._Notification_PinnedContactMessage) self.CallSettings_UseLessDataLongDescription = getValue(dict, "CallSettings.UseLessDataLongDescription") + self.FastTwoStepSetup_PasswordPlaceholder = getValue(dict, "FastTwoStepSetup.PasswordPlaceholder") self.Conversation_SecretChatContextBotAlert = getValue(dict, "Conversation.SecretChatContextBotAlert") self.Channel_Moderator_AccessLevelRevoke = getValue(dict, "Channel.Moderator.AccessLevelRevoke") self.CheckoutInfo_ReceiverInfoTitle = getValue(dict, "CheckoutInfo.ReceiverInfoTitle") self.Channel_AdminLogFilter_EventsRestrictions = getValue(dict, "Channel.AdminLogFilter.EventsRestrictions") self.GroupInfo_InviteLink_RevokeLink = getValue(dict, "GroupInfo.InviteLink.RevokeLink") - self.Conversation_Unmute = getValue(dict, "Conversation.Unmute") self.Checkout_PaymentMethod_Title = getValue(dict, "Checkout.PaymentMethod.Title") + self.Conversation_Unmute = getValue(dict, "Conversation.Unmute") self.Notifications_MessageNotifications = getValue(dict, "Notifications.MessageNotifications") self.ChannelMembers_WhoCanAddMembersAdminsHelp = getValue(dict, "ChannelMembers.WhoCanAddMembersAdminsHelp") self.DialogList_DeleteBotConversationConfirmation = getValue(dict, "DialogList.DeleteBotConversationConfirmation") - self._MediaPicker_AccessDeniedHelp = getValue(dict, "MediaPicker.AccessDeniedHelp") - self._MediaPicker_AccessDeniedHelp_r = extractArgumentRanges(self._MediaPicker_AccessDeniedHelp) - self._GroupInfo_InvitationLinkAccept = getValue(dict, "GroupInfo.InvitationLinkAccept") - self._GroupInfo_InvitationLinkAccept_r = extractArgumentRanges(self._GroupInfo_InvitationLinkAccept) self.Conversation_ClousStorageInfo_Description2 = getValue(dict, "Conversation.ClousStorageInfo.Description2") self._Time_MonthOfYear_m5 = getValue(dict, "Time.MonthOfYear_m5") self._Time_MonthOfYear_m5_r = extractArgumentRanges(self._Time_MonthOfYear_m5) self.Map_Hybrid = getValue(dict, "Map.Hybrid") self.Channel_Setup_Title = getValue(dict, "Channel.Setup.Title") + self.MediaPicker_TimerTooltip = getValue(dict, "MediaPicker.TimerTooltip") self.Activity_UploadingVideo = getValue(dict, "Activity.UploadingVideo") self.Channel_Info_Management = getValue(dict, "Channel.Info.Management") self._Notification_MessageLifetimeChangedOutgoing = getValue(dict, "Notification.MessageLifetimeChangedOutgoing") self._Notification_MessageLifetimeChangedOutgoing_r = extractArgumentRanges(self._Notification_MessageLifetimeChangedOutgoing) - self.Conversation_DeleteOneMessage = getValue(dict, "Conversation.DeleteOneMessage") self.PhotoEditor_QualityVeryLow = getValue(dict, "PhotoEditor.QualityVeryLow") + self.Stickers_AddToFavorites = getValue(dict, "Stickers.AddToFavorites") self.Month_ShortFebruary = getValue(dict, "Month.ShortFebruary") - self.Compose_NewBroadcast = getValue(dict, "Compose.NewBroadcast") self.Conversation_ForwardTitle = getValue(dict, "Conversation.ForwardTitle") self.Settings_FAQ_URL = getValue(dict, "Settings.FAQ_URL") - self.TwoStepAuth_ConfirmationChangeEmail = getValue(dict, "TwoStepAuth.ConfirmationChangeEmail") self.Activity_RecordingVideoMessage = getValue(dict, "Activity.RecordingVideoMessage") - self.WelcomeScreen_ContactsAccessSettings = getValue(dict, "WelcomeScreen.ContactsAccessSettings") self.SharedMedia_EmptyFilesText = getValue(dict, "SharedMedia.EmptyFilesText") self._Contacts_AccessDeniedHelpLandscape = getValue(dict, "Contacts.AccessDeniedHelpLandscape") self._Contacts_AccessDeniedHelpLandscape_r = extractArgumentRanges(self._Contacts_AccessDeniedHelpLandscape) - self.Channel_NotificationCommentsEnabled = getValue(dict, "Channel.NotificationCommentsEnabled") self.PasscodeSettings_UnlockWithTouchId = getValue(dict, "PasscodeSettings.UnlockWithTouchId") self.Contacts_AccessDeniedHelpON = getValue(dict, "Contacts.AccessDeniedHelpON") self.NetworkUsageSettings_ResetStats = getValue(dict, "NetworkUsageSettings.ResetStats") @@ -6111,6 +5800,9 @@ public final class PresentationStrings { self._Notification_ChannelInviter_r = extractArgumentRanges(self._Notification_ChannelInviter) self.SocksProxySetup_TypeSocks = getValue(dict, "SocksProxySetup.TypeSocks") self.Profile_MessageLifetimeForever = getValue(dict, "Profile.MessageLifetimeForever") + self.MediaPicker_UngroupDescription = getValue(dict, "MediaPicker.UngroupDescription") + self._Checkout_SavePasswordTimeoutAndFaceId = getValue(dict, "Checkout.SavePasswordTimeoutAndFaceId") + self._Checkout_SavePasswordTimeoutAndFaceId_r = extractArgumentRanges(self._Checkout_SavePasswordTimeoutAndFaceId) self.SocksProxySetup_Username = getValue(dict, "SocksProxySetup.Username") self.Conversation_Edit = getValue(dict, "Conversation.Edit") self.TwoStepAuth_ResetAccountHelp = getValue(dict, "TwoStepAuth.ResetAccountHelp") @@ -6120,7 +5812,6 @@ public final class PresentationStrings { self.Channel_ErrorAddBlocked = getValue(dict, "Channel.ErrorAddBlocked") self.Conversation_Unpin = getValue(dict, "Conversation.Unpin") self.Call_RecordingDisabledMessage = getValue(dict, "Call.RecordingDisabledMessage") - self.Conversation_Stop = getValue(dict, "Conversation.Stop") self.Conversation_UnblockUser = getValue(dict, "Conversation.UnblockUser") self.Conversation_Unblock = getValue(dict, "Conversation.Unblock") self._CHANNEL_MESSAGE_GIF = getValue(dict, "CHANNEL_MESSAGE_GIF") @@ -6133,14 +5824,13 @@ public final class PresentationStrings { self.Profile_MessageLifetime1w = getValue(dict, "Profile.MessageLifetime1w") self.DialogList_TabTitle = getValue(dict, "DialogList.TabTitle") self.UserInfo_GenericPhoneLabel = getValue(dict, "UserInfo.GenericPhoneLabel") - self.MediaPicker_MomentsDateFormat = getValue(dict, "MediaPicker.MomentsDateFormat") - self._Conversation_DownloadKilobytes = getValue(dict, "Conversation.DownloadKilobytes") - self._Conversation_DownloadKilobytes_r = extractArgumentRanges(self._Conversation_DownloadKilobytes) self._Channel_AdminLog_MessagePromotedName = getValue(dict, "Channel.AdminLog.MessagePromotedName") self._Channel_AdminLog_MessagePromotedName_r = extractArgumentRanges(self._Channel_AdminLog_MessagePromotedName) + self.Group_Members_AddMemberBotErrorNotAllowed = getValue(dict, "Group.Members.AddMemberBotErrorNotAllowed") self._Username_LinkHint = getValue(dict, "Username.LinkHint") self._Username_LinkHint_r = extractArgumentRanges(self._Username_LinkHint) - self.Group_Members_AddMemberBotErrorNotAllowed = getValue(dict, "Group.Members.AddMemberBotErrorNotAllowed") + self.Map_StopLiveLocation = getValue(dict, "Map.StopLiveLocation") + self.Message_LiveLocation = getValue(dict, "Message.LiveLocation") self.NetworkUsageSettings_Title = getValue(dict, "NetworkUsageSettings.Title") self.CheckoutInfo_ShippingInfoPostcodePlaceholder = getValue(dict, "CheckoutInfo.ShippingInfoPostcodePlaceholder") self.Wallpaper_Wallpaper = getValue(dict, "Wallpaper.Wallpaper") @@ -6148,46 +5838,44 @@ public final class PresentationStrings { self.SharedMedia_TitleLink = getValue(dict, "SharedMedia.TitleLink") self._Channel_AdminLog_MessageRestrictedName = getValue(dict, "Channel.AdminLog.MessageRestrictedName") self._Channel_AdminLog_MessageRestrictedName_r = extractArgumentRanges(self._Channel_AdminLog_MessageRestrictedName) + self._Channel_AdminLog_MessageGroupPreHistoryHidden = getValue(dict, "Channel.AdminLog.MessageGroupPreHistoryHidden") + self._Channel_AdminLog_MessageGroupPreHistoryHidden_r = extractArgumentRanges(self._Channel_AdminLog_MessageGroupPreHistoryHidden) self.Channel_JoinChannel = getValue(dict, "Channel.JoinChannel") - self.AccessDenied_LocationDisabled = getValue(dict, "AccessDenied.LocationDisabled") + self.StickerPack_Add = getValue(dict, "StickerPack.Add") self.Group_ErrorNotMutualContact = getValue(dict, "Group.ErrorNotMutualContact") - self.Conversation_DownloadPhoto = getValue(dict, "Conversation.DownloadPhoto") - self.Presence_online = getValue(dict, "Presence.online") + self.AccessDenied_LocationDisabled = getValue(dict, "AccessDenied.LocationDisabled") self.Login_UnknownError = getValue(dict, "Login.UnknownError") + self.Presence_online = getValue(dict, "Presence.online") self.DialogList_Title = getValue(dict, "DialogList.Title") - self.SearchImages_NoImagesFound = getValue(dict, "SearchImages.NoImagesFound") - self._Notification_RemovedUserPhoto = getValue(dict, "Notification.RemovedUserPhoto") - self._Notification_RemovedUserPhoto_r = extractArgumentRanges(self._Notification_RemovedUserPhoto) self.Stickers_Install = getValue(dict, "Stickers.Install") + self.SearchImages_NoImagesFound = getValue(dict, "SearchImages.NoImagesFound") self._Watch_Time_ShortTodayAt = getValue(dict, "Watch.Time.ShortTodayAt") self._Watch_Time_ShortTodayAt_r = extractArgumentRanges(self._Watch_Time_ShortTodayAt) - self.StickerPack_Add = getValue(dict, "StickerPack.Add") - self.ChatSettings_Language = getValue(dict, "ChatSettings.Language") + self.UserInfo_GroupsInCommon = getValue(dict, "UserInfo.GroupsInCommon") self.Message_PinnedContactMessage = getValue(dict, "Message.PinnedContactMessage") self.AccessDenied_CameraDisabled = getValue(dict, "AccessDenied.CameraDisabled") self._Time_PreciseDate_m3 = getValue(dict, "Time.PreciseDate_m3") self._Time_PreciseDate_m3_r = extractArgumentRanges(self._Time_PreciseDate_m3) - self.UserInfo_GroupsInCommon = getValue(dict, "UserInfo.GroupsInCommon") - self.UserInfo_Call = getValue(dict, "UserInfo.Call") - self.Conversation_InputTextDisabledPlaceholder = getValue(dict, "Conversation.InputTextDisabledPlaceholder") - self.Map_ForwardViaTelegram = getValue(dict, "Map.ForwardViaTelegram") + self._LiveLocationUpdated_YesterdayAt = getValue(dict, "LiveLocationUpdated.YesterdayAt") + self._LiveLocationUpdated_YesterdayAt_r = extractArgumentRanges(self._LiveLocationUpdated_YesterdayAt) self.Month_GenMarch = getValue(dict, "Month.GenMarch") self.Watch_UserInfo_Unmute = getValue(dict, "Watch.UserInfo.Unmute") - self.PhotoEditor_BlurTool = getValue(dict, "PhotoEditor.BlurTool") + self.CheckoutInfo_ErrorPostcodeInvalid = getValue(dict, "CheckoutInfo.ErrorPostcodeInvalid") self.Common_Delete = getValue(dict, "Common.Delete") self.Username_Title = getValue(dict, "Username.Title") self.Login_PhoneFloodError = getValue(dict, "Login.PhoneFloodError") - self.CheckoutInfo_ErrorPostcodeInvalid = getValue(dict, "CheckoutInfo.ErrorPostcodeInvalid") + self.Channel_AdminLog_InfoPanelTitle = getValue(dict, "Channel.AdminLog.InfoPanelTitle") self._CHANNEL_MESSAGE_PHOTO = getValue(dict, "CHANNEL_MESSAGE_PHOTO") self._CHANNEL_MESSAGE_PHOTO_r = extractArgumentRanges(self._CHANNEL_MESSAGE_PHOTO) - self.Channel_AdminLog_InfoPanelTitle = getValue(dict, "Channel.AdminLog.InfoPanelTitle") + self._Channel_AdminLog_MessageToggleInvitesOff = getValue(dict, "Channel.AdminLog.MessageToggleInvitesOff") + self._Channel_AdminLog_MessageToggleInvitesOff_r = extractArgumentRanges(self._Channel_AdminLog_MessageToggleInvitesOff) self.Group_ErrorAddTooMuchBots = getValue(dict, "Group.ErrorAddTooMuchBots") self._Notification_CallFormat = getValue(dict, "Notification.CallFormat") self._Notification_CallFormat_r = extractArgumentRanges(self._Notification_CallFormat) self._CHAT_MESSAGE_PHOTO = getValue(dict, "CHAT_MESSAGE_PHOTO") self._CHAT_MESSAGE_PHOTO_r = extractArgumentRanges(self._CHAT_MESSAGE_PHOTO) - self._Channel_AdminLog_MessageToggleInvitesOff = getValue(dict, "Channel.AdminLog.MessageToggleInvitesOff") - self._Channel_AdminLog_MessageToggleInvitesOff_r = extractArgumentRanges(self._Channel_AdminLog_MessageToggleInvitesOff) + self._UserInfo_UnblockConfirmation = getValue(dict, "UserInfo.UnblockConfirmation") + self._UserInfo_UnblockConfirmation_r = extractArgumentRanges(self._UserInfo_UnblockConfirmation) self.UserInfo_ShareBot = getValue(dict, "UserInfo.ShareBot") self.TwoStepAuth_EmailSkip = getValue(dict, "TwoStepAuth.EmailSkip") self.Conversation_JumpToDate = getValue(dict, "Conversation.JumpToDate") @@ -6197,8 +5885,6 @@ public final class PresentationStrings { self.Camera_FlashAuto = getValue(dict, "Camera.FlashAuto") self.Call_ConnectionErrorMessage = getValue(dict, "Call.ConnectionErrorMessage") self.Stickers_FrequentlyUsed = getValue(dict, "Stickers.FrequentlyUsed") - self.Compose_NewChannel_AddMember = getValue(dict, "Compose.NewChannel.AddMember") - self.Watch_State_Updating = getValue(dict, "Watch.State.Updating") self.LastSeen_ALongTimeAgo = getValue(dict, "LastSeen.ALongTimeAgo") self.DialogList_SearchSectionGlobal = getValue(dict, "DialogList.SearchSectionGlobal") self.ChangePhoneNumberNumber_NumberPlaceholder = getValue(dict, "ChangePhoneNumberNumber.NumberPlaceholder") @@ -6206,27 +5892,32 @@ public final class PresentationStrings { self.GroupInfo_GroupType = getValue(dict, "GroupInfo.GroupType") self.Watch_Suggestion_OnMyWay = getValue(dict, "Watch.Suggestion.OnMyWay") self.Checkout_NewCard_PaymentCard = getValue(dict, "Checkout.NewCard.PaymentCard") + self._DialogList_SearchSubtitleFormat = getValue(dict, "DialogList.SearchSubtitleFormat") + self._DialogList_SearchSubtitleFormat_r = extractArgumentRanges(self._DialogList_SearchSubtitleFormat) self.PhotoEditor_CropAspectRatioOriginal = getValue(dict, "PhotoEditor.CropAspectRatioOriginal") self._Conversation_RestrictedInlineTimed = getValue(dict, "Conversation.RestrictedInlineTimed") self._Conversation_RestrictedInlineTimed_r = extractArgumentRanges(self._Conversation_RestrictedInlineTimed) - self.MediaPicker_MomentsDateRangeFormat = getValue(dict, "MediaPicker.MomentsDateRangeFormat") self.UserInfo_NotificationsDisabled = getValue(dict, "UserInfo.NotificationsDisabled") self._CONTACT_JOINED = getValue(dict, "CONTACT_JOINED") self._CONTACT_JOINED_r = extractArgumentRanges(self._CONTACT_JOINED) self.PrivacyLastSeenSettings_AlwaysShareWith_Title = getValue(dict, "PrivacyLastSeenSettings.AlwaysShareWith.Title") + self._Channel_AdminLog_MessageGroupPreHistoryVisible = getValue(dict, "Channel.AdminLog.MessageGroupPreHistoryVisible") + self._Channel_AdminLog_MessageGroupPreHistoryVisible_r = extractArgumentRanges(self._Channel_AdminLog_MessageGroupPreHistoryVisible) self.BlockedUsers_LeavePrefix = getValue(dict, "BlockedUsers.LeavePrefix") self.NetworkUsageSettings_ResetStatsConfirmation = getValue(dict, "NetworkUsageSettings.ResetStatsConfirmation") + self.Group_Setup_HistoryHeader = getValue(dict, "Group.Setup.HistoryHeader") self.Channel_EditAdmin_PermissionPostMessages = getValue(dict, "Channel.EditAdmin.PermissionPostMessages") self._Contacts_AddPhoneNumber = getValue(dict, "Contacts.AddPhoneNumber") self._Contacts_AddPhoneNumber_r = extractArgumentRanges(self._Contacts_AddPhoneNumber) self._MESSAGE_SCREENSHOT = getValue(dict, "MESSAGE_SCREENSHOT") self._MESSAGE_SCREENSHOT_r = extractArgumentRanges(self._MESSAGE_SCREENSHOT) self.DialogList_EncryptionProcessing = getValue(dict, "DialogList.EncryptionProcessing") + self.GroupInfo_GroupHistory = getValue(dict, "GroupInfo.GroupHistory") self.Conversation_ApplyLocalization = getValue(dict, "Conversation.ApplyLocalization") + self.FastTwoStepSetup_Title = getValue(dict, "FastTwoStepSetup.Title") self.Conversation_DeleteManyMessages = getValue(dict, "Conversation.DeleteManyMessages") self.CancelResetAccount_Title = getValue(dict, "CancelResetAccount.Title") self.Notification_CallOutgoingShort = getValue(dict, "Notification.CallOutgoingShort") - self.Channel_Moderator_AccessLevelHeader = getValue(dict, "Channel.Moderator.AccessLevelHeader") self.SharedMedia_TitleAll = getValue(dict, "SharedMedia.TitleAll") self.Conversation_SlideToCancel = getValue(dict, "Conversation.SlideToCancel") self.AuthSessions_TerminateSession = getValue(dict, "AuthSessions.TerminateSession") @@ -6243,21 +5934,24 @@ public final class PresentationStrings { self._Conversation_Moderate_DeleteAllMessages = getValue(dict, "Conversation.Moderate.DeleteAllMessages") self._Conversation_Moderate_DeleteAllMessages_r = extractArgumentRanges(self._Conversation_Moderate_DeleteAllMessages) self.SharedMedia_CategoryOther = getValue(dict, "SharedMedia.CategoryOther") - self.GoogleDrive_LogoutMessage = getValue(dict, "GoogleDrive.LogoutMessage") + self.DialogList_SavedMessagesTooltip = getValue(dict, "DialogList.SavedMessagesTooltip") self.Preview_DeletePhoto = getValue(dict, "Preview.DeletePhoto") - self.PasscodeSettings_TurnPasscodeOn = getValue(dict, "PasscodeSettings.TurnPasscodeOn") self.GroupInfo_ChannelListNamePlaceholder = getValue(dict, "GroupInfo.ChannelListNamePlaceholder") + self.PasscodeSettings_TurnPasscodeOn = getValue(dict, "PasscodeSettings.TurnPasscodeOn") + self._Channel_AdminLog_MessageChangedGroupStickerPack = getValue(dict, "Channel.AdminLog.MessageChangedGroupStickerPack") + self._Channel_AdminLog_MessageChangedGroupStickerPack_r = extractArgumentRanges(self._Channel_AdminLog_MessageChangedGroupStickerPack) self.DialogList_Unpin = getValue(dict, "DialogList.Unpin") self.GroupInfo_SetGroupPhoto = getValue(dict, "GroupInfo.SetGroupPhoto") self.StickerPacksSettings_ArchivedPacks_Info = getValue(dict, "StickerPacksSettings.ArchivedPacks.Info") self.ConvertToSupergroup_Title = getValue(dict, "ConvertToSupergroup.Title") self._CHAT_MESSAGE_NOTEXT = getValue(dict, "CHAT_MESSAGE_NOTEXT") self._CHAT_MESSAGE_NOTEXT_r = extractArgumentRanges(self._CHAT_MESSAGE_NOTEXT) + self.Notification_CallCanceledShort = getValue(dict, "Notification.CallCanceledShort") self.Channel_Setup_TypeHeader = getValue(dict, "Channel.Setup.TypeHeader") self._Notification_NewAuthDetected = getValue(dict, "Notification.NewAuthDetected") self._Notification_NewAuthDetected_r = extractArgumentRanges(self._Notification_NewAuthDetected) - self.Notification_CallCanceledShort = getValue(dict, "Notification.CallCanceledShort") - self.PhotoEditor_RevertMessage = getValue(dict, "PhotoEditor.RevertMessage") + self._Channel_AdminLog_MessageRemovedGroupStickerPack = getValue(dict, "Channel.AdminLog.MessageRemovedGroupStickerPack") + self._Channel_AdminLog_MessageRemovedGroupStickerPack_r = extractArgumentRanges(self._Channel_AdminLog_MessageRemovedGroupStickerPack) self.AccessDenied_VideoMessageCamera = getValue(dict, "AccessDenied.VideoMessageCamera") self.Conversation_Search = getValue(dict, "Conversation.Search") self._Channel_Management_PromotedBy = getValue(dict, "Channel.Management.PromotedBy") @@ -6267,7 +5961,6 @@ public final class PresentationStrings { self._Time_MonthOfYear_m4 = getValue(dict, "Time.MonthOfYear_m4") self._Time_MonthOfYear_m4_r = extractArgumentRanges(self._Time_MonthOfYear_m4) self.SecretImage_Title = getValue(dict, "SecretImage.Title") - self.Preview_ForwardViaTelegram = getValue(dict, "Preview.ForwardViaTelegram") self.Notifications_InAppNotificationsSounds = getValue(dict, "Notifications.InAppNotificationsSounds") self.Call_StatusRequesting = getValue(dict, "Call.StatusRequesting") self._Channel_AdminLog_MessageRestrictedUntil = getValue(dict, "Channel.AdminLog.MessageRestrictedUntil") @@ -6278,6 +5971,7 @@ public final class PresentationStrings { self.ChatSettings_Other = getValue(dict, "ChatSettings.Other") self._Channel_AdminLog_MessageChangedChannelAbout = getValue(dict, "Channel.AdminLog.MessageChangedChannelAbout") self._Channel_AdminLog_MessageChangedChannelAbout_r = extractArgumentRanges(self._Channel_AdminLog_MessageChangedChannelAbout) + self.Channel_Stickers_CreateYourOwn = getValue(dict, "Channel.Stickers.CreateYourOwn") self._Call_EmojiDescription = getValue(dict, "Call.EmojiDescription") self._Call_EmojiDescription_r = extractArgumentRanges(self._Call_EmojiDescription) self.Settings_SaveIncomingPhotos = getValue(dict, "Settings.SaveIncomingPhotos") @@ -6293,11 +5987,7 @@ public final class PresentationStrings { self._Notification_MessageLifetimeChanged = getValue(dict, "Notification.MessageLifetimeChanged") self._Notification_MessageLifetimeChanged_r = extractArgumentRanges(self._Notification_MessageLifetimeChanged) self.Message_Contact = getValue(dict, "Message.Contact") - self._Watch_LastSeen_TodayAt = getValue(dict, "Watch.LastSeen.TodayAt") - self._Watch_LastSeen_TodayAt_r = extractArgumentRanges(self._Watch_LastSeen_TodayAt) - self.Channel_Moderator_AccessLevelModerator = getValue(dict, "Channel.Moderator.AccessLevelModerator") - self.GoogleDrive_Logout = getValue(dict, "GoogleDrive.Logout") - self.PhotoEditor_RevertToOriginal = getValue(dict, "PhotoEditor.RevertToOriginal") + self.PasscodeSettings_AutoLock_IfAwayFor_1minute = getValue(dict, "PasscodeSettings.AutoLock.IfAwayFor_1minute") self.Common_More = getValue(dict, "Common.More") self.Preview_OpenInInstagram = getValue(dict, "Preview.OpenInInstagram") self.PhotoEditor_HighlightsTool = getValue(dict, "PhotoEditor.HighlightsTool") @@ -6307,26 +5997,22 @@ public final class PresentationStrings { self._PINNED_GAME_r = extractArgumentRanges(self._PINNED_GAME) self.Invite_LargeRecipientsCountWarning = getValue(dict, "Invite.LargeRecipientsCountWarning") self.GroupInfo_BroadcastListNamePlaceholder = getValue(dict, "GroupInfo.BroadcastListNamePlaceholder") + self.Activity_UploadingVideoMessage = getValue(dict, "Activity.UploadingVideoMessage") self.Conversation_ShareBotContactConfirmation = getValue(dict, "Conversation.ShareBotContactConfirmation") - self.GroupInfo_ActionBan = getValue(dict, "GroupInfo.ActionBan") self.Login_CodeSentSms = getValue(dict, "Login.CodeSentSms") self.Conversation_ReportSpamConfirmation = getValue(dict, "Conversation.ReportSpamConfirmation") self.ChannelMembers_ChannelAdminsTitle = getValue(dict, "ChannelMembers.ChannelAdminsTitle") self.SocksProxySetup_Credentials = getValue(dict, "SocksProxySetup.Credentials") self.CallSettings_UseLessData = getValue(dict, "CallSettings.UseLessData") + self.MediaPicker_GroupDescription = getValue(dict, "MediaPicker.GroupDescription") self._TwoStepAuth_EnterPasswordHint = getValue(dict, "TwoStepAuth.EnterPasswordHint") self._TwoStepAuth_EnterPasswordHint_r = extractArgumentRanges(self._TwoStepAuth_EnterPasswordHint) self.CallSettings_TabIcon = getValue(dict, "CallSettings.TabIcon") - self.Conversation_EditForward = getValue(dict, "Conversation.EditForward") self.ConversationProfile_UnknownAddMemberError = getValue(dict, "ConversationProfile.UnknownAddMemberError") self._Conversation_FileHowToText = getValue(dict, "Conversation.FileHowToText") self._Conversation_FileHowToText_r = extractArgumentRanges(self._Conversation_FileHowToText) self.Channel_AdminLog_BanSendMedia = getValue(dict, "Channel.AdminLog.BanSendMedia") - self.Tour_Text7 = getValue(dict, "Tour.Text7") - self.Contacts_contactsvar = getValue(dict, "Contacts.contactsvar") self.Watch_UserInfo_Unblock = getValue(dict, "Watch.UserInfo.Unblock") - self.Conversation_EditDelete = getValue(dict, "Conversation.EditDelete") - self.Conversation_ViewPhoto = getValue(dict, "Conversation.ViewPhoto") self.StickerPacksSettings_ArchivedMasks = getValue(dict, "StickerPacksSettings.ArchivedMasks") self.Message_Animation = getValue(dict, "Message.Animation") self.Checkout_PaymentMethod = getValue(dict, "Checkout.PaymentMethod") @@ -6335,52 +6021,41 @@ public final class PresentationStrings { self.Cache_Music = getValue(dict, "Cache.Music") self._Login_CallRequestState1 = getValue(dict, "Login.CallRequestState1") self._Login_CallRequestState1_r = extractArgumentRanges(self._Login_CallRequestState1) - self._SearchImages_ImageNofM = getValue(dict, "SearchImages.ImageNofM") - self._SearchImages_ImageNofM_r = extractArgumentRanges(self._SearchImages_ImageNofM) self.Channel_Username_CreatePrivateLinkHelp = getValue(dict, "Channel.Username.CreatePrivateLinkHelp") self._Time_PreciseDate_m2 = getValue(dict, "Time.PreciseDate_m2") self._Time_PreciseDate_m2_r = extractArgumentRanges(self._Time_PreciseDate_m2) self._FileSize_B = getValue(dict, "FileSize.B") self._FileSize_B_r = extractArgumentRanges(self._FileSize_B) - self.PhotoEditor_SaturationTool = getValue(dict, "PhotoEditor.SaturationTool") - self.ImagePicker_NoPhotos = getValue(dict, "ImagePicker.NoPhotos") self._Target_ShareGameConfirmationGroup = getValue(dict, "Target.ShareGameConfirmationGroup") self._Target_ShareGameConfirmationGroup_r = extractArgumentRanges(self._Target_ShareGameConfirmationGroup) - self.Call_StatusConnecting = getValue(dict, "Call.StatusConnecting") + self.PhotoEditor_SaturationTool = getValue(dict, "PhotoEditor.SaturationTool") self.Channel_BanUser_BlockFor = getValue(dict, "Channel.BanUser.BlockFor") - self.Preview_DeleteVideo = getValue(dict, "Preview.DeleteVideo") + self.Call_StatusConnecting = getValue(dict, "Call.StatusConnecting") self.Bot_Start = getValue(dict, "Bot.Start") self._Channel_AdminLog_MessageChangedGroupAbout = getValue(dict, "Channel.AdminLog.MessageChangedGroupAbout") self._Channel_AdminLog_MessageChangedGroupAbout_r = extractArgumentRanges(self._Channel_AdminLog_MessageChangedGroupAbout) self.Notifications_TextTone = getValue(dict, "Notifications.TextTone") - self.DialogList_Draft = getValue(dict, "DialogList.Draft") + self.Settings_CallSettings = getValue(dict, "Settings.CallSettings") self._Watch_Time_ShortYesterdayAt = getValue(dict, "Watch.Time.ShortYesterdayAt") self._Watch_Time_ShortYesterdayAt_r = extractArgumentRanges(self._Watch_Time_ShortYesterdayAt) self.Contacts_InviteToTelegram = getValue(dict, "Contacts.InviteToTelegram") self._PINNED_DOC = getValue(dict, "PINNED_DOC") self._PINNED_DOC_r = extractArgumentRanges(self._PINNED_DOC) - self._ConversationProfile_UserLeftChatError = getValue(dict, "ConversationProfile.UserLeftChatError") - self._ConversationProfile_UserLeftChatError_r = extractArgumentRanges(self._ConversationProfile_UserLeftChatError) self.ChatSettings_PrivateChats = getValue(dict, "ChatSettings.PrivateChats") - self.Settings_CallSettings = getValue(dict, "Settings.CallSettings") + self.DialogList_Draft = getValue(dict, "DialogList.Draft") self.Channel_EditAdmin_PermissionDeleteMessages = getValue(dict, "Channel.EditAdmin.PermissionDeleteMessages") - self.Conversation_CloudStorageInfo_Title = getValue(dict, "Conversation.CloudStorageInfo.Title") self.Channel_BanUser_PermissionSendStickersAndGifs = getValue(dict, "Channel.BanUser.PermissionSendStickersAndGifs") - self.Channel_AdminLog_Status = getValue(dict, "Channel.AdminLog.Status") + self.Conversation_CloudStorageInfo_Title = getValue(dict, "Conversation.CloudStorageInfo.Title") self.Notification_RenamedChannel = getValue(dict, "Notification.RenamedChannel") self.BlockedUsers_BlockUser = getValue(dict, "BlockedUsers.BlockUser") self.ChatSettings_TextSize = getValue(dict, "ChatSettings.TextSize") - self.MediaPicker_AccessDeniedError = getValue(dict, "MediaPicker.AccessDeniedError") self.ChannelInfo_DeleteGroup = getValue(dict, "ChannelInfo.DeleteGroup") - self._BlockedUsers_BlockFormat = getValue(dict, "BlockedUsers.BlockFormat") - self._BlockedUsers_BlockFormat_r = extractArgumentRanges(self._BlockedUsers_BlockFormat) self.PhoneNumberHelp_Alert = getValue(dict, "PhoneNumberHelp.Alert") self._PINNED_TEXT = getValue(dict, "PINNED_TEXT") self._PINNED_TEXT_r = extractArgumentRanges(self._PINNED_TEXT) self.Watch_ChannelInfo_Title = getValue(dict, "Watch.ChannelInfo.Title") self.WebSearch_RecentSectionClear = getValue(dict, "WebSearch.RecentSectionClear") self.Channel_AdminLogFilter_AdminsAll = getValue(dict, "Channel.AdminLogFilter.AdminsAll") - self.StickerPack_AddStickers = getValue(dict, "StickerPack.AddStickers") self.Channel_Setup_TypePrivate = getValue(dict, "Channel.Setup.TypePrivate") self.PhotoEditor_TintTool = getValue(dict, "PhotoEditor.TintTool") self.Watch_Suggestion_CantTalk = getValue(dict, "Watch.Suggestion.CantTalk") @@ -6388,10 +6063,8 @@ public final class PresentationStrings { self._CHAT_MESSAGE_STICKER = getValue(dict, "CHAT_MESSAGE_STICKER") self._CHAT_MESSAGE_STICKER_r = extractArgumentRanges(self._CHAT_MESSAGE_STICKER) self.Map_ChooseAPlace = getValue(dict, "Map.ChooseAPlace") - self.Tour_Title7 = getValue(dict, "Tour.Title7") + self.Map_ShareLiveLocationHelp = getValue(dict, "Map.ShareLiveLocationHelp") self.Watch_Bot_Restart = getValue(dict, "Watch.Bot.Restart") - self.StickerPack_ShareStickers = getValue(dict, "StickerPack.ShareStickers") - self.ChannelMembers_AllMembersMayInvite = getValue(dict, "ChannelMembers.AllMembersMayInvite") self.Channel_About_Help = getValue(dict, "Channel.About.Help") self.Web_OpenExternal = getValue(dict, "Web.OpenExternal") self.UserInfo_AddContact = getValue(dict, "UserInfo.AddContact") @@ -6402,146 +6075,133 @@ public final class PresentationStrings { self.Notification_MessageLifetime1m = getValue(dict, "Notification.MessageLifetime1m") self._Call_StatusBar = getValue(dict, "Call.StatusBar") self._Call_StatusBar_r = extractArgumentRanges(self._Call_StatusBar) + self.EditProfile_NameAndPhotoHelp = getValue(dict, "EditProfile.NameAndPhotoHelp") self.Month_ShortJuly = getValue(dict, "Month.ShortJuly") - self.Watch_MessageView_ViewOnPhone = getValue(dict, "Watch.MessageView.ViewOnPhone") self.CheckoutInfo_ShippingInfoAddress1Placeholder = getValue(dict, "CheckoutInfo.ShippingInfoAddress1Placeholder") - self.Stickers_Favorited = getValue(dict, "Stickers.Favorited") + self.Watch_MessageView_ViewOnPhone = getValue(dict, "Watch.MessageView.ViewOnPhone") self.CallSettings_Never = getValue(dict, "CallSettings.Never") - self.DialogList_SelectContacts = getValue(dict, "DialogList.SelectContacts") - self.Conversation_DownloadProgressMegabytes = getValue(dict, "Conversation.DownloadProgressMegabytes") self.TwoStepAuth_EmailSent = getValue(dict, "TwoStepAuth.EmailSent") self._Notification_PinnedAnimationMessage = getValue(dict, "Notification.PinnedAnimationMessage") self._Notification_PinnedAnimationMessage_r = extractArgumentRanges(self._Notification_PinnedAnimationMessage) self.TwoStepAuth_RecoveryTitle = getValue(dict, "TwoStepAuth.RecoveryTitle") self.WatchRemote_AlertOpen = getValue(dict, "WatchRemote.AlertOpen") self.ExplicitContent_AlertChannel = getValue(dict, "ExplicitContent.AlertChannel") + self.Widget_AuthRequired = getValue(dict, "Widget.AuthRequired") self._ForwardedAuthors2 = getValue(dict, "ForwardedAuthors2") self._ForwardedAuthors2_r = extractArgumentRanges(self._ForwardedAuthors2) - self.TwoStepAuth_ConfirmationText = getValue(dict, "TwoStepAuth.ConfirmationText") self.ChannelInfo_DeleteGroupConfirmation = getValue(dict, "ChannelInfo.DeleteGroupConfirmation") + self.TwoStepAuth_ConfirmationText = getValue(dict, "TwoStepAuth.ConfirmationText") self.Login_SmsRequestState3 = getValue(dict, "Login.SmsRequestState3") self.Notifications_AlertTones = getValue(dict, "Notifications.AlertTones") self._Time_MonthOfYear_m10 = getValue(dict, "Time.MonthOfYear_m10") self._Time_MonthOfYear_m10_r = extractArgumentRanges(self._Time_MonthOfYear_m10) self.Login_InfoAvatarPhoto = getValue(dict, "Login.InfoAvatarPhoto") - self.Widget_AuthRequired = getValue(dict, "Widget.AuthRequired") self.Calls_TabTitle = getValue(dict, "Calls.TabTitle") - self.Contacts_MemberSearchSectionTitleChannel = getValue(dict, "Contacts.MemberSearchSectionTitleChannel") + self.Map_YouAreHere = getValue(dict, "Map.YouAreHere") self.PhotoEditor_CurvesTool = getValue(dict, "PhotoEditor.CurvesTool") - self.Preview_LoadingVideo = getValue(dict, "Preview.LoadingVideo") - self.State_updating = getValue(dict, "State.updating") + self.Map_LiveLocationFor1Hour = getValue(dict, "Map.LiveLocationFor1Hour") self._Notification_JoinedChannel = getValue(dict, "Notification.JoinedChannel") self._Notification_JoinedChannel_r = extractArgumentRanges(self._Notification_JoinedChannel) - self.TwoStepAuth_ResetAccount = getValue(dict, "TwoStepAuth.ResetAccount") self.GroupInfo_ActionRestrict = getValue(dict, "GroupInfo.ActionRestrict") self.Checkout_ShippingOption_Title = getValue(dict, "Checkout.ShippingOption.Title") - self.Weekday_Tuesday = getValue(dict, "Weekday.Tuesday") - self.Preview_Tooltip = getValue(dict, "Preview.Tooltip") - self.Conversation_EncryptionProcessing = getValue(dict, "Conversation.EncryptionProcessing") - self.Weekday_ShortSunday = getValue(dict, "Weekday.ShortSunday") - self._CHAT_ADD_MEMBER = getValue(dict, "CHAT_ADD_MEMBER") - self._CHAT_ADD_MEMBER_r = extractArgumentRanges(self._CHAT_ADD_MEMBER) self._Channel_AdminLog_MessageKickedName = getValue(dict, "Channel.AdminLog.MessageKickedName") self._Channel_AdminLog_MessageKickedName_r = extractArgumentRanges(self._Channel_AdminLog_MessageKickedName) + self.Conversation_EncryptionProcessing = getValue(dict, "Conversation.EncryptionProcessing") + self._CHAT_ADD_MEMBER = getValue(dict, "CHAT_ADD_MEMBER") + self._CHAT_ADD_MEMBER_r = extractArgumentRanges(self._CHAT_ADD_MEMBER) + self.Weekday_ShortSunday = getValue(dict, "Weekday.ShortSunday") self.Month_ShortJune = getValue(dict, "Month.ShortJune") self.Privacy_Calls_Integration = getValue(dict, "Privacy.Calls.Integration") + self.Channel_TypeSetup_Title = getValue(dict, "Channel.TypeSetup.Title") self.Month_GenApril = getValue(dict, "Month.GenApril") self.StickerPacksSettings_ShowStickersButton = getValue(dict, "StickerPacksSettings.ShowStickersButton") - self.MediaPicker_MomentsDateRangeSameMonthFormat = getValue(dict, "MediaPicker.MomentsDateRangeSameMonthFormat") self.CheckoutInfo_ShippingInfoTitle = getValue(dict, "CheckoutInfo.ShippingInfoTitle") self.StickerPacksSettings_ShowStickersButtonHelp = getValue(dict, "StickerPacksSettings.ShowStickersButtonHelp") self._Compatibility_SecretMediaVersionTooLow = getValue(dict, "Compatibility.SecretMediaVersionTooLow") self._Compatibility_SecretMediaVersionTooLow_r = extractArgumentRanges(self._Compatibility_SecretMediaVersionTooLow) self.CallSettings_RecentCalls = getValue(dict, "CallSettings.RecentCalls") - self.Conversation_Megabytes = getValue(dict, "Conversation.Megabytes") + self._Conversation_Megabytes = getValue(dict, "Conversation.Megabytes") + self._Conversation_Megabytes_r = extractArgumentRanges(self._Conversation_Megabytes) self.Conversation_SearchByName_Prefix = getValue(dict, "Conversation.SearchByName.Prefix") self.TwoStepAuth_FloodError = getValue(dict, "TwoStepAuth.FloodError") - self.Login_InvalidCountryCode = getValue(dict, "Login.InvalidCountryCode") self.Paint_Stickers = getValue(dict, "Paint.Stickers") + self.Login_InvalidCountryCode = getValue(dict, "Login.InvalidCountryCode") self.Privacy_Calls_AlwaysAllow_Title = getValue(dict, "Privacy.Calls.AlwaysAllow.Title") self.Username_InvalidTooShort = getValue(dict, "Username.InvalidTooShort") self._Settings_ApplyProxyAlert = getValue(dict, "Settings.ApplyProxyAlert") self._Settings_ApplyProxyAlert_r = extractArgumentRanges(self._Settings_ApplyProxyAlert) self.Weekday_ShortFriday = getValue(dict, "Weekday.ShortFriday") + self._Login_BannedPhoneBody = getValue(dict, "Login.BannedPhoneBody") + self._Login_BannedPhoneBody_r = extractArgumentRanges(self._Login_BannedPhoneBody) self.Conversation_ClearAll = getValue(dict, "Conversation.ClearAll") - self.MediaPicker_Moments = getValue(dict, "MediaPicker.Moments") self.Call_ReportIncludeLog = getValue(dict, "Call.ReportIncludeLog") self._Time_MonthOfYear_m3 = getValue(dict, "Time.MonthOfYear_m3") self._Time_MonthOfYear_m3_r = extractArgumentRanges(self._Time_MonthOfYear_m3) self.SharedMedia_EmptyTitle = getValue(dict, "SharedMedia.EmptyTitle") self.Call_PhoneCallInProgressMessage = getValue(dict, "Call.PhoneCallInProgressMessage") + self.Notification_GroupActivated = getValue(dict, "Notification.GroupActivated") self.Checkout_Name = getValue(dict, "Checkout.Name") - self.Preview_GroupPhotoTitle = getValue(dict, "Preview.GroupPhotoTitle") self._AUTH_REGION = getValue(dict, "AUTH_REGION") self._AUTH_REGION_r = extractArgumentRanges(self._AUTH_REGION) self.Settings_NotificationsAndSounds = getValue(dict, "Settings.NotificationsAndSounds") + self.Conversation_EncryptionCanceled = getValue(dict, "Conversation.EncryptionCanceled") self._GroupInfo_InvitationLinkAcceptChannel = getValue(dict, "GroupInfo.InvitationLinkAcceptChannel") self._GroupInfo_InvitationLinkAcceptChannel_r = extractArgumentRanges(self._GroupInfo_InvitationLinkAcceptChannel) - self.Conversation_EncryptionCanceled = getValue(dict, "Conversation.EncryptionCanceled") self.AccessDenied_SaveMedia = getValue(dict, "AccessDenied.SaveMedia") self.InviteText_URL = getValue(dict, "InviteText.URL") self._Channel_AdminLog_MessageInvitedNameUsername = getValue(dict, "Channel.AdminLog.MessageInvitedNameUsername") self._Channel_AdminLog_MessageInvitedNameUsername_r = extractArgumentRanges(self._Channel_AdminLog_MessageInvitedNameUsername) - self.Channel_Username_InvalidTooManyUsernames = getValue(dict, "Channel.Username.InvalidTooManyUsernames") self.Compose_GroupTokenListPlaceholder = getValue(dict, "Compose.GroupTokenListPlaceholder") - self.Profile_ImageUploadError = getValue(dict, "Profile.ImageUploadError") self.Conversation_MessageDeliveryFailed = getValue(dict, "Conversation.MessageDeliveryFailed") self.Privacy_PaymentsClear_PaymentInfo = getValue(dict, "Privacy.PaymentsClear.PaymentInfo") self.Notifications_GroupNotifications = getValue(dict, "Notifications.GroupNotifications") - self.Notification_Mute1hMin = getValue(dict, "Notification.Mute1hMin") self.CheckoutInfo_SaveInfoHelp = getValue(dict, "CheckoutInfo.SaveInfoHelp") + self.Notification_Mute1hMin = getValue(dict, "Notification.Mute1hMin") self.StickerPacksSettings_ArchivedMasks_Info = getValue(dict, "StickerPacksSettings.ArchivedMasks.Info") self.ChannelMembers_WhoCanAddMembers_AllMembers = getValue(dict, "ChannelMembers.WhoCanAddMembers.AllMembers") self.Channel_Edit_PrivatePublicLinkAlert = getValue(dict, "Channel.Edit.PrivatePublicLinkAlert") self.Watch_Conversation_UserInfo = getValue(dict, "Watch.Conversation.UserInfo") + self.Application_Name = getValue(dict, "Application.Name") + self.Conversation_AddToReadingList = getValue(dict, "Conversation.AddToReadingList") self.Conversation_FileDropbox = getValue(dict, "Conversation.FileDropbox") self.Login_PhonePlaceholder = getValue(dict, "Login.PhonePlaceholder") - self.ExplicitContent_AlertUser = getValue(dict, "ExplicitContent.AlertUser") - self.Conversation_AddToReadingList = getValue(dict, "Conversation.AddToReadingList") - self.Application_Name = getValue(dict, "Application.Name") self.Profile_MessageLifetime1d = getValue(dict, "Profile.MessageLifetime1d") - self.Calls_CallTabDescription = getValue(dict, "Calls.CallTabDescription") self.CheckoutInfo_ShippingInfoCityPlaceholder = getValue(dict, "CheckoutInfo.ShippingInfoCityPlaceholder") + self.Calls_CallTabDescription = getValue(dict, "Calls.CallTabDescription") self.Resolve_ErrorNotFound = getValue(dict, "Resolve.ErrorNotFound") self.PhotoEditor_FadeTool = getValue(dict, "PhotoEditor.FadeTool") - self.Channel_TitleShowDiscussion = getValue(dict, "Channel.TitleShowDiscussion") self.Channel_Setup_TypePublicHelp = getValue(dict, "Channel.Setup.TypePublicHelp") self.GroupInfo_InviteLink_RevokeAlert_Success = getValue(dict, "GroupInfo.InviteLink.RevokeAlert.Success") self.Channel_Setup_PublicNoLink = getValue(dict, "Channel.Setup.PublicNoLink") self.Privacy_Calls_P2PHelp = getValue(dict, "Privacy.Calls.P2PHelp") self.Conversation_Info = getValue(dict, "Conversation.Info") - self.ChannelInfo_InvitationLinkDoesNotExist = getValue(dict, "ChannelInfo.InvitationLinkDoesNotExist") self._Time_TodayAt = getValue(dict, "Time.TodayAt") self._Time_TodayAt_r = extractArgumentRanges(self._Time_TodayAt) self.Conversation_Processing = getValue(dict, "Conversation.Processing") self.Conversation_RestrictedInline = getValue(dict, "Conversation.RestrictedInline") + self._InstantPage_AuthorAndDateTitle = getValue(dict, "InstantPage.AuthorAndDateTitle") + self._InstantPage_AuthorAndDateTitle_r = extractArgumentRanges(self._InstantPage_AuthorAndDateTitle) self._Watch_LastSeen_AtDate = getValue(dict, "Watch.LastSeen.AtDate") self._Watch_LastSeen_AtDate_r = extractArgumentRanges(self._Watch_LastSeen_AtDate) self.Conversation_Location = getValue(dict, "Conversation.Location") self.DialogList_PasscodeLockHelp = getValue(dict, "DialogList.PasscodeLockHelp") - self._InstantPage_AuthorAndDateTitle = getValue(dict, "InstantPage.AuthorAndDateTitle") - self._InstantPage_AuthorAndDateTitle_r = extractArgumentRanges(self._InstantPage_AuthorAndDateTitle) self.Channel_Management_Title = getValue(dict, "Channel.Management.Title") self.Notifications_InAppNotificationsPreview = getValue(dict, "Notifications.InAppNotificationsPreview") - self.PrivacySettings_FloodControlError = getValue(dict, "PrivacySettings.FloodControlError") self.EnterPasscode_EnterTitle = getValue(dict, "EnterPasscode.EnterTitle") self.ReportPeer_ReasonOther_Title = getValue(dict, "ReportPeer.ReasonOther.Title") self.Month_GenJanuary = getValue(dict, "Month.GenJanuary") self.Conversation_ForwardChats = getValue(dict, "Conversation.ForwardChats") - self.SharedMedia_TitlePhoto = getValue(dict, "SharedMedia.TitlePhoto") self.Channel_UpdatePhotoItem = getValue(dict, "Channel.UpdatePhotoItem") - self.GroupInfo_InvitationLinkAlreadyAccepted = getValue(dict, "GroupInfo.InvitationLinkAlreadyAccepted") self.UserInfo_StartSecretChat = getValue(dict, "UserInfo.StartSecretChat") - self.Watch_State_Connecting = getValue(dict, "Watch.State.Connecting") self.PrivacySettings_LastSeenNobody = getValue(dict, "PrivacySettings.LastSeenNobody") self._FileSize_MB = getValue(dict, "FileSize.MB") self._FileSize_MB_r = extractArgumentRanges(self._FileSize_MB) - self.TwoStepAuth_ConfirmationAbort = getValue(dict, "TwoStepAuth.ConfirmationAbort") self.ChatSearch_SearchPlaceholder = getValue(dict, "ChatSearch.SearchPlaceholder") - self.GroupInfo_KickedStatus = getValue(dict, "GroupInfo.KickedStatus") + self.TwoStepAuth_ConfirmationAbort = getValue(dict, "TwoStepAuth.ConfirmationAbort") self.TwoStepAuth_SetupPasswordConfirmFailed = getValue(dict, "TwoStepAuth.SetupPasswordConfirmFailed") self._LastSeen_YesterdayAt = getValue(dict, "LastSeen.YesterdayAt") self._LastSeen_YesterdayAt_r = extractArgumentRanges(self._LastSeen_YesterdayAt) + self.GroupInfo_GroupHistoryVisible = getValue(dict, "GroupInfo.GroupHistoryVisible") self.AppleWatch_ReplyPresetsHelp = getValue(dict, "AppleWatch.ReplyPresetsHelp") self.Localization_LanguageName = getValue(dict, "Localization.LanguageName") self.Map_OpenIn = getValue(dict, "Map.OpenIn") @@ -6554,16 +6214,10 @@ public final class PresentationStrings { self._Time_PreciseDate_m1 = getValue(dict, "Time.PreciseDate_m1") self._Time_PreciseDate_m1_r = extractArgumentRanges(self._Time_PreciseDate_m1) self.Month_ShortMay = getValue(dict, "Month.ShortMay") - self._WelcomeScreen_Greeting = getValue(dict, "WelcomeScreen.Greeting") - self._WelcomeScreen_Greeting_r = extractArgumentRanges(self._WelcomeScreen_Greeting) self.Tour_Text3 = getValue(dict, "Tour.Text3") self.Contacts_GlobalSearch = getValue(dict, "Contacts.GlobalSearch") - self.Watch_Suggestion_CallSoon = getValue(dict, "Watch.Suggestion.CallSoon") self.DialogList_LanguageTooltip = getValue(dict, "DialogList.LanguageTooltip") self.Map_LoadError = getValue(dict, "Map.LoadError") - self.WelcomeScreen_Logout = getValue(dict, "WelcomeScreen.Logout") - self._Service_ApplyLocalizationWithWarnings = getValue(dict, "Service.ApplyLocalizationWithWarnings") - self._Service_ApplyLocalizationWithWarnings_r = extractArgumentRanges(self._Service_ApplyLocalizationWithWarnings) self.AccessDenied_VoiceMicrophone = getValue(dict, "AccessDenied.VoiceMicrophone") self._CHANNEL_MESSAGE_STICKER = getValue(dict, "CHANNEL_MESSAGE_STICKER") self._CHANNEL_MESSAGE_STICKER_r = extractArgumentRanges(self._CHANNEL_MESSAGE_STICKER) @@ -6571,79 +6225,70 @@ public final class PresentationStrings { self.PasscodeSettings_TurnPasscodeOff = getValue(dict, "PasscodeSettings.TurnPasscodeOff") self.MediaPicker_AddCaption = getValue(dict, "MediaPicker.AddCaption") self.Channel_AdminLog_BanReadMessages = getValue(dict, "Channel.AdminLog.BanReadMessages") - self.SharedMedia_Outgoing = getValue(dict, "SharedMedia.Outgoing") - self.Channel_About_Error = getValue(dict, "Channel.About.Error") self.Channel_Status = getValue(dict, "Channel.Status") self.Map_ChooseLocationTitle = getValue(dict, "Map.ChooseLocationTitle") self.Map_OpenInYandexNavigator = getValue(dict, "Map.OpenInYandexNavigator") - self.SearchImages_SkipImage = getValue(dict, "SearchImages.SkipImage") self.State_WaitingForNetwork = getValue(dict, "State.WaitingForNetwork") self.TwoStepAuth_EmailHelp = getValue(dict, "TwoStepAuth.EmailHelp") + self.Conversation_StopLiveLocation = getValue(dict, "Conversation.StopLiveLocation") self.PhotoEditor_SharpenTool = getValue(dict, "PhotoEditor.SharpenTool") self.Common_of = getValue(dict, "Common.of") self.AuthSessions_Title = getValue(dict, "AuthSessions.Title") + self.Message_PinnedLiveLocationMessage = getValue(dict, "Message.PinnedLiveLocationMessage") self.PrivacyLastSeenSettings_AlwaysShareWith = getValue(dict, "PrivacyLastSeenSettings.AlwaysShareWith") self.EnterPasscode_EnterPasscode = getValue(dict, "EnterPasscode.EnterPasscode") self.Notifications_Reset = getValue(dict, "Notifications.Reset") + self._Map_LiveLocationPrivateDescription = getValue(dict, "Map.LiveLocationPrivateDescription") + self._Map_LiveLocationPrivateDescription_r = extractArgumentRanges(self._Map_LiveLocationPrivateDescription) self.GroupInfo_InvitationLinkGroupFull = getValue(dict, "GroupInfo.InvitationLinkGroupFull") - self.GoogleDrive_LogoutLogout = getValue(dict, "GoogleDrive.LogoutLogout") self._Channel_AdminLog_MessageChangedChannelUsername = getValue(dict, "Channel.AdminLog.MessageChangedChannelUsername") self._Channel_AdminLog_MessageChangedChannelUsername_r = extractArgumentRanges(self._Channel_AdminLog_MessageChangedChannelUsername) self._CHAT_MESSAGE_DOC = getValue(dict, "CHAT_MESSAGE_DOC") self._CHAT_MESSAGE_DOC_r = extractArgumentRanges(self._CHAT_MESSAGE_DOC) self.Watch_AppName = getValue(dict, "Watch.AppName") - self._Channel_NotificationSelfAdded = getValue(dict, "Channel.NotificationSelfAdded") - self._Channel_NotificationSelfAdded_r = extractArgumentRanges(self._Channel_NotificationSelfAdded) self.ConvertToSupergroup_HelpTitle = getValue(dict, "ConvertToSupergroup.HelpTitle") - self.Conversation_TapAndHoldToRecord = getValue(dict, "Conversation.TapAndHoldToRecord") - self.Channel_ShareNoLink = getValue(dict, "Channel.ShareNoLink") self._MESSAGE_GIF = getValue(dict, "MESSAGE_GIF") self._MESSAGE_GIF_r = extractArgumentRanges(self._MESSAGE_GIF) self._DialogList_EncryptedChatStartedOutgoing = getValue(dict, "DialogList.EncryptedChatStartedOutgoing") self._DialogList_EncryptedChatStartedOutgoing_r = extractArgumentRanges(self._DialogList_EncryptedChatStartedOutgoing) self.Checkout_PayWithTouchId = getValue(dict, "Checkout.PayWithTouchId") - self._Notification_InvitedMany = getValue(dict, "Notification.InvitedMany") - self._Notification_InvitedMany_r = extractArgumentRanges(self._Notification_InvitedMany) + self.Conversation_DiscardVoiceMessageTitle = getValue(dict, "Conversation.DiscardVoiceMessageTitle") self._CHAT_ADD_YOU = getValue(dict, "CHAT_ADD_YOU") self._CHAT_ADD_YOU_r = extractArgumentRanges(self._CHAT_ADD_YOU) self.CheckoutInfo_ShippingInfoCity = getValue(dict, "CheckoutInfo.ShippingInfoCity") - self.Conversation_DiscardVoiceMessageTitle = getValue(dict, "Conversation.DiscardVoiceMessageTitle") self.Conversation_ClousStorageInfo_Description3 = getValue(dict, "Conversation.ClousStorageInfo.Description3") - self.Profile_MessageLifetime = getValue(dict, "Profile.MessageLifetime") - self.GoogleDrive_LogoutTitle = getValue(dict, "GoogleDrive.LogoutTitle") self.Conversation_PinMessageAlertGroup = getValue(dict, "Conversation.PinMessageAlertGroup") self.Settings_FAQ_Intro = getValue(dict, "Settings.FAQ_Intro") self.PrivacySettings_AuthSessions = getValue(dict, "PrivacySettings.AuthSessions") + self._CHAT_MESSAGE_GEOLIVE = getValue(dict, "CHAT_MESSAGE_GEOLIVE") + self._CHAT_MESSAGE_GEOLIVE_r = extractArgumentRanges(self._CHAT_MESSAGE_GEOLIVE) self.Tour_Title5 = getValue(dict, "Tour.Title5") self.ChatAdmins_AllMembersAreAdmins = getValue(dict, "ChatAdmins.AllMembersAreAdmins") self.Group_Management_AddModeratorHelp = getValue(dict, "Group.Management.AddModeratorHelp") self.Channel_Username_CheckingUsername = getValue(dict, "Channel.Username.CheckingUsername") - self.Activity_UploadingAudio = getValue(dict, "Activity.UploadingAudio") self._DialogList_SingleRecordingVideoMessageSuffix = getValue(dict, "DialogList.SingleRecordingVideoMessageSuffix") self._DialogList_SingleRecordingVideoMessageSuffix_r = extractArgumentRanges(self._DialogList_SingleRecordingVideoMessageSuffix) self._Contacts_AccessDeniedHelpPortrait = getValue(dict, "Contacts.AccessDeniedHelpPortrait") self._Contacts_AccessDeniedHelpPortrait_r = extractArgumentRanges(self._Contacts_AccessDeniedHelpPortrait) self._Checkout_LiabilityAlert = getValue(dict, "Checkout.LiabilityAlert") self._Checkout_LiabilityAlert_r = extractArgumentRanges(self._Checkout_LiabilityAlert) - self.Profile_BotInfo = getValue(dict, "Profile.BotInfo") self.Channel_Info_BlackList = getValue(dict, "Channel.Info.BlackList") - self.StickerPack_RemoveStickers = getValue(dict, "StickerPack.RemoveStickers") + self.Profile_BotInfo = getValue(dict, "Profile.BotInfo") self.Compose_NewChannel_Members = getValue(dict, "Compose.NewChannel.Members") self.Notification_Reply = getValue(dict, "Notification.Reply") self.Watch_Stickers_Recents = getValue(dict, "Watch.Stickers.Recents") self.GroupInfo_SetGroupPhotoStop = getValue(dict, "GroupInfo.SetGroupPhotoStop") - self.Conversation_PinMessageAlertChannel = getValue(dict, "Conversation.PinMessageAlertChannel") + self.Channel_Stickers_Placeholder = getValue(dict, "Channel.Stickers.Placeholder") self.AttachmentMenu_File = getValue(dict, "AttachmentMenu.File") self._MESSAGE_STICKER = getValue(dict, "MESSAGE_STICKER") self._MESSAGE_STICKER_r = extractArgumentRanges(self._MESSAGE_STICKER) self.Profile_MessageLifetime5s = getValue(dict, "Profile.MessageLifetime5s") self._PINNED_PHOTO = getValue(dict, "PINNED_PHOTO") self._PINNED_PHOTO_r = extractArgumentRanges(self._PINNED_PHOTO) - self.Channel_EditAdmin_PermissionChangeInviteLink = getValue(dict, "Channel.EditAdmin.PermissionChangeInviteLink") self.Channel_AdminLog_CanAddAdmins = getValue(dict, "Channel.AdminLog.CanAddAdmins") - self.WelcomeScreen_Title = getValue(dict, "WelcomeScreen.Title") self.TwoStepAuth_SetupHint = getValue(dict, "TwoStepAuth.SetupHint") self.Conversation_StatusLeftGroup = getValue(dict, "Conversation.StatusLeftGroup") + self.MediaPicker_TapToUngroupDescription = getValue(dict, "MediaPicker.TapToUngroupDescription") self.Conversation_ShareBotLocationConfirmation = getValue(dict, "Conversation.ShareBotLocationConfirmation") self.Conversation_DeleteMessagesForMe = getValue(dict, "Conversation.DeleteMessagesForMe") self.Message_PinnedAnimationMessage = getValue(dict, "Message.PinnedAnimationMessage") @@ -6652,19 +6297,18 @@ public final class PresentationStrings { self._Time_MonthOfYear_m2 = getValue(dict, "Time.MonthOfYear_m2") self._Time_MonthOfYear_m2_r = extractArgumentRanges(self._Time_MonthOfYear_m2) self.Channel_About_Placeholder = getValue(dict, "Channel.About.Placeholder") + self.Map_Directions = getValue(dict, "Map.Directions") self.Channel_About_Title = getValue(dict, "Channel.About.Title") self._MESSAGE_PHOTO = getValue(dict, "MESSAGE_PHOTO") self._MESSAGE_PHOTO_r = extractArgumentRanges(self._MESSAGE_PHOTO) self.Calls_RatingTitle = getValue(dict, "Calls.RatingTitle") self.SharedMedia_EmptyText = getValue(dict, "SharedMedia.EmptyText") - self.Channel_Username_CreateCommentsHelp = getValue(dict, "Channel.Username.CreateCommentsHelp") + self.Channel_Stickers_Searching = getValue(dict, "Channel.Stickers.Searching") self.Login_PadPhoneHelp = getValue(dict, "Login.PadPhoneHelp") self.StickerPacksSettings_ArchivedPacks = getValue(dict, "StickerPacksSettings.ArchivedPacks") self.Channel_ErrorAccessDenied = getValue(dict, "Channel.ErrorAccessDenied") self.Generic_ErrorMoreInfo = getValue(dict, "Generic.ErrorMoreInfo") - self.Notification_GroupDeactivated = getValue(dict, "Notification.GroupDeactivated") self.Channel_AdminLog_TitleAllEvents = getValue(dict, "Channel.AdminLog.TitleAllEvents") - self.PrivacySettings_TouchIdTitle = getValue(dict, "PrivacySettings.TouchIdTitle") self.ChannelMembers_WhoCanAddMembersAllHelp = getValue(dict, "ChannelMembers.WhoCanAddMembersAllHelp") self.ChangePhoneNumberCode_CodePlaceholder = getValue(dict, "ChangePhoneNumberCode.CodePlaceholder") self.Camera_SquareMode = getValue(dict, "Camera.SquareMode") @@ -6677,41 +6321,33 @@ public final class PresentationStrings { self.PhotoEditor_VignetteTool = getValue(dict, "PhotoEditor.VignetteTool") self.LastSeen_WithinAWeek = getValue(dict, "LastSeen.WithinAWeek") self.Widget_NoUsers = getValue(dict, "Widget.NoUsers") - self.Channel_Edit_EnableComments = getValue(dict, "Channel.Edit.EnableComments") - self.DialogList_NoMessagesText = getValue(dict, "DialogList.NoMessagesText") + self.Calls_NewCall = getValue(dict, "Calls.NewCall") self._CHANNEL_MESSAGE_AUDIO = getValue(dict, "CHANNEL_MESSAGE_AUDIO") self._CHANNEL_MESSAGE_AUDIO_r = extractArgumentRanges(self._CHANNEL_MESSAGE_AUDIO) - self.Calls_NewCall = getValue(dict, "Calls.NewCall") - self.SharedMedia_TitleFile = getValue(dict, "SharedMedia.TitleFile") + self.DialogList_NoMessagesText = getValue(dict, "DialogList.NoMessagesText") self.MaskStickerSettings_Info = getValue(dict, "MaskStickerSettings.Info") self.Conversation_FilePhotoOrVideo = getValue(dict, "Conversation.FilePhotoOrVideo") - self._Watch_LastSeen_AtWeekday = getValue(dict, "Watch.LastSeen.AtWeekday") - self._Watch_LastSeen_AtWeekday_r = extractArgumentRanges(self._Watch_LastSeen_AtWeekday) self.Channel_AdminLog_BanSendStickers = getValue(dict, "Channel.AdminLog.BanSendStickers") self.Common_Next = getValue(dict, "Common.Next") + self.Stickers_RemoveFromFavorites = getValue(dict, "Stickers.RemoveFromFavorites") self.Watch_Notification_Joined = getValue(dict, "Watch.Notification.Joined") self._Channel_AdminLog_MessageRestrictedNewSetting = getValue(dict, "Channel.AdminLog.MessageRestrictedNewSetting") self._Channel_AdminLog_MessageRestrictedNewSetting_r = extractArgumentRanges(self._Channel_AdminLog_MessageRestrictedNewSetting) - self.ImagePicker_NoVideos = getValue(dict, "ImagePicker.NoVideos") self.GroupInfo_DeleteAndExitConfirmation = getValue(dict, "GroupInfo.DeleteAndExitConfirmation") - self.ChatSettings_Cache = getValue(dict, "ChatSettings.Cache") self.TwoStepAuth_EmailInvalid = getValue(dict, "TwoStepAuth.EmailInvalid") self._CHAT_MESSAGE_VIDEO = getValue(dict, "CHAT_MESSAGE_VIDEO") self._CHAT_MESSAGE_VIDEO_r = extractArgumentRanges(self._CHAT_MESSAGE_VIDEO) self.Month_GenJune = getValue(dict, "Month.GenJune") + self.Map_LiveLocationFor15Minutes = getValue(dict, "Map.LiveLocationFor15Minutes") self._Login_EmailCodeSubject = getValue(dict, "Login.EmailCodeSubject") self._Login_EmailCodeSubject_r = extractArgumentRanges(self._Login_EmailCodeSubject) self._CHAT_TITLE_EDITED = getValue(dict, "CHAT_TITLE_EDITED") self._CHAT_TITLE_EDITED_r = extractArgumentRanges(self._CHAT_TITLE_EDITED) - self.Watch_UnlockRequired = getValue(dict, "Watch.UnlockRequired") self._NetworkUsageSettings_WifiUsageSince = getValue(dict, "NetworkUsageSettings.WifiUsageSince") self._NetworkUsageSettings_WifiUsageSince_r = extractArgumentRanges(self._NetworkUsageSettings_WifiUsageSince) self.Watch_LastSeen_Lately = getValue(dict, "Watch.LastSeen.Lately") self.Watch_Compose_CurrentLocation = getValue(dict, "Watch.Compose.CurrentLocation") - self._CHANNEL_MESSAGE_FWDS = getValue(dict, "CHANNEL_MESSAGE_FWDS") - self._CHANNEL_MESSAGE_FWDS_r = extractArgumentRanges(self._CHANNEL_MESSAGE_FWDS) self.DialogList_RecentTitlePeople = getValue(dict, "DialogList.RecentTitlePeople") - self.Conversation_ViewLocation = getValue(dict, "Conversation.ViewLocation") self.GroupInfo_Notifications = getValue(dict, "GroupInfo.Notifications") self.Call_ReportPlaceholder = getValue(dict, "Call.ReportPlaceholder") self._MESSAGE_DOC = getValue(dict, "MESSAGE_DOC") @@ -6723,8 +6359,9 @@ public final class PresentationStrings { self._MediaPicker_Nof = getValue(dict, "MediaPicker.Nof") self._MediaPicker_Nof_r = extractArgumentRanges(self._MediaPicker_Nof) self.Common_Create = getValue(dict, "Common.Create") - self.Message_InvoiceShipmentLabel = getValue(dict, "Message.InvoiceShipmentLabel") self.Contacts_TopSection = getValue(dict, "Contacts.TopSection") + self._Map_DirectionsDriveEta = getValue(dict, "Map.DirectionsDriveEta") + self._Map_DirectionsDriveEta_r = extractArgumentRanges(self._Map_DirectionsDriveEta) self.Your_cards_number_is_invalid = getValue(dict, "Your_cards_number_is_invalid") self._MESSAGE_INVOICE = getValue(dict, "MESSAGE_INVOICE") self._MESSAGE_INVOICE_r = extractArgumentRanges(self._MESSAGE_INVOICE) @@ -6737,23 +6374,21 @@ public final class PresentationStrings { self._LastSeen_AtDate_r = extractArgumentRanges(self._LastSeen_AtDate) self.Conversation_MessageDialogRetry = getValue(dict, "Conversation.MessageDialogRetry") self.Watch_ChatList_NoConversationsTitle = getValue(dict, "Watch.ChatList.NoConversationsTitle") + self.Stickers_GroupStickers = getValue(dict, "Stickers.GroupStickers") self.BlockedUsers_Title = getValue(dict, "BlockedUsers.Title") + self._LiveLocationUpdated_TodayAt = getValue(dict, "LiveLocationUpdated.TodayAt") + self._LiveLocationUpdated_TodayAt_r = extractArgumentRanges(self._LiveLocationUpdated_TodayAt) self.ChatSettings_ConnectionType_UseSocks5 = getValue(dict, "ChatSettings.ConnectionType.UseSocks5") - self.MediaPicker_MomentsDateRangeYearFormat = getValue(dict, "MediaPicker.MomentsDateRangeYearFormat") self.Cache_ClearNone = getValue(dict, "Cache.ClearNone") self.SecretTimer_VideoDescription = getValue(dict, "SecretTimer.VideoDescription") self.Login_InvalidCodeError = getValue(dict, "Login.InvalidCodeError") - self.Contacts_contacts = getValue(dict, "Contacts.contacts") self.Channel_BanList_BlockedTitle = getValue(dict, "Channel.BanList.BlockedTitle") self.NetworkUsageSettings_Cellular = getValue(dict, "NetworkUsageSettings.Cellular") self.Watch_Location_Access = getValue(dict, "Watch.Location.Access") - self._CONTACT_ACTIVATED = getValue(dict, "CONTACT_ACTIVATED") - self._CONTACT_ACTIVATED_r = extractArgumentRanges(self._CONTACT_ACTIVATED) - self.BlockedUsers_AlreadyBlocked = getValue(dict, "BlockedUsers.AlreadyBlocked") self.PrivacySettings_DeleteAccountIfAwayFor = getValue(dict, "PrivacySettings.DeleteAccountIfAwayFor") - self.PrivacySettings_DeleteAccountTitle = getValue(dict, "PrivacySettings.DeleteAccountTitle") - self.Channel_AdminLog_EmptyText = getValue(dict, "Channel.AdminLog.EmptyText") self.Channel_AdminLog_EmptyFilterText = getValue(dict, "Channel.AdminLog.EmptyFilterText") + self.Channel_AdminLog_EmptyText = getValue(dict, "Channel.AdminLog.EmptyText") + self.PrivacySettings_DeleteAccountTitle = getValue(dict, "PrivacySettings.DeleteAccountTitle") self.PrivacyLastSeenSettings_CustomShareSettings_Delete = getValue(dict, "PrivacyLastSeenSettings.CustomShareSettings.Delete") self._ENCRYPTED_MESSAGE = getValue(dict, "ENCRYPTED_MESSAGE") self._ENCRYPTED_MESSAGE_r = extractArgumentRanges(self._ENCRYPTED_MESSAGE) @@ -6762,28 +6397,21 @@ public final class PresentationStrings { self.TwoStepAuth_EnterPasswordHelp = getValue(dict, "TwoStepAuth.EnterPasswordHelp") self.Bot_Stop = getValue(dict, "Bot.Stop") self.Privacy_GroupsAndChannels_AlwaysAllow_Placeholder = getValue(dict, "Privacy.GroupsAndChannels.AlwaysAllow.Placeholder") - self._AUTH_UNKNOWN = getValue(dict, "AUTH_UNKNOWN") - self._AUTH_UNKNOWN_r = extractArgumentRanges(self._AUTH_UNKNOWN) self.UserInfo_BotSettings = getValue(dict, "UserInfo.BotSettings") self.Your_cards_expiration_month_is_invalid = getValue(dict, "Your_cards_expiration_month_is_invalid") self.PrivacyLastSeenSettings_EmpryUsersPlaceholder = getValue(dict, "PrivacyLastSeenSettings.EmpryUsersPlaceholder") self._CHANNEL_MESSAGE_ROUND = getValue(dict, "CHANNEL_MESSAGE_ROUND") self._CHANNEL_MESSAGE_ROUND_r = extractArgumentRanges(self._CHANNEL_MESSAGE_ROUND) - self.GoogleDrive_FolderLoadError = getValue(dict, "GoogleDrive.FolderLoadError") self.SocksProxySetup_Port = getValue(dict, "SocksProxySetup.Port") self.Message_VideoMessage = getValue(dict, "Message.VideoMessage") self.Conversation_ContextMenuStickerPackInfo = getValue(dict, "Conversation.ContextMenuStickerPackInfo") - self.Watch_Suggestion_TextInABit = getValue(dict, "Watch.Suggestion.TextInABit") + self.Login_ResetAccountProtected_LimitExceeded = getValue(dict, "Login.ResetAccountProtected.LimitExceeded") self._CHAT_DELETE_MEMBER = getValue(dict, "CHAT_DELETE_MEMBER") self._CHAT_DELETE_MEMBER_r = extractArgumentRanges(self._CHAT_DELETE_MEMBER) - self.Login_ResetAccountProtected_LimitExceeded = getValue(dict, "Login.ResetAccountProtected.LimitExceeded") - self.Conversation_EncryptedForwardingAlert = getValue(dict, "Conversation.EncryptedForwardingAlert") self.Conversation_DiscardVoiceMessageAction = getValue(dict, "Conversation.DiscardVoiceMessageAction") self.Camera_Title = getValue(dict, "Camera.Title") self.PhotoEditor_CurvesBlue = getValue(dict, "PhotoEditor.CurvesBlue") self.Message_PinnedVideoMessage = getValue(dict, "Message.PinnedVideoMessage") - self._Settings_OpenSystemPrivacySettings = getValue(dict, "Settings.OpenSystemPrivacySettings") - self._Settings_OpenSystemPrivacySettings_r = extractArgumentRanges(self._Settings_OpenSystemPrivacySettings) self._Login_EmailPhoneSubject = getValue(dict, "Login.EmailPhoneSubject") self._Login_EmailPhoneSubject_r = extractArgumentRanges(self._Login_EmailPhoneSubject) self.Group_EditAdmin_PermissionChangeInfo = getValue(dict, "Group.EditAdmin.PermissionChangeInfo") @@ -6796,14 +6424,13 @@ public final class PresentationStrings { self.AccessDenied_Title = getValue(dict, "AccessDenied.Title") self.SharedMedia_CategoryLinks = getValue(dict, "SharedMedia.CategoryLinks") self.Localization_LanguageOther = getValue(dict, "Localization.LanguageOther") - self.Conversation_ClearAllConfirmation = getValue(dict, "Conversation.ClearAllConfirmation") self.TwoStepAuth_EmailSkipAlert = getValue(dict, "TwoStepAuth.EmailSkipAlert") self.ChatSettings_Stickers = getValue(dict, "ChatSettings.Stickers") self.Camera_FlashOff = getValue(dict, "Camera.FlashOff") self.TwoStepAuth_Title = getValue(dict, "TwoStepAuth.Title") + self.Checkout_ErrorProviderAccountTimeout = getValue(dict, "Checkout.ErrorProviderAccountTimeout") self.TwoStepAuth_SetupPasswordEnterPasswordChange = getValue(dict, "TwoStepAuth.SetupPasswordEnterPasswordChange") self.WebSearch_Images = getValue(dict, "WebSearch.Images") - self.Checkout_ErrorProviderAccountTimeout = getValue(dict, "Checkout.ErrorProviderAccountTimeout") self.Conversation_typing = getValue(dict, "Conversation.typing") self.Common_Back = getValue(dict, "Common.Back") self.Common_Search = getValue(dict, "Common.Search") @@ -6821,12 +6448,12 @@ public final class PresentationStrings { self._EncryptionKey_Description = getValue(dict, "EncryptionKey.Description") self._EncryptionKey_Description_r = extractArgumentRanges(self._EncryptionKey_Description) self.Conversation_UnreadMessages = getValue(dict, "Conversation.UnreadMessages") + self._DialogList_LiveLocationSharingTo = getValue(dict, "DialogList.LiveLocationSharingTo") + self._DialogList_LiveLocationSharingTo_r = extractArgumentRanges(self._DialogList_LiveLocationSharingTo) self.Tour_Title3 = getValue(dict, "Tour.Title3") self.PrivacyLastSeenSettings_GroupsAndChannelsHelp = getValue(dict, "PrivacyLastSeenSettings.GroupsAndChannelsHelp") self.Watch_Contacts_NoResults = getValue(dict, "Watch.Contacts.NoResults") self.Watch_UserInfo_MuteTitle = getValue(dict, "Watch.UserInfo.MuteTitle") - self.MediaPicker_Choose = getValue(dict, "MediaPicker.Choose") - self.Conversation_DownloadMegabytes = getValue(dict, "Conversation.DownloadMegabytes") self._Privacy_GroupsAndChannels_InviteToGroupError = getValue(dict, "Privacy.GroupsAndChannels.InviteToGroupError") self._Privacy_GroupsAndChannels_InviteToGroupError_r = extractArgumentRanges(self._Privacy_GroupsAndChannels_InviteToGroupError) self._Message_PinnedTextMessage = getValue(dict, "Message.PinnedTextMessage") @@ -6838,31 +6465,29 @@ public final class PresentationStrings { self.Map_LocatingError = getValue(dict, "Map.LocatingError") self.MediaPicker_Send = getValue(dict, "MediaPicker.Send") self.ChannelIntro_Title = getValue(dict, "ChannelIntro.Title") - self.SearchImages_ErrorDownloadingImage = getValue(dict, "SearchImages.ErrorDownloadingImage") + self.AccessDenied_LocationAlwaysDenied = getValue(dict, "AccessDenied.LocationAlwaysDenied") self._PINNED_GIF = getValue(dict, "PINNED_GIF") self._PINNED_GIF_r = extractArgumentRanges(self._PINNED_GIF) self._InviteText_SingleContact = getValue(dict, "InviteText.SingleContact") self._InviteText_SingleContact_r = extractArgumentRanges(self._InviteText_SingleContact) self.Channel_EditAdmin_CannotEdit = getValue(dict, "Channel.EditAdmin.CannotEdit") - self.Profile_PhonebookAccessDisabled = getValue(dict, "Profile.PhonebookAccessDisabled") self.LoginPassword_PasswordHelp = getValue(dict, "LoginPassword.PasswordHelp") self.BlockedUsers_Unblock = getValue(dict, "BlockedUsers.Unblock") self._Time_MonthOfYear_m1 = getValue(dict, "Time.MonthOfYear_m1") self._Time_MonthOfYear_m1_r = extractArgumentRanges(self._Time_MonthOfYear_m1) - self.Conversation_ViewFile = getValue(dict, "Conversation.ViewFile") self.Notifications_GroupNotificationsAlert = getValue(dict, "Notifications.GroupNotificationsAlert") self.Paint_Masks = getValue(dict, "Paint.Masks") self.StickerPack_ErrorNotFound = getValue(dict, "StickerPack.ErrorNotFound") self.SecretTimer_ImageDescription = getValue(dict, "SecretTimer.ImageDescription") self._PINNED_CONTACT = getValue(dict, "PINNED_CONTACT") self._PINNED_CONTACT_r = extractArgumentRanges(self._PINNED_CONTACT) - self._Conversation_ForwardToGroupFormat = getValue(dict, "Conversation.ForwardToGroupFormat") - self._Conversation_ForwardToGroupFormat_r = extractArgumentRanges(self._Conversation_ForwardToGroupFormat) self._FileSize_KB = getValue(dict, "FileSize.KB") self._FileSize_KB_r = extractArgumentRanges(self._FileSize_KB) + self.Map_LiveLocationTitle = getValue(dict, "Map.LiveLocationTitle") self.Watch_GroupInfo_Title = getValue(dict, "Watch.GroupInfo.Title") self.Channel_AdminLog_EmptyTitle = getValue(dict, "Channel.AdminLog.EmptyTitle") self.PhotoEditor_Set = getValue(dict, "PhotoEditor.Set") + self.LiveLocation_MenuStopAll = getValue(dict, "LiveLocation.MenuStopAll") self._Notification_Invited = getValue(dict, "Notification.Invited") self._Notification_Invited_r = extractArgumentRanges(self._Notification_Invited) self.Watch_AuthRequired = getValue(dict, "Watch.AuthRequired") @@ -6870,62 +6495,50 @@ public final class PresentationStrings { self.AppleWatch_ReplyPresets = getValue(dict, "AppleWatch.ReplyPresets") self.Channel_Members_AddAdminErrorNotAMember = getValue(dict, "Channel.Members.AddAdminErrorNotAMember") self.Conversation_EncryptedDescription2 = getValue(dict, "Conversation.EncryptedDescription2") - self.Paint_Edit = getValue(dict, "Paint.Edit") self.NetworkUsageSettings_MediaVideoDataSection = getValue(dict, "NetworkUsageSettings.MediaVideoDataSection") + self.Paint_Edit = getValue(dict, "Paint.Edit") self.Conversation_EncryptedDescription3 = getValue(dict, "Conversation.EncryptedDescription3") self.Login_CodeFloodError = getValue(dict, "Login.CodeFloodError") - self._Call_EncryptionKey_Description = getValue(dict, "Call.EncryptionKey.Description") - self._Call_EncryptionKey_Description_r = extractArgumentRanges(self._Call_EncryptionKey_Description) self.Conversation_EncryptedDescription4 = getValue(dict, "Conversation.EncryptedDescription4") self.AppleWatch_Title = getValue(dict, "AppleWatch.Title") self.Contacts_AccessDeniedError = getValue(dict, "Contacts.AccessDeniedError") self.Conversation_StatusTyping = getValue(dict, "Conversation.StatusTyping") - self.GoogleDrive_LoadErrorTitle = getValue(dict, "GoogleDrive.LoadErrorTitle") self.Share_Title = getValue(dict, "Share.Title") - self.Map_Send = getValue(dict, "Map.Send") self.TwoStepAuth_ConfirmationTitle = getValue(dict, "TwoStepAuth.ConfirmationTitle") - self.Conversation_SupportPlaceholder = getValue(dict, "Conversation.SupportPlaceholder") self.ChatSettings_Title = getValue(dict, "ChatSettings.Title") self.AuthSessions_CurrentSession = getValue(dict, "AuthSessions.CurrentSession") self.Watch_Microphone_Access = getValue(dict, "Watch.Microphone.Access") self._Notification_RenamedChat = getValue(dict, "Notification.RenamedChat") self._Notification_RenamedChat_r = extractArgumentRanges(self._Notification_RenamedChat) + self.Conversation_LiveLocation = getValue(dict, "Conversation.LiveLocation") self.Watch_Conversation_GroupInfo = getValue(dict, "Watch.Conversation.GroupInfo") self.UserInfo_Title = getValue(dict, "UserInfo.Title") - self.Service_LocalizationDownloadError = getValue(dict, "Service.LocalizationDownloadError") + self.Map_LiveLocationGroupDescription = getValue(dict, "Map.LiveLocationGroupDescription") self.Login_InfoHelp = getValue(dict, "Login.InfoHelp") self.ShareMenu_ShareTo = getValue(dict, "ShareMenu.ShareTo") self.Message_PinnedGame = getValue(dict, "Message.PinnedGame") self.Channel_AdminLog_CanSendMessages = getValue(dict, "Channel.AdminLog.CanSendMessages") self.Notification_RenamedGroup = getValue(dict, "Notification.RenamedGroup") - self.Weekday_Thursday = getValue(dict, "Weekday.Thursday") self._Call_PrivacyErrorMessage = getValue(dict, "Call.PrivacyErrorMessage") self._Call_PrivacyErrorMessage_r = extractArgumentRanges(self._Call_PrivacyErrorMessage) self.ChangePhoneNumberNumber_Title = getValue(dict, "ChangePhoneNumberNumber.Title") self.TwoStepAuth_EnterPasswordInvalid = getValue(dict, "TwoStepAuth.EnterPasswordInvalid") self.DialogList_SearchSectionMessages = getValue(dict, "DialogList.SearchSectionMessages") - self._Profile_ShareBotGroupFormat = getValue(dict, "Profile.ShareBotGroupFormat") - self._Profile_ShareBotGroupFormat_r = extractArgumentRanges(self._Profile_ShareBotGroupFormat) + self.Media_ShareThisVideo = getValue(dict, "Media.ShareThisVideo") self.Call_ReportIncludeLogDescription = getValue(dict, "Call.ReportIncludeLogDescription") self.Preview_DeleteGif = getValue(dict, "Preview.DeleteGif") - self.Weekday_Saturday = getValue(dict, "Weekday.Saturday") self.UserInfo_DeleteContact = getValue(dict, "UserInfo.DeleteContact") self.Notifications_ResetAllNotifications = getValue(dict, "Notifications.ResetAllNotifications") self.Notification_MessageLifetimeRemovedOutgoing = getValue(dict, "Notification.MessageLifetimeRemovedOutgoing") - self.Map_More = getValue(dict, "Map.More") self.Login_ContinueWithLocalization = getValue(dict, "Login.ContinueWithLocalization") self.GroupInfo_AddParticipant = getValue(dict, "GroupInfo.AddParticipant") self.Watch_Location_Current = getValue(dict, "Watch.Location.Current") - self.Map_MapTitle = getValue(dict, "Map.MapTitle") self.Checkout_NewCard_SaveInfoHelp = getValue(dict, "Checkout.NewCard.SaveInfoHelp") self._Settings_ApplyProxyAlertCredentials = getValue(dict, "Settings.ApplyProxyAlertCredentials") self._Settings_ApplyProxyAlertCredentials_r = extractArgumentRanges(self._Settings_ApplyProxyAlertCredentials) self.MediaPicker_CameraRoll = getValue(dict, "MediaPicker.CameraRoll") - self._TwoStepAuth_RecoverySent = getValue(dict, "TwoStepAuth.RecoverySent") - self._TwoStepAuth_RecoverySent_r = extractArgumentRanges(self._TwoStepAuth_RecoverySent) self.Channel_AdminLog_CanPinMessages = getValue(dict, "Channel.AdminLog.CanPinMessages") self.KeyCommand_NewMessage = getValue(dict, "KeyCommand.NewMessage") - self.Compose_NewBroadcastButton = getValue(dict, "Compose.NewBroadcastButton") self._Time_PreciseDate_m12 = getValue(dict, "Time.PreciseDate_m12") self._Time_PreciseDate_m12_r = extractArgumentRanges(self._Time_PreciseDate_m12) self.NetworkUsageSettings_TotalSection = getValue(dict, "NetworkUsageSettings.TotalSection") @@ -6937,7 +6550,6 @@ public final class PresentationStrings { self._Notification_ChangedGroupPhoto_r = extractArgumentRanges(self._Notification_ChangedGroupPhoto) self.TwoStepAuth_RemovePassword = getValue(dict, "TwoStepAuth.RemovePassword") self.Privacy_GroupsAndChannels_CustomHelp = getValue(dict, "Privacy.GroupsAndChannels.CustomHelp") - self.Notification_GroupMigratedToChannel = getValue(dict, "Notification.GroupMigratedToChannel") self.UserInfo_NotificationsDisable = getValue(dict, "UserInfo.NotificationsDisable") self.Watch_UserInfo_Service = getValue(dict, "Watch.UserInfo.Service") self.Privacy_Calls_CustomHelp = getValue(dict, "Privacy.Calls.CustomHelp") @@ -6947,7 +6559,6 @@ public final class PresentationStrings { self.DialogList_ClearHistoryConfirmation = getValue(dict, "DialogList.ClearHistoryConfirmation") self.CheckoutInfo_ErrorEmailInvalid = getValue(dict, "CheckoutInfo.ErrorEmailInvalid") self.Month_GenNovember = getValue(dict, "Month.GenNovember") - self.PhotoEditor_TintIntensity = getValue(dict, "PhotoEditor.TintIntensity") self.UserInfo_NotificationsEnable = getValue(dict, "UserInfo.NotificationsEnable") self._Target_InviteToGroupConfirmation = getValue(dict, "Target.InviteToGroupConfirmation") self._Target_InviteToGroupConfirmation_r = extractArgumentRanges(self._Target_InviteToGroupConfirmation) @@ -6955,7 +6566,6 @@ public final class PresentationStrings { self.Map_OpenInMaps = getValue(dict, "Map.OpenInMaps") self.Common_OK = getValue(dict, "Common.OK") self.TwoStepAuth_SetupHintTitle = getValue(dict, "TwoStepAuth.SetupHintTitle") - self.Watch_Suggestion_Nope = getValue(dict, "Watch.Suggestion.Nope") self.GroupInfo_LeftStatus = getValue(dict, "GroupInfo.LeftStatus") self.Cache_ClearProgress = getValue(dict, "Cache.ClearProgress") self.Login_InvalidPhoneError = getValue(dict, "Login.InvalidPhoneError") @@ -6965,27 +6575,22 @@ public final class PresentationStrings { self._Channel_AdminLog_MessageRemovedGroupUsername = getValue(dict, "Channel.AdminLog.MessageRemovedGroupUsername") self._Channel_AdminLog_MessageRemovedGroupUsername_r = extractArgumentRanges(self._Channel_AdminLog_MessageRemovedGroupUsername) self.ChatSettings_AutomaticPhotoDownload = getValue(dict, "ChatSettings.AutomaticPhotoDownload") - self.Update_Update = getValue(dict, "Update.Update") self.Group_ErrorAddTooMuchAdmins = getValue(dict, "Group.ErrorAddTooMuchAdmins") + self.SocksProxySetup_Password = getValue(dict, "SocksProxySetup.Password") self.Login_SelectCountry_Title = getValue(dict, "Login.SelectCountry.Title") - self.Notification_EncryptedChatAccepted = getValue(dict, "Notification.EncryptedChatAccepted") self.Notifications_GroupNotificationsHelp = getValue(dict, "Notifications.GroupNotificationsHelp") self.PhotoEditor_CropAspectRatioSquare = getValue(dict, "PhotoEditor.CropAspectRatioSquare") self.Notification_CallOutgoing = getValue(dict, "Notification.CallOutgoing") - self.SocksProxySetup_Password = getValue(dict, "SocksProxySetup.Password") self.Weekday_ShortMonday = getValue(dict, "Weekday.ShortMonday") - self.Channel_Edit_AboutItem = getValue(dict, "Channel.Edit.AboutItem") self.Checkout_Receipt_Title = getValue(dict, "Checkout.Receipt.Title") + self.Channel_Edit_AboutItem = getValue(dict, "Channel.Edit.AboutItem") self.Login_InfoLastNamePlaceholder = getValue(dict, "Login.InfoLastNamePlaceholder") - self.Contacts_InvitationText = getValue(dict, "Contacts.InvitationText") self.Channel_Members_AddMembersHelp = getValue(dict, "Channel.Members.AddMembersHelp") self._MESSAGE_VIDEO_SECRET = getValue(dict, "MESSAGE_VIDEO_SECRET") self._MESSAGE_VIDEO_SECRET_r = extractArgumentRanges(self._MESSAGE_VIDEO_SECRET) self.ReportPeer_Report = getValue(dict, "ReportPeer.Report") self.Channel_EditMessageErrorGeneric = getValue(dict, "Channel.EditMessageErrorGeneric") self.LoginPassword_FloodError = getValue(dict, "LoginPassword.FloodError") - self.EncryptionKey_TapToEmojify = getValue(dict, "EncryptionKey.TapToEmojify") - self.Conversation_InfoChannel = getValue(dict, "Conversation.InfoChannel") self.TwoStepAuth_SetupPasswordTitle = getValue(dict, "TwoStepAuth.SetupPasswordTitle") self.PhotoEditor_DiscardChanges = getValue(dict, "PhotoEditor.DiscardChanges") self.Group_UpgradeNoticeText2 = getValue(dict, "Group.UpgradeNoticeText2") @@ -6994,8 +6599,6 @@ public final class PresentationStrings { self._ChannelInfo_ChannelForbidden = getValue(dict, "ChannelInfo.ChannelForbidden") self._ChannelInfo_ChannelForbidden_r = extractArgumentRanges(self._ChannelInfo_ChannelForbidden) self.Conversation_ShareMyContactInfo = getValue(dict, "Conversation.ShareMyContactInfo") - self._Profile_ShareContactPersonFormat = getValue(dict, "Profile.ShareContactPersonFormat") - self._Profile_ShareContactPersonFormat_r = extractArgumentRanges(self._Profile_ShareContactPersonFormat) self._CHANNEL_MESSAGE_GEO = getValue(dict, "CHANNEL_MESSAGE_GEO") self._CHANNEL_MESSAGE_GEO_r = extractArgumentRanges(self._CHANNEL_MESSAGE_GEO) self.Contacts_PhoneNumber = getValue(dict, "Contacts.PhoneNumber") @@ -7003,20 +6606,19 @@ public final class PresentationStrings { self.Channel_AdminLogFilter_ChannelEventsInfo = getValue(dict, "Channel.AdminLogFilter.ChannelEventsInfo") self.StickerPacksSettings_FeaturedPacks = getValue(dict, "StickerPacksSettings.FeaturedPacks") self.Month_GenAugust = getValue(dict, "Month.GenAugust") + self.Notification_CallCanceled = getValue(dict, "Notification.CallCanceled") self.Channel_Username_CreatePublicLinkHelp = getValue(dict, "Channel.Username.CreatePublicLinkHelp") self.StickerPack_Send = getValue(dict, "StickerPack.Send") + self.StickerSettings_MaskContextInfo = getValue(dict, "StickerSettings.MaskContextInfo") self.Watch_Suggestion_HoldOn = getValue(dict, "Watch.Suggestion.HoldOn") - self.AttachmentMenu_ImageSearch = getValue(dict, "AttachmentMenu.ImageSearch") - self.PasscodeSettings_EncryptData = getValue(dict, "PasscodeSettings.EncryptData") self._PINNED_GEO = getValue(dict, "PINNED_GEO") self._PINNED_GEO_r = extractArgumentRanges(self._PINNED_GEO) - self.StickerSettings_MaskContextInfo = getValue(dict, "StickerSettings.MaskContextInfo") - self.Notification_CallCanceled = getValue(dict, "Notification.CallCanceled") + self.PasscodeSettings_EncryptData = getValue(dict, "PasscodeSettings.EncryptData") self.Common_NotNow = getValue(dict, "Common.NotNow") + self.FastTwoStepSetup_PasswordConfirmationPlaceholder = getValue(dict, "FastTwoStepSetup.PasswordConfirmationPlaceholder") self.PasscodeSettings_Title = getValue(dict, "PasscodeSettings.Title") self.StickerPack_BuiltinPackName = getValue(dict, "StickerPack.BuiltinPackName") self.Watch_Suggestion_BRB = getValue(dict, "Watch.Suggestion.BRB") - self.Login_CodeTitle = getValue(dict, "Login.CodeTitle") self._CHAT_MESSAGE_ROUND = getValue(dict, "CHAT_MESSAGE_ROUND") self._CHAT_MESSAGE_ROUND_r = extractArgumentRanges(self._CHAT_MESSAGE_ROUND) self.Notifications_MessageNotificationsAlert = getValue(dict, "Notifications.MessageNotificationsAlert") @@ -7030,12 +6632,12 @@ public final class PresentationStrings { self._CHAT_LEFT = getValue(dict, "CHAT_LEFT") self._CHAT_LEFT_r = extractArgumentRanges(self._CHAT_LEFT) self.LoginPassword_ForgotPassword = getValue(dict, "LoginPassword.ForgotPassword") + self._Map_LiveLocationShortHour = getValue(dict, "Map.LiveLocationShortHour") + self._Map_LiveLocationShortHour_r = extractArgumentRanges(self._Map_LiveLocationShortHour) self._DialogList_AwaitingEncryption = getValue(dict, "DialogList.AwaitingEncryption") self._DialogList_AwaitingEncryption_r = extractArgumentRanges(self._DialogList_AwaitingEncryption) self.ChatSettings_Appearance = getValue(dict, "ChatSettings.Appearance") self.Tour_Title1 = getValue(dict, "Tour.Title1") - self._Notification_ChangedUserPhoto = getValue(dict, "Notification.ChangedUserPhoto") - self._Notification_ChangedUserPhoto_r = extractArgumentRanges(self._Notification_ChangedUserPhoto) self.Conversation_LinkDialogCopy = getValue(dict, "Conversation.LinkDialogCopy") self._Notification_PinnedLocationMessage = getValue(dict, "Notification.PinnedLocationMessage") self._Notification_PinnedLocationMessage_r = extractArgumentRanges(self._Notification_PinnedLocationMessage) @@ -7054,6 +6656,7 @@ public final class PresentationStrings { self.UserInfo_SendMessage = getValue(dict, "UserInfo.SendMessage") self._Channel_Username_LinkHint = getValue(dict, "Channel.Username.LinkHint") self._Channel_Username_LinkHint_r = extractArgumentRanges(self._Channel_Username_LinkHint) + self.Settings_ViewPhoto = getValue(dict, "Settings.ViewPhoto") self.Paint_RecentStickers = getValue(dict, "Paint.RecentStickers") self.Login_CallRequestState3 = getValue(dict, "Login.CallRequestState3") self.Channel_Edit_LinkItem = getValue(dict, "Channel.Edit.LinkItem") @@ -7063,15 +6666,14 @@ public final class PresentationStrings { self.Channel_Moderator_Title = getValue(dict, "Channel.Moderator.Title") self.Message_PinnedPhotoMessage = getValue(dict, "Message.PinnedPhotoMessage") self.Notification_SecretChatScreenshot = getValue(dict, "Notification.SecretChatScreenshot") + self._Conversation_DeleteMessagesFor = getValue(dict, "Conversation.DeleteMessagesFor") + self._Conversation_DeleteMessagesFor_r = extractArgumentRanges(self._Conversation_DeleteMessagesFor) self.Activity_UploadingDocument = getValue(dict, "Activity.UploadingDocument") - self.AccessDenied_LocationTracking = getValue(dict, "AccessDenied.LocationTracking") self.Watch_ChatList_NoConversationsText = getValue(dict, "Watch.ChatList.NoConversationsText") self.ReportPeer_AlertSuccess = getValue(dict, "ReportPeer.AlertSuccess") self.Tour_Text4 = getValue(dict, "Tour.Text4") self.Channel_Info_Description = getValue(dict, "Channel.Info.Description") - self._Conversation_DeleteMessagesFor = getValue(dict, "Conversation.DeleteMessagesFor") - self._Conversation_DeleteMessagesFor_r = extractArgumentRanges(self._Conversation_DeleteMessagesFor) - self.MessageTimer_Title = getValue(dict, "MessageTimer.Title") + self.AccessDenied_LocationTracking = getValue(dict, "AccessDenied.LocationTracking") self.Watch_Compose_Send = getValue(dict, "Watch.Compose.Send") self.SocksProxySetup_UseForCallsHelp = getValue(dict, "SocksProxySetup.UseForCallsHelp") self.Preview_CopyAddress = getValue(dict, "Preview.CopyAddress") @@ -7084,20 +6686,16 @@ public final class PresentationStrings { self.Target_InviteToGroupErrorAlreadyInvited = getValue(dict, "Target.InviteToGroupErrorAlreadyInvited") self.AccessDenied_CameraRestricted = getValue(dict, "AccessDenied.CameraRestricted") self.Watch_Message_ForwardedFrom = getValue(dict, "Watch.Message.ForwardedFrom") + self.CheckoutInfo_ShippingInfoCountryPlaceholder = getValue(dict, "CheckoutInfo.ShippingInfoCountryPlaceholder") self.Channel_AboutItem = getValue(dict, "Channel.AboutItem") self.PhotoEditor_CurvesGreen = getValue(dict, "PhotoEditor.CurvesGreen") - self.CheckoutInfo_ShippingInfoCountryPlaceholder = getValue(dict, "CheckoutInfo.ShippingInfoCountryPlaceholder") self.Month_GenJuly = getValue(dict, "Month.GenJuly") - self.Conversation_DeleteChat = getValue(dict, "Conversation.DeleteChat") self._DialogList_SingleUploadingFileSuffix = getValue(dict, "DialogList.SingleUploadingFileSuffix") self._DialogList_SingleUploadingFileSuffix_r = extractArgumentRanges(self._DialogList_SingleUploadingFileSuffix) self.ChannelIntro_CreateChannel = getValue(dict, "ChannelIntro.CreateChannel") - self.WelcomeScreen_ContactsAccessDisabled = getValue(dict, "WelcomeScreen.ContactsAccessDisabled") self.Channel_Management_AddModerator = getValue(dict, "Channel.Management.AddModerator") self.Common_ChoosePhoto = getValue(dict, "Common.ChoosePhoto") - self.Group_Username_Help = getValue(dict, "Group.Username.Help") self.Conversation_Pin = getValue(dict, "Conversation.Pin") - self.Channel_AdminLog_CanStartCalls = getValue(dict, "Channel.AdminLog.CanStartCalls") self._Login_ResetAccountProtected_Text = getValue(dict, "Login.ResetAccountProtected.Text") self._Login_ResetAccountProtected_Text_r = extractArgumentRanges(self._Login_ResetAccountProtected_Text) self._Channel_AdminLog_EmptyFilterQueryText = getValue(dict, "Channel.AdminLog.EmptyFilterQueryText") @@ -7107,19 +6705,15 @@ public final class PresentationStrings { self.FeaturedStickerPacks_Title = getValue(dict, "FeaturedStickerPacks.Title") self.Map_OpenInGoogleMaps = getValue(dict, "Map.OpenInGoogleMaps") self.Notification_MessageLifetime5s = getValue(dict, "Notification.MessageLifetime5s") - self.EnterPasscode_SetupTitle = getValue(dict, "EnterPasscode.SetupTitle") self.Contacts_Title = getValue(dict, "Contacts.Title") self.Channel_Management_AddModeratorHelp = getValue(dict, "Channel.Management.AddModeratorHelp") self._CHAT_MESSAGE_FWDS = getValue(dict, "CHAT_MESSAGE_FWDS") self._CHAT_MESSAGE_FWDS_r = extractArgumentRanges(self._CHAT_MESSAGE_FWDS) - self.WelcomeScreen_UpdatingTitle = getValue(dict, "WelcomeScreen.UpdatingTitle") - self._Login_CodeHelp = getValue(dict, "Login.CodeHelp") - self._Login_CodeHelp_r = extractArgumentRanges(self._Login_CodeHelp) self.Conversation_MessageDialogEdit = getValue(dict, "Conversation.MessageDialogEdit") self.PrivacyLastSeenSettings_Title = getValue(dict, "PrivacyLastSeenSettings.Title") self.Notifications_ClassicTones = getValue(dict, "Notifications.ClassicTones") - self.GoogleDrive_Title = getValue(dict, "GoogleDrive.Title") self.Conversation_LinkDialogOpen = getValue(dict, "Conversation.LinkDialogOpen") + self.Channel_Info_Subscribers = getValue(dict, "Channel.Info.Subscribers") self.Conversation_ClousStorageInfo_Description4 = getValue(dict, "Conversation.ClousStorageInfo.Description4") self.Privacy_Calls_AlwaysAllow = getValue(dict, "Privacy.Calls.AlwaysAllow") self.Privacy_PaymentsClearInfoHelp = getValue(dict, "Privacy.PaymentsClearInfoHelp") @@ -7132,10 +6726,10 @@ public final class PresentationStrings { self.ConversationProfile_ErrorCreatingConversation = getValue(dict, "ConversationProfile.ErrorCreatingConversation") self._PHONE_CALL_MISSED = getValue(dict, "PHONE_CALL_MISSED") self._PHONE_CALL_MISSED_r = extractArgumentRanges(self._PHONE_CALL_MISSED) - self.Map_AccessDeniedError = getValue(dict, "Map.AccessDeniedError") self._Conversation_Kilobytes = getValue(dict, "Conversation.Kilobytes") self._Conversation_Kilobytes_r = extractArgumentRanges(self._Conversation_Kilobytes) self.Group_ErrorAddBlocked = getValue(dict, "Group.ErrorAddBlocked") + self.TwoStepAuth_AdditionalPassword = getValue(dict, "TwoStepAuth.AdditionalPassword") self.MediaPicker_Videos = getValue(dict, "MediaPicker.Videos") self.BlockedUsers_AddNew = getValue(dict, "BlockedUsers.AddNew") self.StickerPacksSettings_StickerPacksSection = getValue(dict, "StickerPacksSettings.StickerPacksSection") @@ -7145,23 +6739,19 @@ public final class PresentationStrings { self.PhotoEditor_ShadowsTint = getValue(dict, "PhotoEditor.ShadowsTint") self.ExplicitContent_AlertTitle = getValue(dict, "ExplicitContent.AlertTitle") self.Channel_AdminLogFilter_EventsLeaving = getValue(dict, "Channel.AdminLogFilter.EventsLeaving") - self.StickerPack_HideStickers = getValue(dict, "StickerPack.HideStickers") - self._Group_MessageTitleUpdated = getValue(dict, "Group.MessageTitleUpdated") - self._Group_MessageTitleUpdated_r = extractArgumentRanges(self._Group_MessageTitleUpdated) + self.Map_LiveLocationFor8Hours = getValue(dict, "Map.LiveLocationFor8Hours") self.Checkout_EnterPassword = getValue(dict, "Checkout.EnterPassword") + self.StickerPack_HideStickers = getValue(dict, "StickerPack.HideStickers") self.UserInfo_NotificationsEnabled = getValue(dict, "UserInfo.NotificationsEnabled") self.Weekday_ShortTuesday = getValue(dict, "Weekday.ShortTuesday") self.Notification_CallIncomingShort = getValue(dict, "Notification.CallIncomingShort") self.ConvertToSupergroup_Note = getValue(dict, "ConvertToSupergroup.Note") self.Conversation_EmptyPlaceholder = getValue(dict, "Conversation.EmptyPlaceholder") - self.Conversation_BroadcastTitle = getValue(dict, "Conversation.BroadcastTitle") self.Username_Help = getValue(dict, "Username.Help") self.StickerSettings_ContextHide = getValue(dict, "StickerSettings.ContextHide") - self.Preview_LoadingImage = getValue(dict, "Preview.LoadingImage") - self.Weekday_Sunday = getValue(dict, "Weekday.Sunday") - self._Conversation_DownloadProgressKilobytes = getValue(dict, "Conversation.DownloadProgressKilobytes") - self._Conversation_DownloadProgressKilobytes_r = extractArgumentRanges(self._Conversation_DownloadProgressKilobytes) + self.Media_ShareThisPhoto = getValue(dict, "Media.ShareThisPhoto") self.Contacts_ShareTelegram = getValue(dict, "Contacts.ShareTelegram") + self.PrivacySettings_PasscodeAndFaceId = getValue(dict, "PrivacySettings.PasscodeAndFaceId") self.Settings_ChatBackground = getValue(dict, "Settings.ChatBackground") self._MessageTimer_Seconds_zero = getValueWithForm(dict, "MessageTimer.Seconds", .zero) self._MessageTimer_Seconds_one = getValueWithForm(dict, "MessageTimer.Seconds", .one) @@ -7205,6 +6795,12 @@ public final class PresentationStrings { self._MuteFor_Hours_few = getValueWithForm(dict, "MuteFor.Hours", .few) self._MuteFor_Hours_many = getValueWithForm(dict, "MuteFor.Hours", .many) self._MuteFor_Hours_other = getValueWithForm(dict, "MuteFor.Hours", .other) + self._Media_ShareVideo_zero = getValueWithForm(dict, "Media.ShareVideo", .zero) + self._Media_ShareVideo_one = getValueWithForm(dict, "Media.ShareVideo", .one) + self._Media_ShareVideo_two = getValueWithForm(dict, "Media.ShareVideo", .two) + self._Media_ShareVideo_few = getValueWithForm(dict, "Media.ShareVideo", .few) + self._Media_ShareVideo_many = getValueWithForm(dict, "Media.ShareVideo", .many) + self._Media_ShareVideo_other = getValueWithForm(dict, "Media.ShareVideo", .other) self._MessageTimer_ShortMinutes_zero = getValueWithForm(dict, "MessageTimer.ShortMinutes", .zero) self._MessageTimer_ShortMinutes_one = getValueWithForm(dict, "MessageTimer.ShortMinutes", .one) self._MessageTimer_ShortMinutes_two = getValueWithForm(dict, "MessageTimer.ShortMinutes", .two) @@ -7247,18 +6843,18 @@ public final class PresentationStrings { self._Call_ShortSeconds_few = getValueWithForm(dict, "Call.ShortSeconds", .few) self._Call_ShortSeconds_many = getValueWithForm(dict, "Call.ShortSeconds", .many) self._Call_ShortSeconds_other = getValueWithForm(dict, "Call.ShortSeconds", .other) + self._Conversation_StatusSubscribers_zero = getValueWithForm(dict, "Conversation.StatusSubscribers", .zero) + self._Conversation_StatusSubscribers_one = getValueWithForm(dict, "Conversation.StatusSubscribers", .one) + self._Conversation_StatusSubscribers_two = getValueWithForm(dict, "Conversation.StatusSubscribers", .two) + self._Conversation_StatusSubscribers_few = getValueWithForm(dict, "Conversation.StatusSubscribers", .few) + self._Conversation_StatusSubscribers_many = getValueWithForm(dict, "Conversation.StatusSubscribers", .many) + self._Conversation_StatusSubscribers_other = getValueWithForm(dict, "Conversation.StatusSubscribers", .other) self._SharedMedia_File_zero = getValueWithForm(dict, "SharedMedia.File", .zero) self._SharedMedia_File_one = getValueWithForm(dict, "SharedMedia.File", .one) self._SharedMedia_File_two = getValueWithForm(dict, "SharedMedia.File", .two) self._SharedMedia_File_few = getValueWithForm(dict, "SharedMedia.File", .few) self._SharedMedia_File_many = getValueWithForm(dict, "SharedMedia.File", .many) self._SharedMedia_File_other = getValueWithForm(dict, "SharedMedia.File", .other) - self._PasscodeSettings_AutoLock_IfAwayFor_zero = getValueWithForm(dict, "PasscodeSettings.AutoLock.IfAwayFor", .zero) - self._PasscodeSettings_AutoLock_IfAwayFor_one = getValueWithForm(dict, "PasscodeSettings.AutoLock.IfAwayFor", .one) - self._PasscodeSettings_AutoLock_IfAwayFor_two = getValueWithForm(dict, "PasscodeSettings.AutoLock.IfAwayFor", .two) - self._PasscodeSettings_AutoLock_IfAwayFor_few = getValueWithForm(dict, "PasscodeSettings.AutoLock.IfAwayFor", .few) - self._PasscodeSettings_AutoLock_IfAwayFor_many = getValueWithForm(dict, "PasscodeSettings.AutoLock.IfAwayFor", .many) - self._PasscodeSettings_AutoLock_IfAwayFor_other = getValueWithForm(dict, "PasscodeSettings.AutoLock.IfAwayFor", .other) self._ForwardedAudios_zero = getValueWithForm(dict, "ForwardedAudios", .zero) self._ForwardedAudios_one = getValueWithForm(dict, "ForwardedAudios", .one) self._ForwardedAudios_two = getValueWithForm(dict, "ForwardedAudios", .two) @@ -7271,12 +6867,6 @@ public final class PresentationStrings { self._PrivacyLastSeenSettings_AddUsers_few = getValueWithForm(dict, "PrivacyLastSeenSettings.AddUsers", .few) self._PrivacyLastSeenSettings_AddUsers_many = getValueWithForm(dict, "PrivacyLastSeenSettings.AddUsers", .many) self._PrivacyLastSeenSettings_AddUsers_other = getValueWithForm(dict, "PrivacyLastSeenSettings.AddUsers", .other) - self._MuteFor_Weeks_zero = getValueWithForm(dict, "MuteFor.Weeks", .zero) - self._MuteFor_Weeks_one = getValueWithForm(dict, "MuteFor.Weeks", .one) - self._MuteFor_Weeks_two = getValueWithForm(dict, "MuteFor.Weeks", .two) - self._MuteFor_Weeks_few = getValueWithForm(dict, "MuteFor.Weeks", .few) - self._MuteFor_Weeks_many = getValueWithForm(dict, "MuteFor.Weeks", .many) - self._MuteFor_Weeks_other = getValueWithForm(dict, "MuteFor.Weeks", .other) self._ForwardedVideoMessages_zero = getValueWithForm(dict, "ForwardedVideoMessages", .zero) self._ForwardedVideoMessages_one = getValueWithForm(dict, "ForwardedVideoMessages", .one) self._ForwardedVideoMessages_two = getValueWithForm(dict, "ForwardedVideoMessages", .two) @@ -7301,6 +6891,24 @@ public final class PresentationStrings { self._Conversation_StatusMembers_few = getValueWithForm(dict, "Conversation.StatusMembers", .few) self._Conversation_StatusMembers_many = getValueWithForm(dict, "Conversation.StatusMembers", .many) self._Conversation_StatusMembers_other = getValueWithForm(dict, "Conversation.StatusMembers", .other) + self._Conversation_LiveLocationMembersCount_zero = getValueWithForm(dict, "Conversation.LiveLocationMembersCount", .zero) + self._Conversation_LiveLocationMembersCount_one = getValueWithForm(dict, "Conversation.LiveLocationMembersCount", .one) + self._Conversation_LiveLocationMembersCount_two = getValueWithForm(dict, "Conversation.LiveLocationMembersCount", .two) + self._Conversation_LiveLocationMembersCount_few = getValueWithForm(dict, "Conversation.LiveLocationMembersCount", .few) + self._Conversation_LiveLocationMembersCount_many = getValueWithForm(dict, "Conversation.LiveLocationMembersCount", .many) + self._Conversation_LiveLocationMembersCount_other = getValueWithForm(dict, "Conversation.LiveLocationMembersCount", .other) + self._Media_SharePhoto_zero = getValueWithForm(dict, "Media.SharePhoto", .zero) + self._Media_SharePhoto_one = getValueWithForm(dict, "Media.SharePhoto", .one) + self._Media_SharePhoto_two = getValueWithForm(dict, "Media.SharePhoto", .two) + self._Media_SharePhoto_few = getValueWithForm(dict, "Media.SharePhoto", .few) + self._Media_SharePhoto_many = getValueWithForm(dict, "Media.SharePhoto", .many) + self._Media_SharePhoto_other = getValueWithForm(dict, "Media.SharePhoto", .other) + self._LiveLocation_MenuChatsCount_zero = getValueWithForm(dict, "LiveLocation.MenuChatsCount", .zero) + self._LiveLocation_MenuChatsCount_one = getValueWithForm(dict, "LiveLocation.MenuChatsCount", .one) + self._LiveLocation_MenuChatsCount_two = getValueWithForm(dict, "LiveLocation.MenuChatsCount", .two) + self._LiveLocation_MenuChatsCount_few = getValueWithForm(dict, "LiveLocation.MenuChatsCount", .few) + self._LiveLocation_MenuChatsCount_many = getValueWithForm(dict, "LiveLocation.MenuChatsCount", .many) + self._LiveLocation_MenuChatsCount_other = getValueWithForm(dict, "LiveLocation.MenuChatsCount", .other) self._Invitation_Members_zero = getValueWithForm(dict, "Invitation.Members", .zero) self._Invitation_Members_one = getValueWithForm(dict, "Invitation.Members", .one) self._Invitation_Members_two = getValueWithForm(dict, "Invitation.Members", .two) @@ -7337,12 +6945,6 @@ public final class PresentationStrings { self._SharedMedia_Video_few = getValueWithForm(dict, "SharedMedia.Video", .few) self._SharedMedia_Video_many = getValueWithForm(dict, "SharedMedia.Video", .many) self._SharedMedia_Video_other = getValueWithForm(dict, "SharedMedia.Video", .other) - self._MuteFor_Minutes_zero = getValueWithForm(dict, "MuteFor.Minutes", .zero) - self._MuteFor_Minutes_one = getValueWithForm(dict, "MuteFor.Minutes", .one) - self._MuteFor_Minutes_two = getValueWithForm(dict, "MuteFor.Minutes", .two) - self._MuteFor_Minutes_few = getValueWithForm(dict, "MuteFor.Minutes", .few) - self._MuteFor_Minutes_many = getValueWithForm(dict, "MuteFor.Minutes", .many) - self._MuteFor_Minutes_other = getValueWithForm(dict, "MuteFor.Minutes", .other) self._AttachmentMenu_SendVideo_zero = getValueWithForm(dict, "AttachmentMenu.SendVideo", .zero) self._AttachmentMenu_SendVideo_one = getValueWithForm(dict, "AttachmentMenu.SendVideo", .one) self._AttachmentMenu_SendVideo_two = getValueWithForm(dict, "AttachmentMenu.SendVideo", .two) @@ -7361,12 +6963,6 @@ public final class PresentationStrings { self._ForwardedContacts_few = getValueWithForm(dict, "ForwardedContacts", .few) self._ForwardedContacts_many = getValueWithForm(dict, "ForwardedContacts", .many) self._ForwardedContacts_other = getValueWithForm(dict, "ForwardedContacts", .other) - self._Channel_NotificationComments_zero = getValueWithForm(dict, "Channel.NotificationComments", .zero) - self._Channel_NotificationComments_one = getValueWithForm(dict, "Channel.NotificationComments", .one) - self._Channel_NotificationComments_two = getValueWithForm(dict, "Channel.NotificationComments", .two) - self._Channel_NotificationComments_few = getValueWithForm(dict, "Channel.NotificationComments", .few) - self._Channel_NotificationComments_many = getValueWithForm(dict, "Channel.NotificationComments", .many) - self._Channel_NotificationComments_other = getValueWithForm(dict, "Channel.NotificationComments", .other) self._ForwardedGifs_zero = getValueWithForm(dict, "ForwardedGifs", .zero) self._ForwardedGifs_one = getValueWithForm(dict, "ForwardedGifs", .one) self._ForwardedGifs_two = getValueWithForm(dict, "ForwardedGifs", .two) @@ -7409,12 +7005,6 @@ public final class PresentationStrings { self._LastSeen_MinutesAgo_few = getValueWithForm(dict, "LastSeen.MinutesAgo", .few) self._LastSeen_MinutesAgo_many = getValueWithForm(dict, "LastSeen.MinutesAgo", .many) self._LastSeen_MinutesAgo_other = getValueWithForm(dict, "LastSeen.MinutesAgo", .other) - self._Conversation_StatusRecipients_zero = getValueWithForm(dict, "Conversation.StatusRecipients", .zero) - self._Conversation_StatusRecipients_one = getValueWithForm(dict, "Conversation.StatusRecipients", .one) - self._Conversation_StatusRecipients_two = getValueWithForm(dict, "Conversation.StatusRecipients", .two) - self._Conversation_StatusRecipients_few = getValueWithForm(dict, "Conversation.StatusRecipients", .few) - self._Conversation_StatusRecipients_many = getValueWithForm(dict, "Conversation.StatusRecipients", .many) - self._Conversation_StatusRecipients_other = getValueWithForm(dict, "Conversation.StatusRecipients", .other) self._ServiceMessage_GameScoreSelfSimple_zero = getValueWithForm(dict, "ServiceMessage.GameScoreSelfSimple", .zero) self._ServiceMessage_GameScoreSelfSimple_one = getValueWithForm(dict, "ServiceMessage.GameScoreSelfSimple", .one) self._ServiceMessage_GameScoreSelfSimple_two = getValueWithForm(dict, "ServiceMessage.GameScoreSelfSimple", .two) @@ -7439,24 +7029,24 @@ public final class PresentationStrings { self._StickerPack_AddMaskCount_few = getValueWithForm(dict, "StickerPack.AddMaskCount", .few) self._StickerPack_AddMaskCount_many = getValueWithForm(dict, "StickerPack.AddMaskCount", .many) self._StickerPack_AddMaskCount_other = getValueWithForm(dict, "StickerPack.AddMaskCount", .other) - self._Channel_Management_LabelRights_zero = getValueWithForm(dict, "Channel.Management.LabelRights", .zero) - self._Channel_Management_LabelRights_one = getValueWithForm(dict, "Channel.Management.LabelRights", .one) - self._Channel_Management_LabelRights_two = getValueWithForm(dict, "Channel.Management.LabelRights", .two) - self._Channel_Management_LabelRights_few = getValueWithForm(dict, "Channel.Management.LabelRights", .few) - self._Channel_Management_LabelRights_many = getValueWithForm(dict, "Channel.Management.LabelRights", .many) - self._Channel_Management_LabelRights_other = getValueWithForm(dict, "Channel.Management.LabelRights", .other) - self._LastSeen_HoursAgo_zero = getValueWithForm(dict, "LastSeen.HoursAgo", .zero) - self._LastSeen_HoursAgo_one = getValueWithForm(dict, "LastSeen.HoursAgo", .one) - self._LastSeen_HoursAgo_two = getValueWithForm(dict, "LastSeen.HoursAgo", .two) - self._LastSeen_HoursAgo_few = getValueWithForm(dict, "LastSeen.HoursAgo", .few) - self._LastSeen_HoursAgo_many = getValueWithForm(dict, "LastSeen.HoursAgo", .many) - self._LastSeen_HoursAgo_other = getValueWithForm(dict, "LastSeen.HoursAgo", .other) self._MuteExpires_Days_zero = getValueWithForm(dict, "MuteExpires.Days", .zero) self._MuteExpires_Days_one = getValueWithForm(dict, "MuteExpires.Days", .one) self._MuteExpires_Days_two = getValueWithForm(dict, "MuteExpires.Days", .two) self._MuteExpires_Days_few = getValueWithForm(dict, "MuteExpires.Days", .few) self._MuteExpires_Days_many = getValueWithForm(dict, "MuteExpires.Days", .many) self._MuteExpires_Days_other = getValueWithForm(dict, "MuteExpires.Days", .other) + self._LastSeen_HoursAgo_zero = getValueWithForm(dict, "LastSeen.HoursAgo", .zero) + self._LastSeen_HoursAgo_one = getValueWithForm(dict, "LastSeen.HoursAgo", .one) + self._LastSeen_HoursAgo_two = getValueWithForm(dict, "LastSeen.HoursAgo", .two) + self._LastSeen_HoursAgo_few = getValueWithForm(dict, "LastSeen.HoursAgo", .few) + self._LastSeen_HoursAgo_many = getValueWithForm(dict, "LastSeen.HoursAgo", .many) + self._LastSeen_HoursAgo_other = getValueWithForm(dict, "LastSeen.HoursAgo", .other) + self._MessageTimer_Hours_zero = getValueWithForm(dict, "MessageTimer.Hours", .zero) + self._MessageTimer_Hours_one = getValueWithForm(dict, "MessageTimer.Hours", .one) + self._MessageTimer_Hours_two = getValueWithForm(dict, "MessageTimer.Hours", .two) + self._MessageTimer_Hours_few = getValueWithForm(dict, "MessageTimer.Hours", .few) + self._MessageTimer_Hours_many = getValueWithForm(dict, "MessageTimer.Hours", .many) + self._MessageTimer_Hours_other = getValueWithForm(dict, "MessageTimer.Hours", .other) self._MuteExpires_Hours_zero = getValueWithForm(dict, "MuteExpires.Hours", .zero) self._MuteExpires_Hours_one = getValueWithForm(dict, "MuteExpires.Hours", .one) self._MuteExpires_Hours_two = getValueWithForm(dict, "MuteExpires.Hours", .two) @@ -7493,12 +7083,12 @@ public final class PresentationStrings { self._SharedMedia_Link_few = getValueWithForm(dict, "SharedMedia.Link", .few) self._SharedMedia_Link_many = getValueWithForm(dict, "SharedMedia.Link", .many) self._SharedMedia_Link_other = getValueWithForm(dict, "SharedMedia.Link", .other) - self._Map_ETAHours_zero = getValueWithForm(dict, "Map.ETAHours", .zero) - self._Map_ETAHours_one = getValueWithForm(dict, "Map.ETAHours", .one) - self._Map_ETAHours_two = getValueWithForm(dict, "Map.ETAHours", .two) - self._Map_ETAHours_few = getValueWithForm(dict, "Map.ETAHours", .few) - self._Map_ETAHours_many = getValueWithForm(dict, "Map.ETAHours", .many) - self._Map_ETAHours_other = getValueWithForm(dict, "Map.ETAHours", .other) + self._DialogList_LiveLocationChatsCount_zero = getValueWithForm(dict, "DialogList.LiveLocationChatsCount", .zero) + self._DialogList_LiveLocationChatsCount_one = getValueWithForm(dict, "DialogList.LiveLocationChatsCount", .one) + self._DialogList_LiveLocationChatsCount_two = getValueWithForm(dict, "DialogList.LiveLocationChatsCount", .two) + self._DialogList_LiveLocationChatsCount_few = getValueWithForm(dict, "DialogList.LiveLocationChatsCount", .few) + self._DialogList_LiveLocationChatsCount_many = getValueWithForm(dict, "DialogList.LiveLocationChatsCount", .many) + self._DialogList_LiveLocationChatsCount_other = getValueWithForm(dict, "DialogList.LiveLocationChatsCount", .other) self._SharedMedia_DeleteItemsConfirmation_zero = getValueWithForm(dict, "SharedMedia.DeleteItemsConfirmation", .zero) self._SharedMedia_DeleteItemsConfirmation_one = getValueWithForm(dict, "SharedMedia.DeleteItemsConfirmation", .one) self._SharedMedia_DeleteItemsConfirmation_two = getValueWithForm(dict, "SharedMedia.DeleteItemsConfirmation", .two) @@ -7517,12 +7107,12 @@ public final class PresentationStrings { self._ForwardedMessages_few = getValueWithForm(dict, "ForwardedMessages", .few) self._ForwardedMessages_many = getValueWithForm(dict, "ForwardedMessages", .many) self._ForwardedMessages_other = getValueWithForm(dict, "ForwardedMessages", .other) - self._SharedMedia_ItemsSelected_zero = getValueWithForm(dict, "SharedMedia.ItemsSelected", .zero) - self._SharedMedia_ItemsSelected_one = getValueWithForm(dict, "SharedMedia.ItemsSelected", .one) - self._SharedMedia_ItemsSelected_two = getValueWithForm(dict, "SharedMedia.ItemsSelected", .two) - self._SharedMedia_ItemsSelected_few = getValueWithForm(dict, "SharedMedia.ItemsSelected", .few) - self._SharedMedia_ItemsSelected_many = getValueWithForm(dict, "SharedMedia.ItemsSelected", .many) - self._SharedMedia_ItemsSelected_other = getValueWithForm(dict, "SharedMedia.ItemsSelected", .other) + self._Map_ETAHours_zero = getValueWithForm(dict, "Map.ETAHours", .zero) + self._Map_ETAHours_one = getValueWithForm(dict, "Map.ETAHours", .one) + self._Map_ETAHours_two = getValueWithForm(dict, "Map.ETAHours", .two) + self._Map_ETAHours_few = getValueWithForm(dict, "Map.ETAHours", .few) + self._Map_ETAHours_many = getValueWithForm(dict, "Map.ETAHours", .many) + self._Map_ETAHours_other = getValueWithForm(dict, "Map.ETAHours", .other) self._Watch_LastSeen_MinutesAgo_zero = getValueWithForm(dict, "Watch.LastSeen.MinutesAgo", .zero) self._Watch_LastSeen_MinutesAgo_one = getValueWithForm(dict, "Watch.LastSeen.MinutesAgo", .one) self._Watch_LastSeen_MinutesAgo_two = getValueWithForm(dict, "Watch.LastSeen.MinutesAgo", .two) @@ -7541,12 +7131,6 @@ public final class PresentationStrings { self._Map_ETAMinutes_few = getValueWithForm(dict, "Map.ETAMinutes", .few) self._Map_ETAMinutes_many = getValueWithForm(dict, "Map.ETAMinutes", .many) self._Map_ETAMinutes_other = getValueWithForm(dict, "Map.ETAMinutes", .other) - self._MessageTimer_Hours_zero = getValueWithForm(dict, "MessageTimer.Hours", .zero) - self._MessageTimer_Hours_one = getValueWithForm(dict, "MessageTimer.Hours", .one) - self._MessageTimer_Hours_two = getValueWithForm(dict, "MessageTimer.Hours", .two) - self._MessageTimer_Hours_few = getValueWithForm(dict, "MessageTimer.Hours", .few) - self._MessageTimer_Hours_many = getValueWithForm(dict, "MessageTimer.Hours", .many) - self._MessageTimer_Hours_other = getValueWithForm(dict, "MessageTimer.Hours", .other) self._Notification_GameScoreSelfSimple_zero = getValueWithForm(dict, "Notification.GameScoreSelfSimple", .zero) self._Notification_GameScoreSelfSimple_one = getValueWithForm(dict, "Notification.GameScoreSelfSimple", .one) self._Notification_GameScoreSelfSimple_two = getValueWithForm(dict, "Notification.GameScoreSelfSimple", .two) @@ -7595,12 +7179,12 @@ public final class PresentationStrings { self._Watch_UserInfo_Mute_few = getValueWithForm(dict, "Watch.UserInfo.Mute", .few) self._Watch_UserInfo_Mute_many = getValueWithForm(dict, "Watch.UserInfo.Mute", .many) self._Watch_UserInfo_Mute_other = getValueWithForm(dict, "Watch.UserInfo.Mute", .other) - self._StickerPack_MaskCount_zero = getValueWithForm(dict, "StickerPack.MaskCount", .zero) - self._StickerPack_MaskCount_one = getValueWithForm(dict, "StickerPack.MaskCount", .one) - self._StickerPack_MaskCount_two = getValueWithForm(dict, "StickerPack.MaskCount", .two) - self._StickerPack_MaskCount_few = getValueWithForm(dict, "StickerPack.MaskCount", .few) - self._StickerPack_MaskCount_many = getValueWithForm(dict, "StickerPack.MaskCount", .many) - self._StickerPack_MaskCount_other = getValueWithForm(dict, "StickerPack.MaskCount", .other) + self._LiveLocationUpdated_MinutesAgo_zero = getValueWithForm(dict, "LiveLocationUpdated.MinutesAgo", .zero) + self._LiveLocationUpdated_MinutesAgo_one = getValueWithForm(dict, "LiveLocationUpdated.MinutesAgo", .one) + self._LiveLocationUpdated_MinutesAgo_two = getValueWithForm(dict, "LiveLocationUpdated.MinutesAgo", .two) + self._LiveLocationUpdated_MinutesAgo_few = getValueWithForm(dict, "LiveLocationUpdated.MinutesAgo", .few) + self._LiveLocationUpdated_MinutesAgo_many = getValueWithForm(dict, "LiveLocationUpdated.MinutesAgo", .many) + self._LiveLocationUpdated_MinutesAgo_other = getValueWithForm(dict, "LiveLocationUpdated.MinutesAgo", .other) self._Call_ShortMinutes_zero = getValueWithForm(dict, "Call.ShortMinutes", .zero) self._Call_ShortMinutes_one = getValueWithForm(dict, "Call.ShortMinutes", .one) self._Call_ShortMinutes_two = getValueWithForm(dict, "Call.ShortMinutes", .two) @@ -7613,6 +7197,12 @@ public final class PresentationStrings { self._StickerPack_RemoveMaskCount_few = getValueWithForm(dict, "StickerPack.RemoveMaskCount", .few) self._StickerPack_RemoveMaskCount_many = getValueWithForm(dict, "StickerPack.RemoveMaskCount", .many) self._StickerPack_RemoveMaskCount_other = getValueWithForm(dict, "StickerPack.RemoveMaskCount", .other) + self._Media_ShareItem_zero = getValueWithForm(dict, "Media.ShareItem", .zero) + self._Media_ShareItem_one = getValueWithForm(dict, "Media.ShareItem", .one) + self._Media_ShareItem_two = getValueWithForm(dict, "Media.ShareItem", .two) + self._Media_ShareItem_few = getValueWithForm(dict, "Media.ShareItem", .few) + self._Media_ShareItem_many = getValueWithForm(dict, "Media.ShareItem", .many) + self._Media_ShareItem_other = getValueWithForm(dict, "Media.ShareItem", .other) self._ForwardedLocations_zero = getValueWithForm(dict, "ForwardedLocations", .zero) self._ForwardedLocations_one = getValueWithForm(dict, "ForwardedLocations", .one) self._ForwardedLocations_two = getValueWithForm(dict, "ForwardedLocations", .two) diff --git a/TelegramUI/PresentationTheme.swift b/TelegramUI/PresentationTheme.swift index 26bfe4889d..122a442b3d 100644 --- a/TelegramUI/PresentationTheme.swift +++ b/TelegramUI/PresentationTheme.swift @@ -1,20 +1,11 @@ import Foundation import UIKit import Display -import Postbox public enum PresentationThemeParsingError: Error { case generic } -private func parseColor(_ decoder: PostboxDecoder, _ key: String) throws -> UIColor { - if let value = decoder.decodeOptionalInt32ForKey(key) { - return UIColor(argb: UInt32(bitPattern: value)) - } else { - throw PresentationThemeParsingError.generic - } -} - public final class PresentationThemeRootTabBar { public let backgroundColor: UIColor public let separatorColor: UIColor @@ -23,9 +14,10 @@ public final class PresentationThemeRootTabBar { public let textColor: UIColor public let selectedTextColor: UIColor public let badgeBackgroundColor: UIColor + public let badgeStrokeColor: UIColor public let badgeTextColor: UIColor - public init(backgroundColor: UIColor, separatorColor: UIColor, iconColor: UIColor, selectedIconColor: UIColor, textColor: UIColor, selectedTextColor: UIColor, badgeBackgroundColor: UIColor, badgeTextColor: UIColor) { + public init(backgroundColor: UIColor, separatorColor: UIColor, iconColor: UIColor, selectedIconColor: UIColor, textColor: UIColor, selectedTextColor: UIColor, badgeBackgroundColor: UIColor, badgeStrokeColor: UIColor, badgeTextColor: UIColor) { self.backgroundColor = backgroundColor self.separatorColor = separatorColor self.iconColor = iconColor @@ -33,31 +25,9 @@ public final class PresentationThemeRootTabBar { self.textColor = textColor self.selectedTextColor = selectedTextColor self.badgeBackgroundColor = badgeBackgroundColor + self.badgeStrokeColor = badgeStrokeColor self.badgeTextColor = badgeTextColor } - - public init(decoder: PostboxDecoder) throws { - self.backgroundColor = try parseColor(decoder, "backgroundColor") - self.separatorColor = try parseColor(decoder, "separatorColor") - self.iconColor = try parseColor(decoder, "iconColor") - self.selectedIconColor = try parseColor(decoder, "selectedIconColor") - self.textColor = try parseColor(decoder, "textColor") - self.selectedTextColor = try parseColor(decoder, "selectedTextColor") - self.badgeBackgroundColor = try parseColor(decoder, "badgeBackgroundColor") - self.badgeTextColor = try parseColor(decoder, "badgeTextColor") - } - - public func encode(_ encoder: PostboxEncoder) { - for child in Mirror(reflecting: self).children { - if let label = child.label { - if let value = child.value as? UIColor { - encoder.encodeInt32(Int32(bitPattern: value.argb), forKey: label) - } else { - assertionFailure() - } - } - } - } } public enum PresentationThemeStatusBarStyle: Int32 { @@ -80,18 +50,6 @@ public final class PresentationThemeRootNavigationStatusBar { public init(style: PresentationThemeStatusBarStyle) { self.style = style } - - public init(decoder: PostboxDecoder) throws { - if let styleValue = decoder.decodeOptionalInt32ForKey("style"), let style = PresentationThemeStatusBarStyle(rawValue: styleValue) { - self.style = style - } else { - throw PresentationThemeParsingError.generic - } - } - - public func encode(_ encoder: PostboxEncoder) { - encoder.encodeInt32(self.style.rawValue, forKey: "style") - } } public final class PresentationThemeRootNavigationBar { @@ -103,9 +61,10 @@ public final class PresentationThemeRootNavigationBar { public let backgroundColor: UIColor public let separatorColor: UIColor public let badgeBackgroundColor: UIColor + public let badgeStrokeColor: UIColor public let badgeTextColor: UIColor - public init(buttonColor: UIColor, primaryTextColor: UIColor, secondaryTextColor: UIColor, controlColor: UIColor, accentTextColor: UIColor, backgroundColor: UIColor, separatorColor: UIColor, badgeBackgroundColor: UIColor, badgeTextColor: UIColor) { + public init(buttonColor: UIColor, primaryTextColor: UIColor, secondaryTextColor: UIColor, controlColor: UIColor, accentTextColor: UIColor, backgroundColor: UIColor, separatorColor: UIColor, badgeBackgroundColor: UIColor, badgeStrokeColor: UIColor, badgeTextColor: UIColor) { self.buttonColor = buttonColor self.primaryTextColor = primaryTextColor self.secondaryTextColor = secondaryTextColor @@ -114,31 +73,22 @@ public final class PresentationThemeRootNavigationBar { self.backgroundColor = backgroundColor self.separatorColor = separatorColor self.badgeBackgroundColor = badgeBackgroundColor + self.badgeStrokeColor = badgeStrokeColor self.badgeTextColor = badgeTextColor } +} + +public final class PresentationThemeExpandedNotificationNavigationBar { + public let backgroundColor: UIColor + public let primaryTextColor: UIColor + public let controlColor: UIColor + public let separatorColor: UIColor - public init(decoder: PostboxDecoder) throws { - self.buttonColor = try parseColor(decoder, "buttonColor") - self.primaryTextColor = try parseColor(decoder, "primaryTextColor") - self.secondaryTextColor = try parseColor(decoder, "secondaryTextColor") - self.controlColor = try parseColor(decoder, "controlColor") - self.accentTextColor = try parseColor(decoder, "accentTextColor") - self.backgroundColor = try parseColor(decoder, "backgroundColor") - self.separatorColor = try parseColor(decoder, "separatorColor") - self.badgeBackgroundColor = try parseColor(decoder, "badgeBackgroundColor") - self.badgeTextColor = try parseColor(decoder, "badgeTextColor") - } - - public func encode(_ encoder: PostboxEncoder) { - for child in Mirror(reflecting: self).children { - if let label = child.label { - if let value = child.value as? UIColor { - encoder.encodeInt32(Int32(bitPattern: value.argb), forKey: label) - } else { - assertionFailure() - } - } - } + init(backgroundColor: UIColor, primaryTextColor: UIColor, controlColor: UIColor, separatorColor: UIColor) { + self.backgroundColor = backgroundColor + self.primaryTextColor = primaryTextColor + self.controlColor = controlColor + self.separatorColor = separatorColor } } @@ -162,29 +112,6 @@ public final class PresentationThemeActiveNavigationSearchBar { self.inputClearButtonColor = inputClearButtonColor self.separatorColor = separatorColor } - - public init(decoder: PostboxDecoder) throws { - self.backgroundColor = try parseColor(decoder, "backgroundColor") - self.accentColor = try parseColor(decoder, "accentColor") - self.inputFillColor = try parseColor(decoder, "inputFillColor") - self.inputTextColor = try parseColor(decoder, "inputTextColor") - self.inputPlaceholderTextColor = try parseColor(decoder, "inputPlaceholderTextColor") - self.inputIconColor = try parseColor(decoder, "inputIconColor") - self.inputClearButtonColor = try parseColor(decoder, "inputClearButtonColor") - self.separatorColor = try parseColor(decoder, "separatorColor") - } - - public func encode(_ encoder: PostboxEncoder) { - for child in Mirror(reflecting: self).children { - if let label = child.label { - if let value = child.value as? UIColor { - encoder.encodeInt32(Int32(bitPattern: value.argb), forKey: label) - } else { - assertionFailure() - } - } - } - } } public final class PresentationThemeRootController { @@ -199,35 +126,20 @@ public final class PresentationThemeRootController { self.navigationBar = navigationBar self.activeNavigationSearchBar = activeNavigationSearchBar } +} + +public enum PresentationThemeExpandedNotificationBackgroundType: Int32 { + case light + case dark +} + +public final class PresentationThemeExpandedNotification { + public let backgroundType: PresentationThemeExpandedNotificationBackgroundType + public let navigationBar: PresentationThemeExpandedNotificationNavigationBar - public init(decoder: PostboxDecoder) throws { - if let statusBar = (try? decoder.decodeObjectForKeyThrowing("statusBar", decoder: { try PresentationThemeRootNavigationStatusBar(decoder: $0) })) as? PresentationThemeRootNavigationStatusBar { - self.statusBar = statusBar - } else { - throw PresentationThemeParsingError.generic - } - if let tabBar = (try? decoder.decodeObjectForKeyThrowing("tabBar", decoder: { try PresentationThemeRootTabBar(decoder: $0) })) as? PresentationThemeRootTabBar { - self.tabBar = tabBar - } else { - throw PresentationThemeParsingError.generic - } - if let navigationBar = (try? decoder.decodeObjectForKeyThrowing("navigationBar", decoder: { try PresentationThemeRootNavigationBar(decoder: $0) })) as? PresentationThemeRootNavigationBar { - self.navigationBar = navigationBar - } else { - throw PresentationThemeParsingError.generic - } - if let activeNavigationSearchBar = (try? decoder.decodeObjectForKeyThrowing("activeNavigationSearchBar", decoder: { try PresentationThemeActiveNavigationSearchBar(decoder: $0) })) as? PresentationThemeActiveNavigationSearchBar { - self.activeNavigationSearchBar = activeNavigationSearchBar - } else { - throw PresentationThemeParsingError.generic - } - } - - public func encode(_ encoder: PostboxEncoder) { - encoder.encodeObjectWithEncoder(self.statusBar, encoder: { self.statusBar.encode($0) }, forKey: "statusBar") - encoder.encodeObjectWithEncoder(self.tabBar, encoder: { self.tabBar.encode($0) }, forKey: "tabBar") - encoder.encodeObjectWithEncoder(self.navigationBar, encoder: { self.navigationBar.encode($0) }, forKey: "navigationBar") - encoder.encodeObjectWithEncoder(self.activeNavigationSearchBar, encoder: { self.activeNavigationSearchBar.encode($0) }, forKey: "activeNavigationSearchBar") + public init(backgroundType: PresentationThemeExpandedNotificationBackgroundType, navigationBar: PresentationThemeExpandedNotificationNavigationBar) { + self.backgroundType = backgroundType + self.navigationBar = navigationBar } } @@ -239,53 +151,40 @@ public enum PresentationThemeActionSheetBackgroundType: Int32 { public final class PresentationThemeActionSheet { public let dimColor: UIColor public let backgroundType: PresentationThemeActionSheetBackgroundType + public let opaqueItemBackgroundColor: UIColor public let itemBackgroundColor: UIColor + public let opaqueItemHighlightedBackgroundColor: UIColor public let itemHighlightedBackgroundColor: UIColor + public let opaqueItemSeparatorColor: UIColor public let standardActionTextColor: UIColor public let destructiveActionTextColor: UIColor public let disabledActionTextColor: UIColor public let primaryTextColor: UIColor public let secondaryTextColor: UIColor public let controlAccentColor: UIColor + public let inputBackgroundColor: UIColor + public let inputPlaceholderColor: UIColor + public let inputTextColor: UIColor + public let inputClearButtonColor: UIColor - init(dimColor: UIColor, backgroundType: PresentationThemeActionSheetBackgroundType, itemBackgroundColor: UIColor, itemHighlightedBackgroundColor: UIColor, standardActionTextColor: UIColor, destructiveActionTextColor: UIColor, disabledActionTextColor: UIColor, primaryTextColor: UIColor, secondaryTextColor: UIColor, controlAccentColor: UIColor) { + init(dimColor: UIColor, backgroundType: PresentationThemeActionSheetBackgroundType, opaqueItemBackgroundColor: UIColor, itemBackgroundColor: UIColor, opaqueItemHighlightedBackgroundColor: UIColor, itemHighlightedBackgroundColor: UIColor, standardActionTextColor: UIColor, opaqueItemSeparatorColor: UIColor, destructiveActionTextColor: UIColor, disabledActionTextColor: UIColor, primaryTextColor: UIColor, secondaryTextColor: UIColor, controlAccentColor: UIColor, inputBackgroundColor: UIColor, inputPlaceholderColor: UIColor, inputTextColor: UIColor, inputClearButtonColor: UIColor) { self.dimColor = dimColor self.backgroundType = backgroundType + self.opaqueItemBackgroundColor = opaqueItemBackgroundColor self.itemBackgroundColor = itemBackgroundColor + self.opaqueItemHighlightedBackgroundColor = opaqueItemHighlightedBackgroundColor self.itemHighlightedBackgroundColor = itemHighlightedBackgroundColor + self.opaqueItemSeparatorColor = opaqueItemSeparatorColor self.standardActionTextColor = standardActionTextColor self.destructiveActionTextColor = destructiveActionTextColor self.disabledActionTextColor = disabledActionTextColor self.primaryTextColor = primaryTextColor self.secondaryTextColor = secondaryTextColor self.controlAccentColor = controlAccentColor - } - - public init(decoder: PostboxDecoder) throws { - self.dimColor = try parseColor(decoder, "dimColor") - self.backgroundType = PresentationThemeActionSheetBackgroundType(rawValue: decoder.decodeInt32ForKey("backgroundType", orElse: 0)) ?? .light - self.itemBackgroundColor = try parseColor(decoder, "itemBackgroundColor") - self.itemHighlightedBackgroundColor = try parseColor(decoder, "itemHighlightedBackgroundColor") - self.standardActionTextColor = try parseColor(decoder, "standardActionTextColor") - self.destructiveActionTextColor = try parseColor(decoder, "destructiveActionTextColor") - self.disabledActionTextColor = try parseColor(decoder, "disabledActionTextColor") - self.primaryTextColor = try parseColor(decoder, "primaryTextColor") - self.secondaryTextColor = try parseColor(decoder, "secondaryTextColor") - self.controlAccentColor = try parseColor(decoder, "controlAccentColor") - } - - public func encode(_ encoder: PostboxEncoder) { - for child in Mirror(reflecting: self).children { - if let label = child.label { - if let value = child.value as? UIColor { - encoder.encodeInt32(Int32(bitPattern: value.argb), forKey: label) - } else if let value = child.value as? PresentationThemeActionSheetBackgroundType { - encoder.encodeInt32(value.rawValue, forKey: label) - } else { - assertionFailure() - } - } - } + self.inputBackgroundColor = inputBackgroundColor + self.inputPlaceholderColor = inputPlaceholderColor + self.inputTextColor = inputTextColor + self.inputClearButtonColor = inputClearButtonColor } } @@ -299,23 +198,39 @@ public final class PresentationThemeSwitch { self.handleColor = handleColor self.contentColor = contentColor } +} + +public final class PresentationThemeItemDisclosureAction { + public let fillColor: UIColor + public let foregroundColor: UIColor - public init(decoder: PostboxDecoder) throws { - self.frameColor = try parseColor(decoder, "frameColor") - self.handleColor = try parseColor(decoder, "handleColor") - self.contentColor = try parseColor(decoder, "contentColor") + init(fillColor: UIColor, foregroundColor: UIColor) { + self.fillColor = fillColor + self.foregroundColor = foregroundColor } +} + +public final class PresentationThemeCheck { + public let strokeColor: UIColor + public let fillColor: UIColor + public let foregroundColor: UIColor - public func encode(_ encoder: PostboxEncoder) { - for child in Mirror(reflecting: self).children { - if let label = child.label { - if let value = child.value as? UIColor { - encoder.encodeInt32(Int32(bitPattern: value.argb), forKey: label) - } else { - assertionFailure() - } - } - } + init(strokeColor: UIColor, fillColor: UIColor, foregroundColor: UIColor) { + self.strokeColor = strokeColor + self.fillColor = fillColor + self.foregroundColor = foregroundColor + } +} + +public final class PresentationThemeItemDisclosureActions { + public let neutral1: PresentationThemeItemDisclosureAction + public let neutral2: PresentationThemeItemDisclosureAction + public let destructive: PresentationThemeItemDisclosureAction + + public init(neutral1: PresentationThemeItemDisclosureAction, neutral2: PresentationThemeItemDisclosureAction, destructive: PresentationThemeItemDisclosureAction) { + self.neutral1 = neutral1 + self.neutral2 = neutral2 + self.destructive = destructive } } @@ -328,17 +243,21 @@ public final class PresentationThemeList { public let itemAccentColor: UIColor public let itemDestructiveColor: UIColor public let itemPlaceholderTextColor: UIColor - public let itemBackgroundColor: UIColor + public let itemBlocksBackgroundColor: UIColor public let itemHighlightedBackgroundColor: UIColor - public let itemSeparatorColor: UIColor + public let itemBlocksSeparatorColor: UIColor + public let itemPlainSeparatorColor: UIColor public let disclosureArrowColor: UIColor public let sectionHeaderTextColor: UIColor public let freeTextColor: UIColor public let freeTextErrorColor: UIColor public let freeTextSuccessColor: UIColor public let itemSwitchColors: PresentationThemeSwitch + public let itemDisclosureActions: PresentationThemeItemDisclosureActions + public let itemCheckColors: PresentationThemeCheck + public let controlSecondaryColor: UIColor - public init(blocksBackgroundColor: UIColor, plainBackgroundColor: UIColor, itemPrimaryTextColor: UIColor, itemSecondaryTextColor: UIColor, itemDisabledTextColor: UIColor, itemAccentColor: UIColor, itemDestructiveColor: UIColor, itemPlaceholderTextColor: UIColor, itemBackgroundColor: UIColor, itemHighlightedBackgroundColor: UIColor, itemSeparatorColor: UIColor, disclosureArrowColor: UIColor, sectionHeaderTextColor: UIColor, freeTextColor: UIColor, freeTextErrorColor: UIColor, freeTextSuccessColor: UIColor, itemSwitchColors: PresentationThemeSwitch) { + public init(blocksBackgroundColor: UIColor, plainBackgroundColor: UIColor, itemPrimaryTextColor: UIColor, itemSecondaryTextColor: UIColor, itemDisabledTextColor: UIColor, itemAccentColor: UIColor, itemDestructiveColor: UIColor, itemPlaceholderTextColor: UIColor, itemBlocksBackgroundColor: UIColor, itemHighlightedBackgroundColor: UIColor, itemBlocksSeparatorColor: UIColor, itemPlainSeparatorColor: UIColor, disclosureArrowColor: UIColor, sectionHeaderTextColor: UIColor, freeTextColor: UIColor, freeTextErrorColor: UIColor, freeTextSuccessColor: UIColor, itemSwitchColors: PresentationThemeSwitch, itemDisclosureActions: PresentationThemeItemDisclosureActions, itemCheckColors: PresentationThemeCheck, controlSecondaryColor: UIColor) { self.blocksBackgroundColor = blocksBackgroundColor self.plainBackgroundColor = plainBackgroundColor self.itemPrimaryTextColor = itemPrimaryTextColor @@ -347,53 +266,19 @@ public final class PresentationThemeList { self.itemAccentColor = itemAccentColor self.itemDestructiveColor = itemDestructiveColor self.itemPlaceholderTextColor = itemPlaceholderTextColor - self.itemBackgroundColor = itemBackgroundColor + self.itemBlocksBackgroundColor = itemBlocksBackgroundColor self.itemHighlightedBackgroundColor = itemHighlightedBackgroundColor - self.itemSeparatorColor = itemSeparatorColor + self.itemBlocksSeparatorColor = itemBlocksSeparatorColor + self.itemPlainSeparatorColor = itemPlainSeparatorColor self.disclosureArrowColor = disclosureArrowColor self.sectionHeaderTextColor = sectionHeaderTextColor self.freeTextColor = freeTextColor self.freeTextErrorColor = freeTextErrorColor self.freeTextSuccessColor = freeTextSuccessColor self.itemSwitchColors = itemSwitchColors - } - - public init(decoder: PostboxDecoder) throws { - self.blocksBackgroundColor = try parseColor(decoder, "blocksBackgroundColor") - self.plainBackgroundColor = try parseColor(decoder, "plainBackgroundColor") - self.itemPrimaryTextColor = try parseColor(decoder, "itemPrimaryTextColor") - self.itemSecondaryTextColor = try parseColor(decoder, "itemSecondaryTextColor") - self.itemDisabledTextColor = try parseColor(decoder, "itemDisabledTextColor") - self.itemAccentColor = try parseColor(decoder, "itemAccentColor") - self.itemDestructiveColor = try parseColor(decoder, "itemDestructiveColor") - self.itemPlaceholderTextColor = try parseColor(decoder, "itemPlaceholderTextColor") - self.itemBackgroundColor = try parseColor(decoder, "itemBackgroundColor") - self.itemHighlightedBackgroundColor = try parseColor(decoder, "itemHighlightedBackgroundColor") - self.itemSeparatorColor = try parseColor(decoder, "itemSeparatorColor") - self.disclosureArrowColor = try parseColor(decoder, "disclosureArrowColor") - self.sectionHeaderTextColor = try parseColor(decoder, "sectionHeaderTextColor") - self.freeTextColor = try parseColor(decoder, "freeTextColor") - self.freeTextErrorColor = try parseColor(decoder, "freeTextErrorColor") - self.freeTextSuccessColor = try parseColor(decoder, "freeTextSuccessColor") - if let itemSwitchColors = (try? decoder.decodeObjectForKeyThrowing("itemSwitchColors", decoder: { try PresentationThemeSwitch(decoder: $0) })) as? PresentationThemeSwitch { - self.itemSwitchColors = itemSwitchColors - } else { - throw PresentationThemeParsingError.generic - } - } - - public func encode(_ encoder: PostboxEncoder) { - for child in Mirror(reflecting: self).children { - if let label = child.label { - if let value = child.value as? UIColor { - encoder.encodeInt32(Int32(bitPattern: value.argb), forKey: label) - } else if let value = child.value as? PresentationThemeSwitch { - encoder.encodeObjectWithEncoder(value, encoder: { value.encode($0) }, forKey: label) - } else { - assertionFailure() - } - } - } + self.itemDisclosureActions = itemDisclosureActions + self.itemCheckColors = itemCheckColors + self.controlSecondaryColor = controlSecondaryColor } } @@ -422,8 +307,11 @@ public final class PresentationThemeChatList { public let sectionHeaderFillColor: UIColor public let sectionHeaderTextColor: UIColor public let searchBarKeyboardColor: PresentationThemeKeyboardColor + public let verifiedIconFillColor: UIColor + public let verifiedIconForegroundColor: UIColor + public let secretIconColor: UIColor - init(backgroundColor: UIColor, itemSeparatorColor: UIColor, itemBackgroundColor: UIColor, pinnedItemBackgroundColor: UIColor, itemHighlightedBackgroundColor: UIColor, titleColor: UIColor, secretTitleColor: UIColor, dateTextColor: UIColor, authorNameColor: UIColor, messageTextColor: UIColor, messageDraftTextColor: UIColor, checkmarkColor: UIColor, pendingIndicatorColor: UIColor, muteIconColor: UIColor, unreadBadgeActiveBackgroundColor: UIColor, unreadBadgeActiveTextColor: UIColor, unreadBadgeInactiveBackgroundColor: UIColor, unreadBadgeInactiveTextColor: UIColor, pinnedBadgeColor: UIColor, pinnedSearchBarColor: UIColor, regularSearchBarColor: UIColor, sectionHeaderFillColor: UIColor, sectionHeaderTextColor: UIColor, searchBarKeyboardColor: PresentationThemeKeyboardColor) { + init(backgroundColor: UIColor, itemSeparatorColor: UIColor, itemBackgroundColor: UIColor, pinnedItemBackgroundColor: UIColor, itemHighlightedBackgroundColor: UIColor, titleColor: UIColor, secretTitleColor: UIColor, dateTextColor: UIColor, authorNameColor: UIColor, messageTextColor: UIColor, messageDraftTextColor: UIColor, checkmarkColor: UIColor, pendingIndicatorColor: UIColor, muteIconColor: UIColor, unreadBadgeActiveBackgroundColor: UIColor, unreadBadgeActiveTextColor: UIColor, unreadBadgeInactiveBackgroundColor: UIColor, unreadBadgeInactiveTextColor: UIColor, pinnedBadgeColor: UIColor, pinnedSearchBarColor: UIColor, regularSearchBarColor: UIColor, sectionHeaderFillColor: UIColor, sectionHeaderTextColor: UIColor, searchBarKeyboardColor: PresentationThemeKeyboardColor, verifiedIconFillColor: UIColor, verifiedIconForegroundColor: UIColor, secretIconColor: UIColor) { self.backgroundColor = backgroundColor self.itemSeparatorColor = itemSeparatorColor self.itemBackgroundColor = itemBackgroundColor @@ -448,53 +336,9 @@ public final class PresentationThemeChatList { self.sectionHeaderFillColor = sectionHeaderFillColor self.sectionHeaderTextColor = sectionHeaderTextColor self.searchBarKeyboardColor = searchBarKeyboardColor - } - - init(decoder: PostboxDecoder) throws { - self.backgroundColor = try parseColor(decoder, "backgroundColor") - self.itemSeparatorColor = try parseColor(decoder, "itemSeparatorColor") - self.itemBackgroundColor = try parseColor(decoder, "itemBackgroundColor") - self.pinnedItemBackgroundColor = try parseColor(decoder, "pinnedItemBackgroundColor") - self.itemHighlightedBackgroundColor = try parseColor(decoder, "itemHighlightedBackgroundColor") - self.titleColor = try parseColor(decoder, "titleColor") - self.secretTitleColor = try parseColor(decoder, "secretTitleColor") - self.dateTextColor = try parseColor(decoder, "dateTextColor") - self.authorNameColor = try parseColor(decoder, "authorNameColor") - self.messageTextColor = try parseColor(decoder, "messageTextColor") - self.messageDraftTextColor = try parseColor(decoder, "messageDraftTextColor") - self.checkmarkColor = try parseColor(decoder, "checkmarkColor") - self.pendingIndicatorColor = try parseColor(decoder, "pendingIndicatorColor") - self.muteIconColor = try parseColor(decoder, "muteIconColor") - self.unreadBadgeActiveBackgroundColor = try parseColor(decoder, "unreadBadgeActiveBackgroundColor") - self.unreadBadgeActiveTextColor = try parseColor(decoder, "unreadBadgeActiveTextColor") - self.unreadBadgeInactiveBackgroundColor = try parseColor(decoder, "unreadBadgeInactiveBackgroundColor") - self.unreadBadgeInactiveTextColor = try parseColor(decoder, "unreadBadgeInactiveTextColor") - self.pinnedBadgeColor = try parseColor(decoder, "pinnedBadgeColor") - self.pinnedSearchBarColor = try parseColor(decoder, "pinnedSearchBarColor") - self.regularSearchBarColor = try parseColor(decoder, "regularSearchBarColor") - self.sectionHeaderFillColor = try parseColor(decoder, "sectionHeaderFillColor") - self.sectionHeaderTextColor = try parseColor(decoder, "sectionHeaderTextColor") - if let value = decoder.decodeOptionalInt32ForKey("searchBarKeyboardColor"), let color = PresentationThemeKeyboardColor(rawValue: value) { - self.searchBarKeyboardColor = color - } else { - throw PresentationThemeParsingError.generic - } - } - - public func encode(_ encoder: PostboxEncoder) { - for child in Mirror(reflecting: self).children { - if let label = child.label { - if let value = child.value as? UIColor { - encoder.encodeInt32(Int32(bitPattern: value.argb), forKey: label) - } else if let value = child.value as? PresentationThemeSwitch { - encoder.encodeObjectWithEncoder(value, encoder: { value.encode($0) }, forKey: label) - } else if let value = child.value as? PresentationThemeKeyboardColor { - encoder.encodeInt32(value.rawValue, forKey: label) - } else { - assertionFailure() - } - } - } + self.verifiedIconFillColor = verifiedIconFillColor + self.verifiedIconForegroundColor = verifiedIconForegroundColor + self.secretIconColor = secretIconColor } } @@ -525,8 +369,15 @@ public final class PresentationThemeChatBubble { public let infoPrimaryTextColor: UIColor public let infoLinkTextColor: UIColor - public let incomingAccentColor: UIColor - public let outgoingAccentColor: UIColor + public let incomingAccentTextColor: UIColor + public let outgoingAccentTextColor: UIColor + + public let incomingAccentControlColor: UIColor + public let outgoingAccentControlColor: UIColor + public let incomingMediaActiveControlColor: UIColor + public let outgoingMediaActiveControlColor: UIColor + public let incomingMediaInactiveControlColor: UIColor + public let outgoingMediaInactiveControlColor: UIColor public let outgoingCheckColor: UIColor public let incomingPendingActivityColor: UIColor @@ -543,15 +394,25 @@ public final class PresentationThemeChatBubble { public let outgoingFileDurationColor: UIColor public let shareButtonFillColor: UIColor + public let shareButtonStrokeColor: UIColor public let shareButtonForegroundColor: UIColor public let mediaOverlayControlBackgroundColor: UIColor public let mediaOverlayControlForegroundColor: UIColor - public let actionButtonsFillColor: UIColor - public let actionButtonsTextColor: UIColor + public let actionButtonsIncomingFillColor: UIColor + public let actionButtonsIncomingStrokeColor: UIColor + public let actionButtonsIncomingTextColor: UIColor - public init(incomingFillColor: UIColor, incomingFillHighlightedColor: UIColor, incomingStrokeColor: UIColor, outgoingFillColor: UIColor, outgoingFillHighlightedColor: UIColor, outgoingStrokeColor: UIColor, freeformFillColor: UIColor, freeformFillHighlightedColor: UIColor, freeformStrokeColor: UIColor, infoFillColor: UIColor, infoStrokeColor: UIColor, incomingPrimaryTextColor: UIColor, incomingSecondaryTextColor: UIColor, incomingLinkTextColor: UIColor, incomingLinkHighlightColor: UIColor, outgoingPrimaryTextColor: UIColor, outgoingSecondaryTextColor: UIColor, outgoingLinkTextColor: UIColor, outgoingLinkHighlightColor: UIColor, infoPrimaryTextColor: UIColor, infoLinkTextColor: UIColor, incomingAccentColor: UIColor, outgoingAccentColor: UIColor, outgoingCheckColor: UIColor, incomingPendingActivityColor: UIColor, outgoingPendingActivityColor: UIColor, mediaDateAndStatusFillColor: UIColor, mediaDateAndStatusTextColor: UIColor, incomingFileTitleColor: UIColor, outgoingFileTitleColor: UIColor, incomingFileDescriptionColor: UIColor, outgoingFileDescriptionColor: UIColor, incomingFileDurationColor: UIColor, outgoingFileDurationColor: UIColor, shareButtonFillColor: UIColor, shareButtonForegroundColor: UIColor, mediaOverlayControlBackgroundColor: UIColor, mediaOverlayControlForegroundColor: UIColor, actionButtonsFillColor: UIColor, actionButtonsTextColor: UIColor) { + public let actionButtonsOutgoingFillColor: UIColor + public let actionButtonsOutgoingStrokeColor: UIColor + public let actionButtonsOutgoingTextColor: UIColor + + public let selectionControlBorderColor: UIColor + public let selectionControlFillColor: UIColor + public let selectionControlForegroundColor: UIColor + + public init(incomingFillColor: UIColor, incomingFillHighlightedColor: UIColor, incomingStrokeColor: UIColor, outgoingFillColor: UIColor, outgoingFillHighlightedColor: UIColor, outgoingStrokeColor: UIColor, freeformFillColor: UIColor, freeformFillHighlightedColor: UIColor, freeformStrokeColor: UIColor, infoFillColor: UIColor, infoStrokeColor: UIColor, incomingPrimaryTextColor: UIColor, incomingSecondaryTextColor: UIColor, incomingLinkTextColor: UIColor, incomingLinkHighlightColor: UIColor, outgoingPrimaryTextColor: UIColor, outgoingSecondaryTextColor: UIColor, outgoingLinkTextColor: UIColor, outgoingLinkHighlightColor: UIColor, infoPrimaryTextColor: UIColor, infoLinkTextColor: UIColor, incomingAccentTextColor: UIColor, outgoingAccentTextColor: UIColor, incomingAccentControlColor: UIColor, outgoingAccentControlColor: UIColor, incomingMediaActiveControlColor: UIColor, outgoingMediaActiveControlColor: UIColor, incomingMediaInactiveControlColor: UIColor, outgoingMediaInactiveControlColor: UIColor, outgoingCheckColor: UIColor, incomingPendingActivityColor: UIColor, outgoingPendingActivityColor: UIColor, mediaDateAndStatusFillColor: UIColor, mediaDateAndStatusTextColor: UIColor, incomingFileTitleColor: UIColor, outgoingFileTitleColor: UIColor, incomingFileDescriptionColor: UIColor, outgoingFileDescriptionColor: UIColor, incomingFileDurationColor: UIColor, outgoingFileDurationColor: UIColor, shareButtonFillColor: UIColor, shareButtonStrokeColor: UIColor, shareButtonForegroundColor: UIColor, mediaOverlayControlBackgroundColor: UIColor, mediaOverlayControlForegroundColor: UIColor, actionButtonsIncomingFillColor: UIColor, actionButtonsIncomingStrokeColor: UIColor, actionButtonsIncomingTextColor: UIColor, actionButtonsOutgoingFillColor: UIColor, actionButtonsOutgoingStrokeColor: UIColor, actionButtonsOutgoingTextColor: UIColor, selectionControlBorderColor: UIColor, selectionControlFillColor: UIColor, selectionControlForegroundColor: UIColor) { self.incomingFillColor = incomingFillColor self.incomingFillHighlightedColor = incomingFillHighlightedColor self.incomingStrokeColor = incomingStrokeColor @@ -575,8 +436,15 @@ public final class PresentationThemeChatBubble { self.infoPrimaryTextColor = infoPrimaryTextColor self.infoLinkTextColor = infoLinkTextColor - self.incomingAccentColor = incomingAccentColor - self.outgoingAccentColor = outgoingAccentColor + self.incomingAccentTextColor = incomingAccentTextColor + self.outgoingAccentTextColor = outgoingAccentTextColor + self.incomingAccentControlColor = incomingAccentControlColor + self.outgoingAccentControlColor = outgoingAccentControlColor + + self.incomingMediaActiveControlColor = incomingMediaActiveControlColor + self.outgoingMediaActiveControlColor = outgoingMediaActiveControlColor + self.incomingMediaInactiveControlColor = incomingMediaInactiveControlColor + self.outgoingMediaInactiveControlColor = outgoingMediaInactiveControlColor self.outgoingCheckColor = outgoingCheckColor self.incomingPendingActivityColor = incomingPendingActivityColor @@ -592,77 +460,23 @@ public final class PresentationThemeChatBubble { self.outgoingFileDurationColor = outgoingFileDurationColor self.shareButtonFillColor = shareButtonFillColor + self.shareButtonStrokeColor = shareButtonStrokeColor self.shareButtonForegroundColor = shareButtonForegroundColor self.mediaOverlayControlBackgroundColor = mediaOverlayControlBackgroundColor self.mediaOverlayControlForegroundColor = mediaOverlayControlForegroundColor - self.actionButtonsFillColor = actionButtonsFillColor - self.actionButtonsTextColor = actionButtonsTextColor - } - - public init(decoder: PostboxDecoder) throws { - self.incomingFillColor = try parseColor(decoder, "incomingFillColor") - self.incomingFillHighlightedColor = try parseColor(decoder, "incomingFillHighlightedColor") - self.incomingStrokeColor = try parseColor(decoder, "incomingStrokeColor") - self.outgoingFillColor = try parseColor(decoder, "outgoingFillColor") - self.outgoingFillHighlightedColor = try parseColor(decoder, "outgoingFillHighlightedColor") - self.outgoingStrokeColor = try parseColor(decoder, "outgoingStrokeColor") - self.freeformFillColor = try parseColor(decoder, "freeformFillColor") - self.freeformFillHighlightedColor = try parseColor(decoder, "freeformFillHighlightedColor") - self.freeformStrokeColor = try parseColor(decoder, "freeformStrokeColor") - self.infoFillColor = try parseColor(decoder, "infoFillColor") - self.infoStrokeColor = try parseColor(decoder, "infoStrokeColor") + self.actionButtonsIncomingFillColor = actionButtonsIncomingFillColor + self.actionButtonsIncomingStrokeColor = actionButtonsIncomingStrokeColor + self.actionButtonsIncomingTextColor = actionButtonsIncomingTextColor - self.incomingPrimaryTextColor = try parseColor(decoder, "incomingPrimaryTextColor") - self.incomingSecondaryTextColor = try parseColor(decoder, "incomingSecondaryTextColor") - self.incomingLinkTextColor = try parseColor(decoder, "incomingLinkTextColor") - self.incomingLinkHighlightColor = try parseColor(decoder, "incomingLinkHighlightColor") - self.outgoingPrimaryTextColor = try parseColor(decoder, "outgoingPrimaryTextColor") - self.outgoingSecondaryTextColor = try parseColor(decoder, "outgoingSecondaryTextColor") - self.outgoingLinkTextColor = try parseColor(decoder, "outgoingLinkTextColor") - self.outgoingLinkHighlightColor = try parseColor(decoder, "outgoingLinkhighlightColor") - self.infoPrimaryTextColor = try parseColor(decoder, "infoPrimaryTextColor") - self.infoLinkTextColor = try parseColor(decoder, "infoLinkTextColor") + self.actionButtonsOutgoingFillColor = actionButtonsOutgoingFillColor + self.actionButtonsOutgoingStrokeColor = actionButtonsOutgoingStrokeColor + self.actionButtonsOutgoingTextColor = actionButtonsOutgoingTextColor - self.incomingAccentColor = try parseColor(decoder, "incomingAccentColor") - self.outgoingAccentColor = try parseColor(decoder, "outgoingAccentColor") - - self.outgoingCheckColor = try parseColor(decoder, "outgoingCheckColor") - self.incomingPendingActivityColor = try parseColor(decoder, "incomingPendingActivityColor") - self.outgoingPendingActivityColor = try parseColor(decoder, "outgoingPendingActivityColor") - self.mediaDateAndStatusFillColor = try parseColor(decoder, "mediaDateAndStatusFillColor") - self.mediaDateAndStatusTextColor = try parseColor(decoder, "mediaDateAndStatusTextColor") - - self.incomingFileTitleColor = try parseColor(decoder, "incomingFileTitleColor") - self.outgoingFileTitleColor = try parseColor(decoder, "outgoingFileTitleColor") - self.incomingFileDescriptionColor = try parseColor(decoder, "incomingFileDescriptionColor") - self.outgoingFileDescriptionColor = try parseColor(decoder, "outgoingFileDescriptionColor") - self.incomingFileDurationColor = try parseColor(decoder, "incomingFileDurationColor") - self.outgoingFileDurationColor = try parseColor(decoder, "outgoingFileDurationColor") - - self.shareButtonFillColor = try parseColor(decoder, "shareButtonFillColor") - self.shareButtonForegroundColor = try parseColor(decoder, "shareButtonForegroundColor") - - self.mediaOverlayControlBackgroundColor = try parseColor(decoder, "mediaOverlayControlBackgroundColor") - self.mediaOverlayControlForegroundColor = try parseColor(decoder, "mediaOverlayControlForegroundColor") - - self.actionButtonsFillColor = try parseColor(decoder, "actionButtonsFillColor") - self.actionButtonsTextColor = try parseColor(decoder, "actionButtonsTextColor") - } - - public func encode(_ encoder: PostboxEncoder) { - for child in Mirror(reflecting: self).children { - if let label = child.label { - if let value = child.value as? UIColor { - encoder.encodeInt32(Int32(bitPattern: value.argb), forKey: label) - } else if let value = child.value as? PresentationThemeSwitch { - encoder.encodeObjectWithEncoder(value, encoder: { value.encode($0) }, forKey: label) - } else { - assertionFailure() - } - } - } + self.selectionControlBorderColor = selectionControlBorderColor + self.selectionControlFillColor = selectionControlFillColor + self.selectionControlForegroundColor = selectionControlForegroundColor } } @@ -690,32 +504,6 @@ public final class PresentationThemeServiceMessage { self.dateFillFloatingColor = dateFillFloatingColor self.dateTextColor = dateTextColor } - - public init(decoder: PostboxDecoder) throws { - self.serviceMessageFillColor = try parseColor(decoder, "serviceMessageFillColor") - self.serviceMessagePrimaryTextColor = try parseColor(decoder, "serviceMessagePrimaryTextColor") - self.serviceMessageLinkHighlightColor = try parseColor(decoder, "serviceMessageLinkHighlightColor") - self.unreadBarFillColor = try parseColor(decoder, "unreadBarFillColor") - self.unreadBarStrokeColor = try parseColor(decoder, "unreadBarStrokeColor") - self.unreadBarTextColor = try parseColor(decoder, "unreadBarTextColor") - self.dateFillStaticColor = try parseColor(decoder, "dateFillStaticColor") - self.dateFillFloatingColor = try parseColor(decoder, "dateFillFloatingColor") - self.dateTextColor = try parseColor(decoder, "dateTextColor") - } - - public func encode(_ encoder: PostboxEncoder) { - for child in Mirror(reflecting: self).children { - if let label = child.label { - if let value = child.value as? UIColor { - encoder.encodeInt32(Int32(bitPattern: value.argb), forKey: label) - } else if let value = child.value as? PresentationThemeSwitch { - encoder.encodeObjectWithEncoder(value, encoder: { value.encode($0) }, forKey: label) - } else { - assertionFailure() - } - } - } - } } public enum PresentationThemeKeyboardColor: Int32 { @@ -750,28 +538,6 @@ public final class PresentationThemeChatInputPanelMediaRecordingControl { self.panelControlContentPrimaryColor = panelControlContentPrimaryColor self.panelControlContentAccentColor = panelControlContentAccentColor } - - public init(decoder: PostboxDecoder) throws { - self.buttonColor = try parseColor(decoder, "buttonColor") - self.micLevelColor = try parseColor(decoder, "micLevelColor") - self.activeIconColor = try parseColor(decoder, "activeIconColor") - self.panelControlFillColor = try parseColor(decoder, "panelControlFillColor") - self.panelControlStrokeColor = try parseColor(decoder, "panelControlStrokeColor") - self.panelControlContentPrimaryColor = try parseColor(decoder, "panelControlContentPrimaryColor") - self.panelControlContentAccentColor = try parseColor(decoder, "panelControlContentAccentColor") - } - - public func encode(_ encoder: PostboxEncoder) { - for child in Mirror(reflecting: self).children { - if let label = child.label { - if let value = child.value as? UIColor { - encoder.encodeInt32(Int32(bitPattern: value.argb), forKey: label) - } else { - assertionFailure() - } - } - } - } } public final class PresentationThemeChatInputPanel { @@ -786,12 +552,15 @@ public final class PresentationThemeChatInputPanel { public let inputPlaceholderColor: UIColor public let inputTextColor: UIColor public let inputControlColor: UIColor + public let actionControlFillColor: UIColor + public let actionControlForegroundColor: UIColor public let primaryTextColor: UIColor + public let secondaryTextColor: UIColor public let mediaRecordingDotColor: UIColor public let keyboardColor: PresentationThemeKeyboardColor public let mediaRecordingControl: PresentationThemeChatInputPanelMediaRecordingControl - public init(panelBackgroundColor: UIColor, panelStrokeColor: UIColor, panelControlAccentColor: UIColor, panelControlColor: UIColor, panelControlDisabledColor: UIColor, panelControlDestructiveColor: UIColor, inputBackgroundColor: UIColor, inputStrokeColor: UIColor, inputPlaceholderColor: UIColor, inputTextColor: UIColor, inputControlColor: UIColor, primaryTextColor: UIColor, mediaRecordingDotColor: UIColor, keyboardColor: PresentationThemeKeyboardColor, mediaRecordingControl: PresentationThemeChatInputPanelMediaRecordingControl) { + public init(panelBackgroundColor: UIColor, panelStrokeColor: UIColor, panelControlAccentColor: UIColor, panelControlColor: UIColor, panelControlDisabledColor: UIColor, panelControlDestructiveColor: UIColor, inputBackgroundColor: UIColor, inputStrokeColor: UIColor, inputPlaceholderColor: UIColor, inputTextColor: UIColor, inputControlColor: UIColor, actionControlFillColor: UIColor, actionControlForegroundColor: UIColor, primaryTextColor: UIColor, secondaryTextColor: UIColor, mediaRecordingDotColor: UIColor, keyboardColor: PresentationThemeKeyboardColor, mediaRecordingControl: PresentationThemeChatInputPanelMediaRecordingControl) { self.panelBackgroundColor = panelBackgroundColor self.panelStrokeColor = panelStrokeColor self.panelControlAccentColor = panelControlAccentColor @@ -803,55 +572,14 @@ public final class PresentationThemeChatInputPanel { self.inputPlaceholderColor = inputPlaceholderColor self.inputTextColor = inputTextColor self.inputControlColor = inputControlColor + self.actionControlFillColor = actionControlFillColor + self.actionControlForegroundColor = actionControlForegroundColor self.primaryTextColor = primaryTextColor + self.secondaryTextColor = secondaryTextColor self.mediaRecordingDotColor = mediaRecordingDotColor self.keyboardColor = keyboardColor self.mediaRecordingControl = mediaRecordingControl } - - public init(decoder: PostboxDecoder) throws { - self.panelBackgroundColor = try parseColor(decoder, "panelBackgroundColor") - self.panelStrokeColor = try parseColor(decoder, "panelStrokeColor") - self.panelControlAccentColor = try parseColor(decoder, "panelControlAccentColor") - self.panelControlColor = try parseColor(decoder, "panelControlColor") - self.panelControlDisabledColor = try parseColor(decoder, "panelControlDisabledColor") - self.panelControlDestructiveColor = try parseColor(decoder, "panelControlDestructiveColor") - self.inputBackgroundColor = try parseColor(decoder, "inputBackgroundColor") - self.inputStrokeColor = try parseColor(decoder, "inputStrokeColor") - self.inputPlaceholderColor = try parseColor(decoder, "inputPlaceholderColor") - self.inputTextColor = try parseColor(decoder, "inputTextColor") - self.inputControlColor = try parseColor(decoder, "inputControlColor") - self.primaryTextColor = try parseColor(decoder, "primaryTextColor") - self.mediaRecordingDotColor = try parseColor(decoder, "mediaRecordingDotColor") - if let value = decoder.decodeOptionalInt32ForKey("keyboardColor"), let color = PresentationThemeKeyboardColor(rawValue: value) { - self.keyboardColor = color - } else { - throw PresentationThemeParsingError.generic - } - if let mediaRecordingControl = (try? decoder.decodeObjectForKeyThrowing("mediaRecordingControl", decoder: { try PresentationThemeChatInputPanelMediaRecordingControl(decoder: $0) })) as? PresentationThemeChatInputPanelMediaRecordingControl { - self.mediaRecordingControl = mediaRecordingControl - } else { - throw PresentationThemeParsingError.generic - } - } - - public func encode(_ encoder: PostboxEncoder) { - for child in Mirror(reflecting: self).children { - if let label = child.label { - if let value = child.value as? UIColor { - encoder.encodeInt32(Int32(bitPattern: value.argb), forKey: label) - } else if let value = child.value as? PresentationThemeSwitch { - encoder.encodeObjectWithEncoder(value, encoder: { value.encode($0) }, forKey: label) - } else if let value = child.value as? PresentationThemeKeyboardColor { - encoder.encodeInt32(value.rawValue, forKey: label) - } else if let value = child.value as? PresentationThemeChatInputPanelMediaRecordingControl { - encoder.encodeObjectWithEncoder(value, encoder: { value.encode($0) }, forKey: label) - } else { - assertionFailure() - } - } - } - } } public final class PresentationThemeInputMediaPanel { @@ -870,29 +598,6 @@ public final class PresentationThemeInputMediaPanel { self.stickersSectionTextColor = stickersSectionTextColor self.gifsBackgroundColor = gifsBackgroundColor } - - public init(decoder: PostboxDecoder) throws { - self.panelSerapatorColor = try parseColor(decoder, "panelSerapatorColor") - self.panelIconColor = try parseColor(decoder, "panelIconColor") - self.panelHighlightedIconBackgroundColor = try parseColor(decoder, "panelHighlightedIconBackgroundColor") - self.stickersBackgroundColor = try parseColor(decoder, "stickersBackgroundColor") - self.stickersSectionTextColor = try parseColor(decoder, "stickersSectionTextColor") - self.gifsBackgroundColor = try parseColor(decoder, "gifsBackgroundColor") - } - - public func encode(_ encoder: PostboxEncoder) { - for child in Mirror(reflecting: self).children { - if let label = child.label { - if let value = child.value as? UIColor { - encoder.encodeInt32(Int32(bitPattern: value.argb), forKey: label) - } else if let value = child.value as? PresentationThemeSwitch { - encoder.encodeObjectWithEncoder(value, encoder: { value.encode($0) }, forKey: label) - } else { - assertionFailure() - } - } - } - } } public final class PresentationThemeInputButtonPanel { @@ -913,30 +618,6 @@ public final class PresentationThemeInputButtonPanel { self.buttonHighlightedStrokeColor = buttonHighlightedStrokeColor self.buttonTextColor = buttonTextColor } - - public init(decoder: PostboxDecoder) throws { - self.panelSerapatorColor = try parseColor(decoder, "panelSerapatorColor") - self.panelBackgroundColor = try parseColor(decoder, "panelBackgroundColor") - self.buttonFillColor = try parseColor(decoder, "buttonFillColor") - self.buttonStrokeColor = try parseColor(decoder, "buttonStrokeColor") - self.buttonHighlightedFillColor = try parseColor(decoder, "buttonHighlightedFillColor") - self.buttonHighlightedStrokeColor = try parseColor(decoder, "buttonHighlightedStrokeColor") - self.buttonTextColor = try parseColor(decoder, "buttonTextColor") - } - - public func encode(_ encoder: PostboxEncoder) { - for child in Mirror(reflecting: self).children { - if let label = child.label { - if let value = child.value as? UIColor { - encoder.encodeInt32(Int32(bitPattern: value.argb), forKey: label) - } else if let value = child.value as? PresentationThemeSwitch { - encoder.encodeObjectWithEncoder(value, encoder: { value.encode($0) }, forKey: label) - } else { - assertionFailure() - } - } - } - } } public final class PresentationThemeChatHistoryNavigation { @@ -944,39 +625,17 @@ public final class PresentationThemeChatHistoryNavigation { public let strokeColor: UIColor public let foregroundColor: UIColor public let badgeBackgroundColor: UIColor + public let badgeStrokeColor: UIColor public let badgeTextColor: UIColor - public init(fillColor: UIColor, strokeColor: UIColor, foregroundColor: UIColor, badgeBackgroundColor: UIColor, badgeTextColor: UIColor) { + public init(fillColor: UIColor, strokeColor: UIColor, foregroundColor: UIColor, badgeBackgroundColor: UIColor, badgeStrokeColor: UIColor, badgeTextColor: UIColor) { self.fillColor = fillColor self.strokeColor = strokeColor self.foregroundColor = foregroundColor self.badgeBackgroundColor = badgeBackgroundColor + self.badgeStrokeColor = badgeStrokeColor self.badgeTextColor = badgeTextColor } - - public init(decoder: PostboxDecoder) throws { - self.fillColor = try parseColor(decoder, "fillColor") - self.strokeColor = try parseColor(decoder, "strokeColor") - self.foregroundColor = try parseColor(decoder, "foregroundColor") - self.badgeBackgroundColor = try parseColor(decoder, "badgeBackgroundColor") - self.badgeTextColor = try parseColor(decoder, "badgeTextColor") - } - - public func encode(_ encoder: PostboxEncoder) { - for child in Mirror(reflecting: self).children { - if let label = child.label { - if let value = child.value as? UIColor { - encoder.encodeInt32(Int32(bitPattern: value.argb), forKey: label) - } else if let value = child.value as? PresentationThemeSwitch { - encoder.encodeObjectWithEncoder(value, encoder: { value.encode($0) }, forKey: label) - } else if let value = child.value as? PresentationThemeKeyboardColor { - encoder.encodeInt32(value.rawValue, forKey: label) - } else { - assertionFailure() - } - } - } - } } public final class PresentationThemeChat { @@ -995,105 +654,73 @@ public final class PresentationThemeChat { self.inputButtonPanel = inputButtonPanel self.historyNavigation = historyNavigation } +} + +public final class PresentationThemeInAppNotification { + public let fillColor: UIColor + public let primaryTextColor: UIColor - public init(decoder: PostboxDecoder) throws { - if let bubble = (try? decoder.decodeObjectForKeyThrowing("bubble", decoder: { try PresentationThemeChatBubble(decoder: $0) })) as? PresentationThemeChatBubble { - self.bubble = bubble - } else { - throw PresentationThemeParsingError.generic - } - if let serviceMessage = (try? decoder.decodeObjectForKeyThrowing("serviceMessage", decoder: { try PresentationThemeServiceMessage(decoder: $0) })) as? PresentationThemeServiceMessage { - self.serviceMessage = serviceMessage - } else { - throw PresentationThemeParsingError.generic - } - if let inputPanel = (try? decoder.decodeObjectForKeyThrowing("inputPanel", decoder: { try PresentationThemeChatInputPanel(decoder: $0) })) as? PresentationThemeChatInputPanel { - self.inputPanel = inputPanel - } else { - throw PresentationThemeParsingError.generic - } - if let inputMediaPanel = (try? decoder.decodeObjectForKeyThrowing("inputMediaPanel", decoder: { try PresentationThemeInputMediaPanel(decoder: $0) })) as? PresentationThemeInputMediaPanel { - self.inputMediaPanel = inputMediaPanel - } else { - throw PresentationThemeParsingError.generic - } - if let inputButtonPanel = (try? decoder.decodeObjectForKeyThrowing("inputButtonPanel", decoder: { try PresentationThemeInputButtonPanel(decoder: $0) })) as? PresentationThemeInputButtonPanel { - self.inputButtonPanel = inputButtonPanel - } else { - throw PresentationThemeParsingError.generic - } - if let historyNavigation = (try? decoder.decodeObjectForKeyThrowing("historyNavigation", decoder: { try PresentationThemeChatHistoryNavigation(decoder: $0) })) as? PresentationThemeChatHistoryNavigation { - self.historyNavigation = historyNavigation - } else { - throw PresentationThemeParsingError.generic - } - } + public let expandedNotification: PresentationThemeExpandedNotification - public func encode(_ encoder: PostboxEncoder) { - encoder.encodeObjectWithEncoder(self.bubble, encoder: { self.bubble.encode($0) }, forKey: "bubble") - encoder.encodeObjectWithEncoder(self.serviceMessage, encoder: { self.serviceMessage.encode($0) }, forKey: "serviceMessage") - encoder.encodeObjectWithEncoder(self.inputPanel, encoder: { self.inputPanel.encode($0) }, forKey: "inputPanel") - encoder.encodeObjectWithEncoder(self.inputMediaPanel, encoder: { self.inputMediaPanel.encode($0) }, forKey: "inputMediaPanel") - encoder.encodeObjectWithEncoder(self.inputButtonPanel, encoder: { self.inputButtonPanel.encode($0) }, forKey: "inputButtonPanel") - encoder.encodeObjectWithEncoder(self.historyNavigation, encoder: { self.historyNavigation.encode($0) }, forKey: "historyNavigation") + public init(fillColor: UIColor, primaryTextColor: UIColor, expandedNotification: PresentationThemeExpandedNotification) { + self.fillColor = fillColor + self.primaryTextColor = primaryTextColor + self.expandedNotification = expandedNotification } } -public final class PresentationTheme: Equatable { +public enum PresentationThemeBuiltinName { + case dayClassic + case day + case nightGrayscale + case nightAccent +} + +public enum PresentationThemeName: Equatable { + case builtin(PresentationThemeBuiltinName) + case custom(String) + + public static func ==(lhs: PresentationThemeName, rhs: PresentationThemeName) -> Bool { + switch lhs { + case let .builtin(name): + if case .builtin(name) = rhs { + return true + } else { + return false + } + case let .custom(name): + if case .custom(name) = rhs { + return true + } else { + return false + } + } + } +} + +public final class PresentationTheme { + public let name: PresentationThemeName + public let overallDarkAppearance: Bool + public let allowsCustomWallpapers: Bool public let rootController: PresentationThemeRootController public let list: PresentationThemeList public let chatList: PresentationThemeChatList public let chat: PresentationThemeChat public let actionSheet: PresentationThemeActionSheet + public let inAppNotification: PresentationThemeInAppNotification public let resourceCache: PresentationsResourceCache = PresentationsResourceCache() - public init(rootController: PresentationThemeRootController, list: PresentationThemeList, chatList: PresentationThemeChatList, chat: PresentationThemeChat, actionSheet: PresentationThemeActionSheet) { + public init(name: PresentationThemeName, overallDarkAppearance: Bool, allowsCustomWallpapers: Bool, rootController: PresentationThemeRootController, list: PresentationThemeList, chatList: PresentationThemeChatList, chat: PresentationThemeChat, actionSheet: PresentationThemeActionSheet, inAppNotification: PresentationThemeInAppNotification) { + self.name = name + self.overallDarkAppearance = overallDarkAppearance + self.allowsCustomWallpapers = allowsCustomWallpapers self.rootController = rootController self.list = list self.chatList = chatList self.chat = chat self.actionSheet = actionSheet - } - - public init(decoder: PostboxDecoder) throws { - if let rootController = (try? decoder.decodeObjectForKeyThrowing("rootController", decoder: { try PresentationThemeRootController(decoder: $0) })) as? PresentationThemeRootController { - self.rootController = rootController - } else { - throw PresentationThemeParsingError.generic - } - if let list = (try? decoder.decodeObjectForKeyThrowing("list", decoder: { try PresentationThemeList(decoder: $0) })) as? PresentationThemeList { - self.list = list - } else { - throw PresentationThemeParsingError.generic - } - if let chatList = (try? decoder.decodeObjectForKeyThrowing("chatList", decoder: { try PresentationThemeChatList(decoder: $0) })) as? PresentationThemeChatList { - self.chatList = chatList - } else { - throw PresentationThemeParsingError.generic - } - if let chat = (try? decoder.decodeObjectForKeyThrowing("chat", decoder: { try PresentationThemeChat(decoder: $0) })) as? PresentationThemeChat { - self.chat = chat - } else { - throw PresentationThemeParsingError.generic - } - if let actionSheet = (try? decoder.decodeObjectForKeyThrowing("actionSheet", decoder: { try PresentationThemeActionSheet(decoder: $0) })) as? PresentationThemeActionSheet { - self.actionSheet = actionSheet - } else { - throw PresentationThemeParsingError.generic - } - } - - public func encode(_ encoder: PostboxEncoder) { - encoder.encodeObjectWithEncoder(self.rootController, encoder: { self.rootController.encode($0) }, forKey: "list") - encoder.encodeObjectWithEncoder(self.list, encoder: { self.list.encode($0) }, forKey: "list") - encoder.encodeObjectWithEncoder(self.chatList, encoder: { self.chatList.encode($0) }, forKey: "chatList") - encoder.encodeObjectWithEncoder(self.chat, encoder: { self.chat.encode($0) }, forKey: "chat") - encoder.encodeObjectWithEncoder(self.actionSheet, encoder: { self.actionSheet.encode($0) }, forKey: "actionSheet") - } - - public static func ==(lhs: PresentationTheme, rhs: PresentationTheme) -> Bool { - return lhs === rhs + self.inAppNotification = inAppNotification } public func image(_ key: Int32, _ generate: (PresentationTheme) -> UIImage?) -> UIImage? { diff --git a/TelegramUI/PresentationThemeEssentialGraphics.swift b/TelegramUI/PresentationThemeEssentialGraphics.swift index 6c01d1c884..7d1ca5c940 100644 --- a/TelegramUI/PresentationThemeEssentialGraphics.swift +++ b/TelegramUI/PresentationThemeEssentialGraphics.swift @@ -3,21 +3,18 @@ import UIKit import Display private func generateCheckImage(partial: Bool, color: UIColor) -> UIImage? { - return generateImage(CGSize(width: 11.0, height: 9.0), contextGenerator: { size, context in - context.translateBy(x: size.width / 2.0, y: size.height / 2.0) - context.scaleBy(x: 1.0, y: -1.0) - context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) - + return generateImage(CGSize(width: 11.0, height: 9.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) - context.scaleBy(x: 0.5, y: 0.5) + context.translateBy(x: 1.0, y: 1.0) context.setStrokeColor(color.cgColor) - context.setLineWidth(2.5) + context.setLineWidth(0.99) + context.setLineCap(.round) + context.setLineJoin(.round) if partial { - let _ = try? drawSvgPath(context, path: "M1,14.5 L2.5,16 L16.4985125,1 ") + let _ = try? drawSvgPath(context, path: "M0.5,7 L7,0 S ") } else { - let _ = try? drawSvgPath(context, path: "M1,10 L7,16 L20.9985125,1 ") + let _ = try? drawSvgPath(context, path: "M0,4 L2.95157047,6.95157047 L2.95157047,6.95157047 C2.97734507,6.97734507 3.01913396,6.97734507 3.04490857,6.95157047 C3.04548448,6.95099456 3.04604969,6.95040803 3.04660389,6.9498112 L9.5,0 S ") } - context.strokePath() }) } @@ -51,6 +48,8 @@ public final class PrincipalThemeEssentialGraphics { public let chatMessageBackgroundIncomingMergedBottomHighlightedImage: UIImage public let chatMessageBackgroundIncomingMergedBothImage: UIImage public let chatMessageBackgroundIncomingMergedBothHighlightedImage: UIImage + public let chatMessageBackgroundIncomingMergedSideImage: UIImage + public let chatMessageBackgroundIncomingMergedSideHighlightedImage: UIImage public let chatMessageBackgroundOutgoingImage: UIImage public let chatMessageBackgroundOutgoingHighlightedImage: UIImage @@ -60,6 +59,8 @@ public final class PrincipalThemeEssentialGraphics { public let chatMessageBackgroundOutgoingMergedBottomHighlightedImage: UIImage public let chatMessageBackgroundOutgoingMergedBothImage: UIImage public let chatMessageBackgroundOutgoingMergedBothHighlightedImage: UIImage + public let chatMessageBackgroundOutgoingMergedSideImage: UIImage + public let chatMessageBackgroundOutgoingMergedSideHighlightedImage: UIImage public let checkBubbleFullImage: UIImage public let checkBubblePartialImage: UIImage @@ -102,6 +103,11 @@ public final class PrincipalThemeEssentialGraphics { self.chatMessageBackgroundOutgoingMergedBottomHighlightedImage = messageBubbleImage(incoming: false, fillColor: bubble.outgoingFillHighlightedColor, strokeColor: bubble.outgoingStrokeColor, neighbors: .bottom) self.chatMessageBackgroundOutgoingMergedBothImage = messageBubbleImage(incoming: false, fillColor: bubble.outgoingFillColor, strokeColor: bubble.outgoingStrokeColor, neighbors: .both) self.chatMessageBackgroundOutgoingMergedBothHighlightedImage = messageBubbleImage(incoming: false, fillColor: bubble.outgoingFillHighlightedColor, strokeColor: bubble.outgoingStrokeColor, neighbors: .both) + + self.chatMessageBackgroundIncomingMergedSideImage = messageBubbleImage(incoming: true, fillColor: bubble.incomingFillColor, strokeColor: bubble.incomingStrokeColor, neighbors: .side) + self.chatMessageBackgroundOutgoingMergedSideImage = messageBubbleImage(incoming: false, fillColor: bubble.outgoingFillColor, strokeColor: bubble.outgoingStrokeColor, neighbors: .side) + self.chatMessageBackgroundIncomingMergedSideHighlightedImage = messageBubbleImage(incoming: true, fillColor: bubble.incomingFillHighlightedColor, strokeColor: bubble.incomingStrokeColor, neighbors: .side) + self.chatMessageBackgroundOutgoingMergedSideHighlightedImage = messageBubbleImage(incoming: false, fillColor: bubble.outgoingFillHighlightedColor, strokeColor: bubble.outgoingStrokeColor, neighbors: .side) self.checkBubbleFullImage = generateCheckImage(partial: false, color: theme.bubble.outgoingCheckColor)! self.checkBubblePartialImage = generateCheckImage(partial: true, color: theme.bubble.outgoingCheckColor)! diff --git a/TelegramUI/PresentationThemeSettings.swift b/TelegramUI/PresentationThemeSettings.swift index d266130cf4..361a0387b5 100644 --- a/TelegramUI/PresentationThemeSettings.swift +++ b/TelegramUI/PresentationThemeSettings.swift @@ -2,21 +2,23 @@ import Foundation import Postbox import SwiftSignalKit -public enum PresentationBuilinThemeReference: Int32 { - case light - case dark +public enum PresentationBuiltinThemeReference: Int32 { + case dayClassic = 0 + case nightGrayscale = 1 + case day = 2 + case nightAccent = 3 } public enum PresentationThemeReference: PostboxCoding, Equatable { - case builtin(PresentationBuilinThemeReference) + case builtin(PresentationBuiltinThemeReference) public init(decoder: PostboxDecoder) { switch decoder.decodeInt32ForKey("v", orElse: 0) { case 0: - self = .builtin(PresentationBuilinThemeReference(rawValue: decoder.decodeInt32ForKey("t", orElse: 0))!) + self = .builtin(PresentationBuiltinThemeReference(rawValue: decoder.decodeInt32ForKey("t", orElse: 0))!) default: //assertionFailure() - self = .builtin(.light) + self = .builtin(.dayClassic) } } @@ -40,27 +42,39 @@ public enum PresentationThemeReference: PostboxCoding, Equatable { } } +public enum PresentationFontSize: Int32 { + case extraSmall = 0 + case small = 1 + case regular = 2 + case large = 3 + case extraLarge = 4 +} + public struct PresentationThemeSettings: PreferencesEntry { public let chatWallpaper: TelegramWallpaper public let theme: PresentationThemeReference + public let fontSize: PresentationFontSize public static var defaultSettings: PresentationThemeSettings { - return PresentationThemeSettings(chatWallpaper: .builtin, theme: .builtin(.light)) + return PresentationThemeSettings(chatWallpaper: .color(0x000000), theme: .builtin(.nightAccent), fontSize: .regular) } - init(chatWallpaper: TelegramWallpaper, theme: PresentationThemeReference) { + public init(chatWallpaper: TelegramWallpaper, theme: PresentationThemeReference, fontSize: PresentationFontSize) { self.chatWallpaper = chatWallpaper self.theme = theme + self.fontSize = fontSize } public init(decoder: PostboxDecoder) { self.chatWallpaper = (decoder.decodeObjectForKey("w", decoder: { TelegramWallpaper(decoder: $0) }) as? TelegramWallpaper) ?? .builtin self.theme = decoder.decodeObjectForKey("t", decoder: { PresentationThemeReference(decoder: $0) }) as! PresentationThemeReference + self.fontSize = PresentationFontSize(rawValue: decoder.decodeInt32ForKey("f", orElse: PresentationFontSize.regular.rawValue)) ?? .regular } public func encode(_ encoder: PostboxEncoder) { encoder.encodeObject(self.chatWallpaper, forKey: "w") encoder.encodeObject(self.theme, forKey: "t") + encoder.encodeInt32(self.fontSize.rawValue, forKey: "f") } public func isEqual(to: PreferencesEntry) -> Bool { @@ -72,11 +86,11 @@ public struct PresentationThemeSettings: PreferencesEntry { } public static func ==(lhs: PresentationThemeSettings, rhs: PresentationThemeSettings) -> Bool { - return lhs.chatWallpaper == rhs.chatWallpaper && lhs.theme == rhs.theme + return lhs.chatWallpaper == rhs.chatWallpaper && lhs.theme == rhs.theme && lhs.fontSize == rhs.fontSize } } -func updatePresentationThemeSettingsInteractively(postbox: Postbox, _ f: @escaping (PresentationThemeSettings) -> PresentationThemeSettings) -> Signal { +public func updatePresentationThemeSettingsInteractively(postbox: Postbox, _ f: @escaping (PresentationThemeSettings) -> PresentationThemeSettings) -> Signal { return postbox.modify { modifier -> Void in modifier.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.presentationThemeSettings, { entry in let currentSettings: PresentationThemeSettings diff --git a/TelegramUI/PrivacyAndSecurityController.swift b/TelegramUI/PrivacyAndSecurityController.swift index 8271416f6c..da786aaea3 100644 --- a/TelegramUI/PrivacyAndSecurityController.swift +++ b/TelegramUI/PrivacyAndSecurityController.swift @@ -14,8 +14,9 @@ private final class PrivacyAndSecurityControllerArguments { let openTwoStepVerification: () -> Void let openActiveSessions: () -> Void let setupAccountAutoremove: () -> Void + let clearPaymentInfo: () -> Void - init(account: Account, openBlockedUsers: @escaping () -> Void, openLastSeenPrivacy: @escaping () -> Void, openGroupsPrivacy: @escaping () -> Void, openVoiceCallPrivacy: @escaping () -> Void, openPasscode: @escaping () -> Void, openTwoStepVerification: @escaping () -> Void, openActiveSessions: @escaping () -> Void, setupAccountAutoremove: @escaping () -> Void) { + init(account: Account, openBlockedUsers: @escaping () -> Void, openLastSeenPrivacy: @escaping () -> Void, openGroupsPrivacy: @escaping () -> Void, openVoiceCallPrivacy: @escaping () -> Void, openPasscode: @escaping () -> Void, openTwoStepVerification: @escaping () -> Void, openActiveSessions: @escaping () -> Void, setupAccountAutoremove: @escaping () -> Void, clearPaymentInfo: @escaping () -> Void) { self.account = account self.openBlockedUsers = openBlockedUsers self.openLastSeenPrivacy = openLastSeenPrivacy @@ -25,6 +26,7 @@ private final class PrivacyAndSecurityControllerArguments { self.openTwoStepVerification = openTwoStepVerification self.openActiveSessions = openActiveSessions self.setupAccountAutoremove = setupAccountAutoremove + self.clearPaymentInfo = clearPaymentInfo } } @@ -32,6 +34,7 @@ private enum PrivacyAndSecuritySection: Int32 { case privacy case security case account + case payment } private enum PrivacyAndSecurityEntry: ItemListNodeEntry { @@ -47,6 +50,9 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry { case accountHeader(PresentationTheme, String) case accountTimeout(PresentationTheme, String, String) case accountInfo(PresentationTheme, String) + case paymentHeader(PresentationTheme, String) + case clearPaymentInfo(PresentationTheme, String, Bool) + case paymentInfo(PresentationTheme, String) var section: ItemListSectionId { switch self { @@ -56,6 +62,8 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry { return PrivacyAndSecuritySection.security.rawValue case .accountHeader, .accountTimeout, .accountInfo: return PrivacyAndSecuritySection.account.rawValue + case .paymentHeader, .clearPaymentInfo, .paymentInfo: + return PrivacyAndSecuritySection.payment.rawValue } } @@ -85,6 +93,12 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry { return 10 case .accountInfo: return 11 + case .paymentHeader: + return 12 + case .clearPaymentInfo: + return 13 + case .paymentInfo: + return 14 } } @@ -162,6 +176,24 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry { } else { return false } + case let .paymentHeader(lhsTheme, lhsText): + if case let .paymentHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .clearPaymentInfo(lhsTheme, lhsText, lhsEnabled): + if case let .clearPaymentInfo(rhsTheme, rhsText, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsEnabled == rhsEnabled { + return true + } else { + return false + } + case let .paymentInfo(lhsTheme, lhsText): + if case let .paymentInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } } } @@ -211,31 +243,53 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry { }) case let .accountInfo(theme, text): return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + case let .paymentHeader(theme, text): + return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + case let .clearPaymentInfo(theme, text, enabled): + return ItemListActionItem(theme: theme, title: text, kind: enabled ? .generic : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: { + arguments.clearPaymentInfo() + }) + case let .paymentInfo(theme, text): + return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) } } } private struct PrivacyAndSecurityControllerState: Equatable { let updatingAccountTimeoutValue: Int32? + let clearingPaymentInfo: Bool + let clearedPaymentInfo: Bool - init() { - self.updatingAccountTimeoutValue = nil - } - - init(updatingAccountTimeoutValue: Int32?) { + init(updatingAccountTimeoutValue: Int32? = nil, clearingPaymentInfo: Bool = false, clearedPaymentInfo: Bool = false) { self.updatingAccountTimeoutValue = updatingAccountTimeoutValue + self.clearingPaymentInfo = clearingPaymentInfo + self.clearedPaymentInfo = clearedPaymentInfo } static func ==(lhs: PrivacyAndSecurityControllerState, rhs: PrivacyAndSecurityControllerState) -> Bool { if lhs.updatingAccountTimeoutValue != rhs.updatingAccountTimeoutValue { return false } + if lhs.clearingPaymentInfo != rhs.clearingPaymentInfo { + return false + } + if lhs.clearedPaymentInfo != rhs.clearedPaymentInfo { + return false + } return true } func withUpdatedUpdatingAccountTimeoutValue(_ updatingAccountTimeoutValue: Int32?) -> PrivacyAndSecurityControllerState { - return PrivacyAndSecurityControllerState(updatingAccountTimeoutValue: updatingAccountTimeoutValue) + return PrivacyAndSecurityControllerState(updatingAccountTimeoutValue: updatingAccountTimeoutValue, clearingPaymentInfo: self.clearingPaymentInfo, clearedPaymentInfo: self.clearedPaymentInfo) + } + + func withUpdatedClearingPaymentInfo(_ clearingPaymentInfo: Bool) -> PrivacyAndSecurityControllerState { + return PrivacyAndSecurityControllerState(updatingAccountTimeoutValue: self.updatingAccountTimeoutValue, clearingPaymentInfo: clearingPaymentInfo, clearedPaymentInfo: self.clearedPaymentInfo) + } + + func withUpdatedClearedPaymentInfo(_ clearedPaymentInfo: Bool) -> PrivacyAndSecurityControllerState { + return PrivacyAndSecurityControllerState(updatingAccountTimeoutValue: self.updatingAccountTimeoutValue, clearingPaymentInfo: self.clearingPaymentInfo, clearedPaymentInfo: clearedPaymentInfo) } } @@ -282,7 +336,16 @@ private func privacyAndSecurityControllerEntries(presentationData: PresentationD } entries.append(.securityHeader(presentationData.theme, presentationData.strings.PrivacySettings_SecurityTitle)) - entries.append(.passcode(presentationData.theme, presentationData.strings.PrivacySettings_Passcode)) + if let biometricAuthentication = LocalAuth.biometricAuthentication { + switch biometricAuthentication { + case .touchId: + entries.append(.passcode(presentationData.theme, presentationData.strings.PrivacySettings_PasscodeAndTouchId)) + case .faceId: + entries.append(.passcode(presentationData.theme, presentationData.strings.PrivacySettings_PasscodeAndFaceId)) + } + } else { + entries.append(.passcode(presentationData.theme, presentationData.strings.PrivacySettings_Passcode)) + } entries.append(.twoStepVerification(presentationData.theme, presentationData.strings.PrivacySettings_TwoStepAuth)) entries.append(.activeSessions(presentationData.theme, presentationData.strings.PrivacySettings_AuthSessions)) entries.append(.accountHeader(presentationData.theme, presentationData.strings.PrivacySettings_DeleteAccountTitle)) @@ -299,6 +362,14 @@ private func privacyAndSecurityControllerEntries(presentationData: PresentationD } entries.append(.accountInfo(presentationData.theme, presentationData.strings.PrivacySettings_DeleteAccountHelp)) + entries.append(.paymentHeader(presentationData.theme, presentationData.strings.Privacy_PaymentsTitle)) + entries.append(.clearPaymentInfo(presentationData.theme, presentationData.strings.Privacy_PaymentsClearInfo, !state.clearingPaymentInfo && !state.clearedPaymentInfo)) + if state.clearedPaymentInfo { + entries.append(.paymentInfo(presentationData.theme, presentationData.strings.Privacy_PaymentsClearInfoDoneHelp)) + } else { + entries.append(.paymentInfo(presentationData.theme, presentationData.strings.Privacy_PaymentsClearInfoHelp)) + } + return entries } @@ -321,6 +392,9 @@ public func privacyAndSecurityController(account: Account, initialSettings: Sign let updateAccountTimeoutDisposable = MetaDisposable() actionsDisposable.add(updateAccountTimeoutDisposable) + let clearPaymentInfoDisposable = MetaDisposable() + actionsDisposable.add(clearPaymentInfoDisposable) + let privacySettingsPromise = Promise() privacySettingsPromise.set(initialSettings) @@ -465,8 +539,42 @@ public func privacyAndSecurityController(account: Account, initialSettings: Sign presentControllerImpl?(controller) } })) + }, clearPaymentInfo: { + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + let controller = ActionSheetController(presentationTheme: presentationData.theme) + let dismissAction: () -> Void = { [weak controller] in + controller?.dismissAnimated() + } + controller.setItemGroups([ + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.Privacy_PaymentsClearInfo, color: .destructive, action: { + var clear = false + updateState { current in + if !current.clearingPaymentInfo && !current.clearedPaymentInfo { + clear = true + return current.withUpdatedClearingPaymentInfo(true) + } else { + return current + } + } + if clear { + clearPaymentInfoDisposable.set((clearBotPaymentInfo(network: account.network) + |> deliverOnMainQueue).start(completed: { + updateState { current in + return current.withUpdatedClearingPaymentInfo(false).withUpdatedClearedPaymentInfo(true) + } + })) + } + dismissAction() + }) + ]), + ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) + ]) + presentControllerImpl?(controller) }) + let previousState = Atomic(value: nil) + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get() |> deliverOnMainQueue, privacySettingsPromise.get()) |> map { presentationData, state, privacySettings -> (ItemListControllerState, (ItemListNodeState, PrivacyAndSecurityEntry.ItemGenerationArguments)) in @@ -476,7 +584,16 @@ public func privacyAndSecurityController(account: Account, initialSettings: Sign } let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.PrivacySettings_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) - let listState = ItemListNodeState(entries: privacyAndSecurityControllerEntries(presentationData: presentationData, state: state, privacySettings: privacySettings), style: .blocks, animateChanges: false) + + let previousStateValue = previousState.swap(state) + var animateChanges = false + if let previousStateValue = previousStateValue { + if previousStateValue.clearedPaymentInfo != state.clearedPaymentInfo { + animateChanges = true + } + } + + let listState = ItemListNodeState(entries: privacyAndSecurityControllerEntries(presentationData: presentationData, state: state, privacySettings: privacySettings), style: .blocks, animateChanges: animateChanges) return (controllerState, (listState, arguments)) } |> afterDisposed { diff --git a/TelegramUI/ProgressNavigationButtonNode.swift b/TelegramUI/ProgressNavigationButtonNode.swift index 428d91f751..80974f054e 100644 --- a/TelegramUI/ProgressNavigationButtonNode.swift +++ b/TelegramUI/ProgressNavigationButtonNode.swift @@ -5,13 +5,17 @@ import Display final class ProgressNavigationButtonNode: ASDisplayNode { private var indicatorNode: ASImageNode - init(theme: PresentationTheme = defaultPresentationTheme) { + convenience init(theme: PresentationTheme) { + self.init(color: theme.rootController.navigationBar.accentTextColor) + } + + init(color: UIColor) { self.indicatorNode = ASImageNode() self.indicatorNode.isLayerBacked = true self.indicatorNode.displayWithoutProcessing = true self.indicatorNode.displaysAsynchronously = false - self.indicatorNode.image = PresentationResourcesRootController.navigationIndefiniteActivityImage(theme) + self.indicatorNode.image = generateIndefiniteActivityIndicatorImage(color: color) super.init() diff --git a/TelegramUI/ProxySettingsController.swift b/TelegramUI/ProxySettingsController.swift new file mode 100644 index 0000000000..5d243180d8 --- /dev/null +++ b/TelegramUI/ProxySettingsController.swift @@ -0,0 +1,350 @@ +import Foundation +import Display +import SwiftSignalKit +import Postbox +import TelegramCore + +private final class ProxySettingsControllerArguments { + let updateState: ((ProxySettingsControllerState) -> ProxySettingsControllerState) -> Void + let share: () -> Void + + init(updateState: @escaping ((ProxySettingsControllerState) -> ProxySettingsControllerState) -> Void, share: @escaping () -> Void) { + self.updateState = updateState + self.share = share + } +} + +private enum ProxySettingsSection: Int32 { + case mode + case connection + case credentials + case calls + case share +} + +private enum ProxySettingsEntry: ItemListNodeEntry { + case modeDisabled(PresentationTheme, String, Bool) + case modeSocks5(PresentationTheme, String, Bool) + + case connectionHeader(PresentationTheme, String) + case connectionServer(PresentationTheme, String, String) + case connectionPort(PresentationTheme, String, String) + + case credentialsHeader(PresentationTheme, String) + case credentialsUsername(PresentationTheme, String, String) + case credentialsPassword(PresentationTheme, String, String) + + case useForCalls(PresentationTheme, String, Bool) + case useForCallsInfo(PresentationTheme, String) + + case share(PresentationTheme, String, Bool) + + var section: ItemListSectionId { + switch self { + case .modeDisabled, .modeSocks5: + return ProxySettingsSection.mode.rawValue + case .connectionHeader, .connectionServer, .connectionPort: + return ProxySettingsSection.connection.rawValue + case .credentialsHeader, .credentialsUsername, .credentialsPassword: + return ProxySettingsSection.credentials.rawValue + case .useForCalls, .useForCallsInfo: + return ProxySettingsSection.calls.rawValue + case .share: + return ProxySettingsSection.share.rawValue + } + } + + var stableId: Int32 { + switch self { + case .modeDisabled: + return 0 + case .modeSocks5: + return 1 + case .connectionHeader: + return 2 + case .connectionServer: + return 3 + case .connectionPort: + return 4 + case .credentialsHeader: + return 5 + case .credentialsUsername: + return 6 + case .credentialsPassword: + return 7 + case .useForCalls: + return 8 + case .useForCallsInfo: + return 9 + case .share: + return 10 + } + } + + static func ==(lhs: ProxySettingsEntry, rhs: ProxySettingsEntry) -> Bool { + switch lhs { + case let .modeDisabled(lhsTheme, lhsText, lhsValue): + if case let .modeDisabled(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + case let .modeSocks5(lhsTheme, lhsText, lhsValue): + if case let .modeSocks5(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + case let .connectionHeader(lhsTheme, lhsText): + if case let .connectionHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .connectionServer(lhsTheme, lhsText, lhsValue): + if case let .connectionServer(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + case let .connectionPort(lhsTheme, lhsText, lhsValue): + if case let .connectionPort(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + case let .credentialsHeader(lhsTheme, lhsText): + if case let .credentialsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .credentialsUsername(lhsTheme, lhsText, lhsValue): + if case let .credentialsUsername(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + case let .credentialsPassword(lhsTheme, lhsText, lhsValue): + if case let .credentialsPassword(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + case let .useForCalls(lhsTheme, lhsText, lhsValue): + if case let .useForCalls(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + case let .useForCallsInfo(lhsTheme, lhsText): + if case let .useForCallsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .share(lhsTheme, lhsText, lhsValue): + if case let .share(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + } + } + + static func <(lhs: ProxySettingsEntry, rhs: ProxySettingsEntry) -> Bool { + return lhs.stableId < rhs.stableId + } + + func item(_ arguments: ProxySettingsControllerArguments) -> ListViewItem { + switch self { + case let .modeDisabled(theme, text, value): + return ItemListCheckboxItem(theme: theme, title: text, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { + arguments.updateState { current in + var state = current + state.enabled = false + return state + } + }) + case let .modeSocks5(theme, text, value): + return ItemListCheckboxItem(theme: theme, title: text, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { + arguments.updateState { current in + var state = current + state.enabled = true + return state + } + }) + case let .connectionHeader(theme, text): + return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + case let .connectionServer(theme, placeholder, text): + return ItemListSingleLineInputItem(theme: theme, title: NSAttributedString(), text: text, placeholder: placeholder, sectionId: self.section, textUpdated: { value in + arguments.updateState { current in + var state = current + state.host = value + return state + } + }, action: {}) + case let .connectionPort(theme, placeholder, text): + return ItemListSingleLineInputItem(theme: theme, title: NSAttributedString(), text: text, placeholder: placeholder, type: .number, sectionId: self.section, textUpdated: { value in + arguments.updateState { current in + var state = current + state.port = value + return state + } + }, action: {}) + case let .credentialsHeader(theme, text): + return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + case let .credentialsUsername(theme, placeholder, text): + return ItemListSingleLineInputItem(theme: theme, title: NSAttributedString(), text: text, placeholder: placeholder, sectionId: self.section, textUpdated: { value in + arguments.updateState { current in + var state = current + state.username = value + return state + } + }, action: {}) + case let .credentialsPassword(theme, placeholder, text): + return ItemListSingleLineInputItem(theme: theme, title: NSAttributedString(), text: text, placeholder: placeholder, type: .password, sectionId: self.section, textUpdated: { value in + arguments.updateState { current in + var state = current + state.password = value + return state + } + }, action: {}) + case let .useForCalls(theme, text, value): + return ItemListSwitchItem(theme: theme, title: text, value: value, enabled: true, sectionId: self.section, style: .blocks, updated: { value in + arguments.updateState { current in + var state = current + state.useForCalls = value + return state + } + }) + case let .useForCallsInfo(theme, text): + return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + case let .share(theme, text, enabled): + return ItemListActionItem(theme: theme, title: text, kind: enabled ? .generic : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: { + arguments.share() + }) + } + } +} + +private struct ProxySettingsControllerState: Equatable { + var enabled: Bool + var host: String + var port: String + var username: String + var password: String + var useForCalls: Bool + + static func ==(lhs: ProxySettingsControllerState, rhs: ProxySettingsControllerState) -> Bool { + if lhs.enabled != rhs.enabled { + return false + } + if lhs.host != rhs.host { + return false + } + if lhs.port != rhs.port { + return false + } + if lhs.username != rhs.username { + return false + } + if lhs.password != rhs.password { + return false + } + if lhs.useForCalls != rhs.useForCalls { + return false + } + return true + } + + var isComplete: Bool { + if !self.enabled { + return false + } + if self.host.isEmpty || self.port.isEmpty || Int(self.port) == nil { + return false + } + return true + } +} + +private func proxySettingsControllerEntries(presentationData: PresentationData, state: ProxySettingsControllerState) -> [ProxySettingsEntry] { + var entries: [ProxySettingsEntry] = [] + + entries.append(.modeDisabled(presentationData.theme, presentationData.strings.SocksProxySetup_TypeNone, !state.enabled)) + entries.append(.modeSocks5(presentationData.theme, presentationData.strings.SocksProxySetup_TypeSocks, state.enabled)) + + if state.enabled { + entries.append(.connectionHeader(presentationData.theme, presentationData.strings.SocksProxySetup_Connection.uppercased())) + entries.append(.connectionServer(presentationData.theme, presentationData.strings.SocksProxySetup_Hostname, state.host)) + entries.append(.connectionPort(presentationData.theme, presentationData.strings.SocksProxySetup_Port, state.port)) + + entries.append(.credentialsHeader(presentationData.theme, presentationData.strings.SocksProxySetup_Credentials)) + entries.append(.credentialsUsername(presentationData.theme, presentationData.strings.SocksProxySetup_Username, state.username)) + entries.append(.credentialsPassword(presentationData.theme, presentationData.strings.SocksProxySetup_Password, state.password)) + + entries.append(.useForCalls(presentationData.theme, presentationData.strings.SocksProxySetup_UseForCalls, state.useForCalls)) + entries.append(.useForCallsInfo(presentationData.theme, presentationData.strings.SocksProxySetup_UseForCallsHelp)) + + entries.append(.share(presentationData.theme, presentationData.strings.Conversation_ContextMenuShare, state.isComplete)) + } + + return entries +} + +func proxySettingsController(account: Account, currentSettings: ProxySettings?) -> ViewController { + let initialState = ProxySettingsControllerState(enabled: currentSettings != nil, host: currentSettings?.host ?? "", port: (currentSettings?.port).flatMap { "\($0)" } ?? "", username: currentSettings?.username ?? "", password: currentSettings?.password ?? "", useForCalls: currentSettings?.useForCalls ?? false) + let stateValue = Atomic(value: initialState) + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let updateState: ((ProxySettingsControllerState) -> ProxySettingsControllerState) -> Void = { f in + statePromise.set(stateValue.modify { f($0) }) + } + + var presentImpl: ((ViewController, Any?) -> Void)? + var dismissImpl: (() -> Void)? + + let arguments = ProxySettingsControllerArguments(updateState: { f in + updateState(f) + }, share: { + let state = stateValue.with { $0 } + if state.enabled && state.isComplete { + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + var result = "tg://socks?server=\(state.host)&port=\(state.port)" + if !state.username.isEmpty { + result += "&user=\(state.username)&pass=\(state.password)" + } + + UIPasteboard.general.string = result + + presentImpl?(standardTextAlertController(title: nil, text: presentationData.strings.Username_LinkCopied, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) + } + }) + + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get()) |> deliverOnMainQueue + |> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, ProxySettingsEntry.ItemGenerationArguments)) in + let rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Done, style: .bold, enabled: !state.enabled || state.isComplete, action: { + var proxySettings: ProxySettings? + if state.enabled && state.isComplete, let port = Int32(state.port) { + proxySettings = ProxySettings(host: state.host, port: port, username: state.username.isEmpty ? nil : state.username, password: state.password.isEmpty ? nil : state.password, useForCalls: state.useForCalls) + } + let _ = applyProxySettings(postbox: account.postbox, network: account.network, settings: proxySettings).start() + dismissImpl?() + }) + + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.SocksProxySetup_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) + let listState = ItemListNodeState(entries: proxySettingsControllerEntries(presentationData: presentationData, state: state), style: .blocks, emptyStateItem: nil, animateChanges: false) + + return (controllerState, (listState, arguments)) + } + + let controller = ItemListController(account: account, state: signal) + presentImpl = { [weak controller] c, d in + controller?.present(c, in: .window(.root), with: d) + } + dismissImpl = { [weak controller] in + let _ = (controller?.navigationController as? NavigationController)?.popViewController(animated: true) + } + return controller +} + diff --git a/TelegramUI/RadialCheckContentNode.swift b/TelegramUI/RadialCheckContentNode.swift new file mode 100644 index 0000000000..289569b194 --- /dev/null +++ b/TelegramUI/RadialCheckContentNode.swift @@ -0,0 +1,178 @@ +import Foundation +import Display +import AsyncDisplayKit +import LegacyComponents +import SwiftSignalKit + +private final class RadialCheckContentNodeParameters: NSObject { + let color: UIColor + let progress: CGFloat + + init(color: UIColor, progress: CGFloat) { + self.color = color + self.progress = progress + + super.init() + } +} + +final class RadialCheckContentNode: RadialStatusContentNode { + var color: UIColor { + didSet { + self.setNeedsDisplay() + } + } + + private var effectiveProgress: CGFloat = 1.0 { + didSet { + self.setNeedsDisplay() + } + } + + private var animationCompletionTimer: SwiftSignalKit.Timer? + + private var isAnimatingProgress: Bool { + return self.pop_animation(forKey: "progress") != nil || self.animationCompletionTimer != nil + } + + private var enqueuedReadyForTransition: (() -> Void)? + + init(color: UIColor) { + self.color = color + + super.init() + + self.displaysAsynchronously = true + self.isOpaque = false + self.isLayerBacked = true + } + + func animateProgress() { + self.animationCompletionTimer?.invalidate() + self.animationCompletionTimer = nil + let animation = POPBasicAnimation() + animation.property = POPAnimatableProperty.property(withName: "progress", initializer: { property in + property?.readBlock = { node, values in + values?.pointee = (node as! RadialCheckContentNode).effectiveProgress + } + property?.writeBlock = { node, values in + (node as! RadialCheckContentNode).effectiveProgress = values!.pointee + } + property?.threshold = 0.01 + }) as! POPAnimatableProperty + animation.fromValue = 0.0 as NSNumber + animation.toValue = 1.0 as NSNumber + animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear) + animation.duration = 0.25 + animation.completionBlock = { [weak self] _, _ in + if let strongSelf = self { + strongSelf.animationCompletionTimer?.invalidate() + /*let animationCompletionTimer = SwiftSignalKit.Timer(timeout: 0.15, repeat: false, completion: { + }, queue: Queue.mainQueue()) + strongSelf.animationCompletionTimer = animationCompletionTimer + animationCompletionTimer.start()*/ + if let strongSelf = self { + strongSelf.animationCompletionTimer = nil + if let enqueuedReadyForTransition = strongSelf.enqueuedReadyForTransition { + strongSelf.enqueuedReadyForTransition = nil + enqueuedReadyForTransition() + } + } + } + } + self.pop_add(animation, forKey: "progress") + } + + override func enqueueReadyForTransition(_ f: @escaping () -> Void) { + if self.isAnimatingProgress { + self.enqueuedReadyForTransition = f + } else { + f() + } + } + + override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { + return RadialCheckContentNodeParameters(color: self.color, progress: self.effectiveProgress) + } + + @objc override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) { + let context = UIGraphicsGetCurrentContext()! + + if !isRasterizing { + context.setBlendMode(.copy) + context.setFillColor(UIColor.clear.cgColor) + context.fill(bounds) + } + + if let parameters = parameters as? RadialCheckContentNodeParameters { + let diameter = bounds.size.width + + let progress = parameters.progress + + var pathLineWidth: CGFloat = 2.0 + var pathDiameter: CGFloat = diameter - pathLineWidth + + if (abs(diameter - 37.0) < 0.1) { + pathLineWidth = 2.5 + pathDiameter = diameter - pathLineWidth * 2.0 - 1.5 + } else if (abs(diameter - 32.0) < 0.1) { + pathLineWidth = 2.0 + pathDiameter = diameter - pathLineWidth * 2.0 - 1.5 + } else { + pathLineWidth = 2.5 + pathDiameter = diameter - pathLineWidth * 2.0 - 1.5 + } + + let center = CGPoint(x: diameter / 2.0, y: diameter / 2.0) + + context.setStrokeColor(parameters.color.cgColor) + context.setLineWidth(pathLineWidth) + context.setLineCap(.round) + context.setLineJoin(.round) + context.setMiterLimit(10.0) + + let firstSegment: CGFloat = max(0.0, min(1.0, progress * 3.0)) + + var s = CGPoint(x: center.x - 10.0, y: center.y + 1.0) + var p1 = CGPoint(x: 7.0, y: 7.0) + var p2 = CGPoint(x: 15.0, y: -16.0) + + if diameter < 36.0 { + s = CGPoint(x: center.x - 7.0, y: center.y + 1.0) + p1 = CGPoint(x: 4.5, y: 4.5) + p2 = CGPoint(x: 10.0, y: -11.0) + } + + if !firstSegment.isZero { + if firstSegment < 1.0 { + context.move(to: CGPoint(x: s.x + p1.x * firstSegment, y: s.y + p1.y * firstSegment)) + context.addLine(to: s) + } else { + let secondSegment = (progress - 0.33) * 1.5 + context.move(to: CGPoint(x: s.x + p1.x + p2.x * secondSegment, y: s.y + p1.y + p2.y * secondSegment)) + context.addLine(to: CGPoint(x: s.x + p1.x, y: s.y + p1.y)) + context.addLine(to: s) + } + } + context.strokePath() + } + } + + override func layout() { + super.layout() + } + + override func animateOut(completion: @escaping () -> Void) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { _ in + completion() + }) + self.layer.animateScale(from: 1.0, to: 0.6, duration: 0.15, removeOnCompletion: false) + } + + override func animateIn() { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + self.layer.animateScale(from: 0.7, to: 1.0, duration: 0.15) + self.animateProgress() + } +} + diff --git a/TelegramUI/RadialProgressContentNode.swift b/TelegramUI/RadialProgressContentNode.swift index 69229e13f5..61b366030b 100644 --- a/TelegramUI/RadialProgressContentNode.swift +++ b/TelegramUI/RadialProgressContentNode.swift @@ -113,6 +113,8 @@ private final class RadialProgressContentSpinnerNode: ASDisplayNode { if let parameters = parameters as? RadialProgressContentSpinnerNodeParameters { context.setStrokeColor(parameters.color.cgColor) + let factor = bounds.size.width / 50.0 + var progress = parameters.progress var startAngle = -CGFloat.pi / 2.0 var endAngle = CGFloat(progress) * 2.0 * CGFloat.pi + startAngle @@ -125,11 +127,13 @@ private final class RadialProgressContentSpinnerNode: ASDisplayNode { } progress = min(1.0, progress) - let pathDiameter = bounds.size.width - 2.25 - 2.5 * 2.0 + let lineWidth = max(1.6, 2.25 * factor) - let path = UIBezierPath(arcCenter: CGPoint(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0), radius: pathDiameter / 2.0, startAngle: startAngle, endAngle:endAngle, clockwise:true) - path.lineWidth = 2.25; - path.lineCapStyle = .round; + let pathDiameter = bounds.size.width - lineWidth - 2.5 * 2.0 + + let path = UIBezierPath(arcCenter: CGPoint(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0), radius: pathDiameter / 2.0, startAngle: startAngle, endAngle: endAngle, clockwise:true) + path.lineWidth = lineWidth + path.lineCapStyle = .round path.stroke() } } @@ -192,11 +196,14 @@ private final class RadialProgressContentCancelNode: ASDisplayNode { if let parameters = parameters as? RadialProgressContentCancelNodeParameters { if parameters.displayCancel { let diameter = min(bounds.size.width, bounds.size.height) + + let factor = diameter / 50.0 + context.setStrokeColor(parameters.color.cgColor) - context.setLineWidth(2.0) + context.setLineWidth(max(1.6, 2.0 * factor)) context.setLineCap(.round) - let crossSize: CGFloat = 14.0 + let crossSize: CGFloat = 14.0 * factor context.move(to: CGPoint(x: diameter / 2.0 - crossSize / 2.0, y: diameter / 2.0 - crossSize / 2.0)) context.addLine(to: CGPoint(x: diameter / 2.0 + crossSize / 2.0, y: diameter / 2.0 + crossSize / 2.0)) context.strokePath() diff --git a/TelegramUI/RadialStatusIconContentNode.swift b/TelegramUI/RadialStatusIconContentNode.swift index 01dbafd8a9..83476d777e 100644 --- a/TelegramUI/RadialStatusIconContentNode.swift +++ b/TelegramUI/RadialStatusIconContentNode.swift @@ -53,9 +53,11 @@ final class RadialStatusIconContentNode: RadialStatusContentNode { context.setLineCap(.round) context.setLineJoin(.round) - let arrowHeadSize: CGFloat = 15.0 - let arrowLength: CGFloat = 18.0 - let arrowHeadOffset: CGFloat = 1.0 + let factor = diameter / 50.0 + + let arrowHeadSize: CGFloat = 15.0 * factor + let arrowLength: CGFloat = 18.0 * factor + let arrowHeadOffset: CGFloat = 1.0 * factor context.move(to: CGPoint(x: diameter / 2.0, y: diameter / 2.0 - arrowLength / 2.0 + arrowHeadOffset)) context.addLine(to: CGPoint(x: diameter / 2.0, y: diameter / 2.0 + arrowLength / 2.0 - 1.0 + arrowHeadOffset)) @@ -68,11 +70,13 @@ final class RadialStatusIconContentNode: RadialStatusContentNode { case let .play(color): context.setFillColor(color.cgColor) + let factor = diameter / 50.0 + let size = CGSize(width: 15.0, height: 18.0) context.translateBy(x: (diameter - size.width) / 2.0 + 1.5, y: (diameter - size.height) / 2.0) if (diameter < 40.0) { context.translateBy(x: size.width / 2.0, y: size.height / 2.0) - context.scaleBy(x: 0.8, y: 0.8) + context.scaleBy(x: factor, y: factor) context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) } let _ = try? drawSvgPath(context, path: "M1.71891969,0.209353049 C0.769586558,-0.350676705 0,0.0908839327 0,1.18800046 L0,16.8564753 C0,17.9569971 0.750549162,18.357187 1.67393713,17.7519379 L14.1073836,9.60224049 C15.0318735,8.99626906 15.0094718,8.04970371 14.062401,7.49100858 L1.71891969,0.209353049 ") @@ -86,11 +90,13 @@ final class RadialStatusIconContentNode: RadialStatusContentNode { case let .pause(color): context.setFillColor(color.cgColor) + let factor = diameter / 50.0 + let size = CGSize(width: 15.0, height: 16.0) context.translateBy(x: (diameter - size.width) / 2.0, y: (diameter - size.height) / 2.0) if (diameter < 40.0) { context.translateBy(x: size.width / 2.0, y: size.height / 2.0) - context.scaleBy(x: 0.8, y: 0.8) + context.scaleBy(x: factor, y: factor) context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) } let _ = try? drawSvgPath(context, path: "M0,1.00087166 C0,0.448105505 0.443716645,0 0.999807492,0 L4.00019251,0 C4.55237094,0 5,0.444630861 5,1.00087166 L5,14.9991283 C5,15.5518945 4.55628335,16 4.00019251,16 L0.999807492,16 C0.447629061,16 0,15.5553691 0,14.9991283 L0,1.00087166 Z M10,1.00087166 C10,0.448105505 10.4437166,0 10.9998075,0 L14.0001925,0 C14.5523709,0 15,0.444630861 15,1.00087166 L15,14.9991283 C15,15.5518945 14.5562834,16 14.0001925,16 L10.9998075,16 C10.4476291,16 10,15.5553691 10,14.9991283 L10,1.00087166 ") diff --git a/TelegramUI/RadialStatusNode.swift b/TelegramUI/RadialStatusNode.swift index f2482496b8..24f0286a16 100644 --- a/TelegramUI/RadialStatusNode.swift +++ b/TelegramUI/RadialStatusNode.swift @@ -7,6 +7,7 @@ enum RadialStatusNodeState: Equatable { case play(UIColor) case pause(UIColor) case progress(color: UIColor, value: CGFloat?, cancelEnabled: Bool) + case check(UIColor) case customIcon(UIImage) static func ==(lhs: RadialStatusNodeState, rhs: RadialStatusNodeState) -> Bool { @@ -41,6 +42,12 @@ enum RadialStatusNodeState: Equatable { } else { return false } + case let .check(lhsColor): + if case let .check(rhsColor) = rhs, lhsColor.isEqual(rhsColor) { + return true + } else { + return false + } case let .customIcon(lhsImage): if case let .customIcon(rhsImage) = rhs, lhsImage === rhsImage { return true @@ -71,6 +78,8 @@ enum RadialStatusNodeState: Equatable { return RadialStatusIconContentNode(icon: .pause(color)) case let .customIcon(image): return RadialStatusIconContentNode(icon: .custom(image)) + case let .check(color): + return RadialCheckContentNode(color: color) case let .progress(color, value, cancelEnabled): if let current = current as? RadialProgressContentNode, current.displayCancel == cancelEnabled { if !current.color.isEqual(color) { @@ -112,6 +121,8 @@ final class RadialStatusNode: ASControlNode { } else { self.transitionToBackgroundColor(state.backgroundColor(color: self.backgroundNodeColor), animated: animated, completion: completion) } + } else { + completion() } } @@ -121,10 +132,14 @@ final class RadialStatusNode: ASControlNode { contentNode.enqueueReadyForTransition { [weak contentNode, weak self] in if let strongSelf = self, let contentNode = contentNode, strongSelf.contentNode === contentNode { if animated { - contentNode.animateOut { [weak contentNode] in - contentNode?.removeFromSupernode() - } strongSelf.contentNode = strongSelf.nextContentNode + contentNode.animateOut { [weak contentNode] in + if let strongSelf = self, let contentNode = contentNode { + if contentNode !== strongSelf.contentNode { + contentNode.removeFromSupernode() + } + } + } if let contentNode = strongSelf.contentNode { strongSelf.addSubnode(contentNode) contentNode.frame = strongSelf.bounds diff --git a/TelegramUI/RaiseToListen.swift b/TelegramUI/RaiseToListen.swift new file mode 100644 index 0000000000..7233257392 --- /dev/null +++ b/TelegramUI/RaiseToListen.swift @@ -0,0 +1,23 @@ +import Foundation + +import TelegramUIPrivateModule + +final class RaiseToListenManager { + private let activator: RaiseToListenActivator + + var enabled: Bool = false { + didSet { + self.activator.enabled = self.enabled + } + } + + init(shouldActivate: @escaping () -> Bool, activate: @escaping () -> Void, deactivate: @escaping () -> Void) { + self.activator = RaiseToListenActivator(shouldActivate: { + return shouldActivate() + }, activate: { + return activate() + }, deactivate: { + return deactivate() + }) + } +} diff --git a/TelegramUI/RaiseToListenActivator.h b/TelegramUI/RaiseToListenActivator.h new file mode 100644 index 0000000000..0c5edfaab5 --- /dev/null +++ b/TelegramUI/RaiseToListenActivator.h @@ -0,0 +1,11 @@ +#import + +@interface RaiseToListenActivator : NSObject + +@property (nonatomic) bool enabled; +@property (nonatomic, readonly) bool activated; + +- (instancetype)initWithShouldActivate:(bool (^)(void))shouldActivate activate:(void (^)(void))activate deactivate:(void (^)(void))deactivate; + +@end + diff --git a/TelegramUI/RaiseToListenActivator.m b/TelegramUI/RaiseToListenActivator.m new file mode 100644 index 0000000000..d851a39336 --- /dev/null +++ b/TelegramUI/RaiseToListenActivator.m @@ -0,0 +1,187 @@ +#import "RaiseToListenActivator.h" + +#import + +#import "DeviceProximityManager.h" + +static NSString *TGEncodeText(NSString *string, int key) +{ + NSMutableString *result = [[NSMutableString alloc] init]; + + for (int i = 0; i < (int)[string length]; i++) + { + unichar c = [string characterAtIndex:i]; + c += key; + [result appendString:[NSString stringWithCharacters:&c length:1]]; + } + + return result; +} + +static void TGDispatchOnMainThread(dispatch_block_t block) +{ + if ([NSThread isMainThread]) + block(); + else + dispatch_async(dispatch_get_main_queue(), block); +} + +@protocol RaiseManager + +- (id)initWithPriority:(int)priority; +- (void)setGestureHandler:(void (^)(int, int))handler; + +@end + +@interface RaiseToListenActivator () { + NSInteger _proximityStateIndex; + + bool (^_shouldActivate)(void); + void (^_activate)(void); + void (^_deactivate)(void); + + bool _proximityState; + STimer *_timer; + + id _manager; +} + +@end + +@implementation RaiseToListenActivator + +- (instancetype)initWithShouldActivate:(bool (^)(void))shouldActivate activate:(void (^)(void))activate deactivate:(void (^)(void))deactivate { + self = [super init]; + if (self != nil) { + _shouldActivate = [shouldActivate copy]; + _activate = [activate copy]; + _deactivate = [deactivate copy]; + + _enabled = false; + } + return self; +} + +- (void)dealloc { + [_timer invalidate]; + _timer = nil; + + [self setEnabled:false]; +} + +- (void)setEnabled:(bool)enabled { + if (_enabled != enabled) { + _enabled = enabled; + + if (enabled) { + Class c = NSClassFromString(TGEncodeText(@"DNHftuvsfNbobhfs", -1)); + if (c != nil) { + _manager = [(id)[c alloc] initWithPriority:0x2]; + __weak RaiseToListenActivator *weakSelf = self; + [_manager setGestureHandler:^(int arg0, int arg1) { + __strong RaiseToListenActivator *strongSelf = weakSelf; + if (strongSelf != nil) { + if (arg0 == 0) { + [strongSelf startCheckingProximity]; + } + } + }]; + } + } else { + [_manager setGestureHandler:nil]; + _manager = nil; + [self stopCheckingProximity]; + if (_activated) { + _activated = false; + if (_deactivate) { + _deactivate(); + } + } + } + } +} + +- (void)stopCheckingProximity { + if (_proximityStateIndex != -1) { + [[DeviceProximityManager shared] remove:_proximityStateIndex]; + _proximityStateIndex = -1; + } +} + +- (bool)shouldActivate { + /*if ([TGMusicPlayer isHeadsetPluggedIn]) { + return false; + }*/ + + if (_shouldActivate) { + return _shouldActivate(); + } + + return true; +} + +- (void)startCheckingProximity { + if (_enabled && [self shouldActivate]) { + NSInteger previousIndex = _proximityStateIndex; + __weak RaiseToListenActivator *weakSelf = self; + _proximityStateIndex = [[DeviceProximityManager shared] add:^(bool value) { + __strong RaiseToListenActivator *strongSelf = weakSelf; + if (strongSelf != nil) { + [strongSelf proximityChanged:value]; + } + }]; + if (previousIndex != -1) { + [[DeviceProximityManager shared] remove:previousIndex]; + } + + if (_proximityState) { + _activated = true; + if (_activate) { + _activate(); + } + + [_timer invalidate]; + _timer = nil; + } else if (_timer == nil) { + __weak RaiseToListenActivator *weakSelf = self; + _timer = [[STimer alloc] initWithTimeout:1.0 repeat:false completion:^{ + __strong RaiseToListenActivator *strongSelf = weakSelf; + if (strongSelf != nil) { + strongSelf->_timer = nil; + [strongSelf stopCheckingProximity]; + } + } queue:[SQueue mainQueue]]; + [_timer start]; + } + } +} + +- (void)proximityChanged:(bool)proximityState { + TGDispatchOnMainThread(^{ + if (_proximityState != proximityState) { + _proximityState = proximityState; + + if (proximityState && _timer != nil) { + [_timer invalidate]; + _timer = nil; + _activated = true; + + if (_activate) { + _activate(); + } + } else if (!proximityState) { + [_timer invalidate]; + _timer = nil; + [self stopCheckingProximity]; + + _activated = false; + if (_deactivate) { + _deactivate(); + } + } + } + }); +} + +@end + diff --git a/TelegramUI/RecentSessionsController.swift b/TelegramUI/RecentSessionsController.swift index 4620e4c580..cb1181f5a2 100644 --- a/TelegramUI/RecentSessionsController.swift +++ b/TelegramUI/RecentSessionsController.swift @@ -400,7 +400,7 @@ public func recentSessionsController(account: Account) -> ViewController { var emptyStateItem: ItemListControllerEmptyStateItem? if sessions == nil { - emptyStateItem = ItemListLoadingIndicatorEmptyStateItem() + emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme) } let previous = previousSessions diff --git a/TelegramUI/ReplyAccessoryPanelNode.swift b/TelegramUI/ReplyAccessoryPanelNode.swift index c7cec5f5b4..7168852a80 100644 --- a/TelegramUI/ReplyAccessoryPanelNode.swift +++ b/TelegramUI/ReplyAccessoryPanelNode.swift @@ -45,6 +45,7 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { self.textNode.displaysAsynchronously = false self.imageNode = TransformImageNode() + self.imageNode.contentAnimations = [.subsequentUpdates] self.imageNode.isHidden = true super.init() @@ -66,13 +67,12 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { authorName = author.displayTitle } if let message = message { - let (string, _) = textStringForReplyMessage(message) - text = string + text = descriptionStringForMessage(message, strings: strings, accountPeerId: account.peerId) } var updatedMedia: Media? var imageDimensions: CGSize? - if let message = message { + if let message = message, !message.containsSecretMedia { for media in message.media { if let image = media as? TelegramMediaImage { updatedMedia = image @@ -82,7 +82,7 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { break } else if let file = media as? TelegramMediaFile { updatedMedia = file - if let representation = largestImageRepresentation(file.previewRepresentations), !file.isSticker { + if !file.isInstantVideo, let representation = largestImageRepresentation(file.previewRepresentations), !file.isSticker { imageDimensions = representation.dimensions } break @@ -103,6 +103,7 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { } else if (updatedMedia != nil) != (strongSelf.previousMedia != nil) { mediaUpdated = true } + strongSelf.previousMedia = updatedMedia var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? if mediaUpdated { @@ -110,15 +111,32 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { if let image = updatedMedia as? TelegramMediaImage { updateImageSignal = chatMessagePhotoThumbnail(account: account, photo: image) } else if let file = updatedMedia as? TelegramMediaFile { - + if file.isVideo { + updateImageSignal = chatMessageVideoThumbnail(account: account, file: file) + } else if let iconImageRepresentation = smallestImageRepresentation(file.previewRepresentations) { + let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [iconImageRepresentation], reference: nil) + updateImageSignal = chatWebpageSnippetPhoto(account: account, photo: tmpImage) + } } } else { updateImageSignal = .single({ _ in return nil }) } } + let isMedia: Bool + if let message = message { + switch messageContentKind(message, strings: strings, accountPeerId: account.peerId) { + case .text: + isMedia = false + default: + isMedia = true + } + } else { + isMedia = false + } + strongSelf.titleNode.attributedText = NSAttributedString(string: authorName, font: Font.medium(15.0), textColor: strongSelf.theme.chat.inputPanel.panelControlAccentColor) - strongSelf.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(15.0), textColor: strongSelf.theme.chat.inputPanel.primaryTextColor) + strongSelf.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(15.0), textColor: isMedia ? strongSelf.theme.chat.inputPanel.secondaryTextColor : strongSelf.theme.chat.inputPanel.primaryTextColor) if let applyImage = applyImage { applyImage() @@ -128,7 +146,7 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { } if let updateImageSignal = updateImageSignal { - strongSelf.imageNode.setSignal(account: account, signal: updateImageSignal) + strongSelf.imageNode.setSignal(updateImageSignal) } strongSelf.setNeedsLayout() diff --git a/TelegramUI/Resources/mute.json b/TelegramUI/Resources/mute.json new file mode 100644 index 0000000000..a8d3b47c04 --- /dev/null +++ b/TelegramUI/Resources/mute.json @@ -0,0 +1 @@ +{"v":"5.1.2","fr":60,"ip":0,"op":3600,"w":228,"h":228,"nm":"mute","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"un Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[117.875,88.875,0],"ix":2},"a":{"a":0,"k":[256,256,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.863,0.863,-0.19]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p833_0p863_0p333_0","0p833_0p863_0p333_0","0p833_-0p19_0p333_0"],"t":0,"s":[0,0,100],"e":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.365,0.365,0.476]},"n":["0p667_1_0p167_0p365","0p667_1_0p167_0p365","0p667_1_0p167_0p476"],"t":10,"s":[100,100,100],"e":[115,115,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_1_0p333_0"],"t":14,"s":[115,115,100],"e":[100,100,100]},{"t":25}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":5,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-34,-34],[-33.992,-34]],"c":false}],"e":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-34,-34],[34,34]],"c":false}]},{"t":15}],"ix":2,"x":"var $bm_rt;\n$bm_rt = content('Group 1').content('Path 1').path;"},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":6,"ix":5},"lc":2,"lj":1,"ml":10,"nm":"Stroke 2","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.584313750267,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":15,"ix":5},"lc":2,"lj":1,"ml":10,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[256,256],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"mute Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[111,88.781,0],"ix":2},"a":{"a":0,"k":[256,256,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.863,0.863,-12.69]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p833_0p863_0p333_0","0p833_0p863_0p333_0","0p833_-12p69_0p333_0"],"t":0,"s":[0,0,100],"e":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.365,0.365,5.476]},"n":["0p667_1_0p167_0p365","0p667_1_0p167_0p365","0p667_1_0p167_5p476"],"t":10,"s":[100,100,100],"e":[115,115,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_1_0p333_0"],"t":14,"s":[115,115,100],"e":[100,100,100]},{"t":25}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.995,-0.904],[0,0],[0.746,0],[0,0],[0,-3.866],[0,0],[-3.866,0],[0,0],[-0.552,-0.503],[0,0],[-2.006,2.207],[0,1.343],[0,0],[2.982,0]],"o":[[0,0],[-0.552,0.502],[0,0],[-3.866,0],[0,0],[0,3.866],[0,0],[0.746,0],[0,0],[2.207,2.007],[0.903,-0.995],[0,0],[0,-2.982],[-1.343,0]],"v":[[16.967,-36.589],[-6.142,-15.581],[-8.16,-14.801],[-19,-14.801],[-26,-7.801],[-26,7.199],[-19,14.199],[-8.16,14.199],[-6.142,14.98],[16.967,35.987],[24.596,35.625],[26,31.992],[26,-32.594],[20.6,-37.994]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[256,256.994],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/TelegramUI/SampleBufferPool.swift b/TelegramUI/SampleBufferPool.swift index d87e286e35..52f5fbf938 100644 --- a/TelegramUI/SampleBufferPool.swift +++ b/TelegramUI/SampleBufferPool.swift @@ -38,8 +38,9 @@ func takeSampleBufferLayer() -> SampleBufferLayer { layer = SampleBufferLayerImpl() } return SampleBufferLayer(layer: layer!, enqueue: { layer in - Queue.concurrentDefaultQueue().async { + Queue.mainQueue().async { layer.flushAndRemoveImage() + layer.setAffineTransform(CGAffineTransform.identity) let _ = pool.modify { list in var list = list list.append(layer) diff --git a/TelegramUI/SaveToCameraRoll.swift b/TelegramUI/SaveToCameraRoll.swift index 8e1d8fd7f3..11dcae0003 100644 --- a/TelegramUI/SaveToCameraRoll.swift +++ b/TelegramUI/SaveToCameraRoll.swift @@ -7,14 +7,27 @@ import Photos func saveToCameraRoll(postbox: Postbox, media: Media) -> Signal { var resource: MediaResource? - var isImage = false + var isImage = true if let image = media as? TelegramMediaImage { if let representation = largestImageRepresentation(image.representations) { resource = representation.resource - isImage = true } } else if let file = media as? TelegramMediaFile { resource = file.resource + if file.isVideo { + isImage = false + } + } else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { + if let image = content.image { + if let representation = largestImageRepresentation(image.representations) { + resource = representation.resource + } + } else if let file = content.file { + resource = file.resource + if file.isVideo { + isImage = false + } + } } if let resource = resource { @@ -37,8 +50,14 @@ func saveToCameraRoll(postbox: Postbox, media: Media) -> Signal { let tempVideoPath = NSTemporaryDirectory() + "\(arc4random64()).mp4" PHPhotoLibrary.shared().performChanges({ if isImage { - if let data = try? Data(contentsOf: URL(fileURLWithPath: data.path)), let image = UIImage(data: data) { - PHAssetChangeRequest.creationRequestForAsset(from: image) + if let fileData = try? Data(contentsOf: URL(fileURLWithPath: data.path)) { + if #available(iOSApplicationExtension 9.0, *) { + PHAssetCreationRequest.forAsset().addResource(with: .photo, data: fileData, options: nil) + } else { + if let image = UIImage(data: fileData) { + PHAssetChangeRequest.creationRequestForAsset(from: image) + } + } } } else { if let _ = try? FileManager.default.copyItem(atPath: data.path, toPath: tempVideoPath) { diff --git a/TelegramUI/SearchBarNode.swift b/TelegramUI/SearchBarNode.swift index d50c817337..fa6e6280af 100644 --- a/TelegramUI/SearchBarNode.swift +++ b/TelegramUI/SearchBarNode.swift @@ -23,17 +23,37 @@ private func generateBackground(backgroundColor: UIColor, foregroundColor: UICol } private class SearchBarTextField: UITextField { - let placeholderLabel: TextNode - var placeholderString: NSAttributedString? + public var didDeleteBackwardWhileEmpty: (() -> Void)? + + let placeholderLabel: ASTextNode + var placeholderString: NSAttributedString? { + didSet { + self.placeholderLabel.attributedText = self.placeholderString + } + } + + let prefixLabel: ASTextNode + var prefixString: NSAttributedString? { + didSet { + self.prefixLabel.attributedText = self.prefixString + } + } override init(frame: CGRect) { - self.placeholderLabel = TextNode() + self.placeholderLabel = ASTextNode() self.placeholderLabel.isLayerBacked = true self.placeholderLabel.displaysAsynchronously = false + self.placeholderLabel.maximumNumberOfLines = 1 + self.placeholderLabel.truncationMode = .byTruncatingTail + + self.prefixLabel = ASTextNode() + self.prefixLabel.isLayerBacked = true + self.prefixLabel.displaysAsynchronously = false super.init(frame: frame) self.addSubnode(self.placeholderLabel) + self.addSubnode(self.prefixLabel) } required init?(coder aDecoder: NSCoder) { @@ -41,7 +61,18 @@ private class SearchBarTextField: UITextField { } override func textRect(forBounds bounds: CGRect) -> CGRect { - return bounds.insetBy(dx: 4.0, dy: 4.0) + if bounds.size.width.isZero { + return CGRect(origin: CGPoint(), size: CGSize()) + } + var rect = bounds.insetBy(dx: 4.0, dy: 4.0) + + let prefixSize = self.prefixLabel.measure(bounds.size) + if !prefixSize.width.isZero { + let prefixOffset = prefixSize.width + rect.origin.x += prefixOffset + rect.size.width -= prefixOffset + } + return rect } override func editingRect(forBounds bounds: CGRect) -> CGRect { @@ -52,20 +83,31 @@ private class SearchBarTextField: UITextField { super.layoutSubviews() let bounds = self.bounds + if bounds.size.width.isZero { + return + } let constrainedSize = self.textRect(forBounds: self.bounds).size - if let placeholderString = self.placeholderString { - let makeLayout = TextNode.asyncLayout(self.placeholderLabel) - let (labelLayout, labelApply) = makeLayout(placeholderString, nil, 1, .end, constrainedSize, .left, nil, UIEdgeInsets()) - let _ = labelApply() - self.placeholderLabel.frame = CGRect(origin: CGPoint(x: self.textRect(forBounds: bounds).minX, y: self.textRect(forBounds: bounds).minY + UIScreenPixel), size: labelLayout.size) + let labelSize = self.placeholderLabel.measure(constrainedSize) + self.placeholderLabel.frame = CGRect(origin: CGPoint(x: self.textRect(forBounds: bounds).minX, y: self.textRect(forBounds: bounds).minY + 1.0), size: labelSize) + + let prefixSize = self.prefixLabel.measure(constrainedSize) + let prefixBounds = bounds.insetBy(dx: 4.0, dy: 4.0) + self.prefixLabel.frame = CGRect(origin: CGPoint(x: prefixBounds.minX, y: prefixBounds.minY + 1.0), size: prefixSize) + } + + override func deleteBackward() { + if self.text == nil || self.text!.isEmpty { + self.didDeleteBackwardWhileEmpty?() } + super.deleteBackward() } } class SearchBarNode: ASDisplayNode, UITextFieldDelegate { var cancel: (() -> Void)? var textUpdated: ((String) -> Void)? + var clearPrefix: (() -> Void)? private let backgroundNode: ASDisplayNode private let separatorNode: ASDisplayNode @@ -83,6 +125,36 @@ class SearchBarNode: ASDisplayNode, UITextFieldDelegate { } } + var prefixString: NSAttributedString? { + get { + return self.textField.prefixString + } set(value) { + let previous = self.prefixString + let updated: Bool + if let previous = previous, let value = value { + updated = !previous.isEqual(to: value) + } else { + updated = (previous != nil) != (value != nil) + } + if updated { + self.textField.prefixString = value + self.textField.setNeedsLayout() + self.updateIsEmpty() + } + } + } + + var text: String { + get { + return self.textField.text ?? "" + } set(value) { + if self.textField.text ?? "" != value { + self.textField.text = value + self.textFieldDidChange(self.textField) + } + } + } + private var theme: PresentationTheme private var strings: PresentationStrings @@ -149,6 +221,10 @@ class SearchBarNode: ASDisplayNode, UITextFieldDelegate { self.textField.delegate = self self.textField.addTarget(self, action: #selector(self.textFieldDidChange(_:)), for: .editingChanged) + self.textField.didDeleteBackwardWhileEmpty = { [weak self] in + self?.clearPressed() + } + self.cancelButton.addTarget(self, action: #selector(self.cancelPressed), forControlEvents: .touchUpInside) self.clearButton.addTarget(self, action: #selector(self.clearPressed), forControlEvents: .touchUpInside) } @@ -166,25 +242,29 @@ class SearchBarNode: ASDisplayNode, UITextFieldDelegate { self.strings = strings } - override func layout() { + func updateLayout(boundingSize: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) { self.backgroundNode.frame = self.bounds - self.separatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: self.bounds.size.height), size: CGSize(width: self.bounds.size.width, height: UIScreenPixel)) + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: self.bounds.size.height), size: CGSize(width: self.bounds.size.width, height: UIScreenPixel))) + + let verticalOffset: CGFloat = boundingSize.height - 64.0 + + let contentFrame = CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: boundingSize.width - leftInset - rightInset, height: boundingSize.height)) let cancelButtonSize = self.cancelButton.measure(CGSize(width: 100.0, height: CGFloat.infinity)) - self.cancelButton.frame = CGRect(origin: CGPoint(x: self.bounds.size.width - 8.0 - cancelButtonSize.width, y: 20.0 + 10.0), size: cancelButtonSize) + transition.updateFrame(node: self.cancelButton, frame: CGRect(origin: CGPoint(x: contentFrame.maxX - 8.0 - cancelButtonSize.width, y: verticalOffset + 31.0), size: cancelButtonSize)) - let textBackgroundFrame = CGRect(origin: CGPoint(x: 8.0, y: 20.0 + 8.0), size: CGSize(width: self.bounds.size.width - 16.0 - cancelButtonSize.width - 11.0, height: 28.0)) - self.textBackgroundNode.frame = textBackgroundFrame + let textBackgroundFrame = CGRect(origin: CGPoint(x: contentFrame.minX + 8.0, y: verticalOffset + 28.0), size: CGSize(width: contentFrame.width - 16.0 - cancelButtonSize.width - 11.0, height: 28.0)) + transition.updateFrame(node: self.textBackgroundNode, frame: textBackgroundFrame) let textFrame = CGRect(origin: CGPoint(x: textBackgroundFrame.minX + 23.0, y: textBackgroundFrame.minY), size: CGSize(width: max(1.0, textBackgroundFrame.size.width - 23.0 - 20.0), height: textBackgroundFrame.size.height)) if let iconImage = self.iconNode.image { let iconSize = iconImage.size - self.iconNode.frame = CGRect(origin: CGPoint(x: textBackgroundFrame.minX + 8.0, y: textBackgroundFrame.minY + floor((textBackgroundFrame.size.height - iconSize.height) / 2.0)), size: iconSize) + transition.updateFrame(node: self.iconNode, frame: CGRect(origin: CGPoint(x: textBackgroundFrame.minX + 8.0, y: textBackgroundFrame.minY + floor((textBackgroundFrame.size.height - iconSize.height) / 2.0)), size: iconSize)) } let clearSize = self.clearButton.measure(CGSize(width: 100.0, height: 100.0)) - self.clearButton.frame = CGRect(origin: CGPoint(x: textBackgroundFrame.maxX - 8.0 - clearSize.width, y: textBackgroundFrame.minY + floor((textBackgroundFrame.size.height - clearSize.height) / 2.0)), size: clearSize) + transition.updateFrame(node: self.clearButton, frame: CGRect(origin: CGPoint(x: textBackgroundFrame.maxX - 8.0 - clearSize.width, y: textBackgroundFrame.minY + floor((textBackgroundFrame.size.height - clearSize.height) / 2.0)), size: clearSize)) self.textField.frame = textFrame } @@ -205,7 +285,11 @@ class SearchBarNode: ASDisplayNode, UITextFieldDelegate { let initialTextBackgroundFrame = node.convert(node.backgroundNode.frame, to: self) let initialBackgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: self.bounds.size.width, height: max(0.0, initialTextBackgroundFrame.maxY + 8.0))) - self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration) + if let fromBackgroundColor = node.backgroundColor, let toBackgroundColor = self.backgroundNode.backgroundColor { + self.backgroundNode.layer.animate(from: fromBackgroundColor.cgColor, to: toBackgroundColor.cgColor, keyPath: "backgroundColor", timingFunction: kCAMediaTimingFunctionEaseInEaseOut, duration: duration * 0.7) + } else { + self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration) + } self.backgroundNode.layer.animateFrame(from: initialBackgroundFrame, to: self.backgroundNode.frame, duration: duration, timingFunction: timingFunction) let initialSeparatorFrame = CGRect(origin: CGPoint(x: 0.0, y: max(0.0, initialTextBackgroundFrame.maxY + 8.0)), size: CGSize(width: self.bounds.size.width, height: UIScreenPixel)) @@ -256,7 +340,11 @@ class SearchBarNode: ASDisplayNode, UITextFieldDelegate { } let targetBackgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: self.bounds.size.width, height: max(0.0, targetTextBackgroundFrame.maxY + 8.0))) - self.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration / 2.0, removeOnCompletion: false) + if let toBackgroundColor = node.backgroundColor, let fromBackgroundColor = self.backgroundNode.backgroundColor { + self.backgroundNode.layer.animate(from: fromBackgroundColor.cgColor, to: toBackgroundColor.cgColor, keyPath: "backgroundColor", timingFunction: kCAMediaTimingFunctionEaseInEaseOut, duration: duration * 0.5, removeOnCompletion: false) + } else { + self.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration / 2.0, removeOnCompletion: false) + } self.backgroundNode.layer.animateFrame(from: self.backgroundNode.frame, to: targetBackgroundFrame, duration: duration, timingFunction: timingFunction, removeOnCompletion: false, completion: { _ in backgroundCompleted = true intermediateCompletion() @@ -284,11 +372,19 @@ class SearchBarNode: ASDisplayNode, UITextFieldDelegate { transitionBackgroundNode.layer.animateFrame(from: self.textBackgroundNode.frame, to: targetTextBackgroundFrame, duration: duration, timingFunction: timingFunction, removeOnCompletion: false) let textFieldFrame = self.textField.frame - let targetLabelNodeFrame = CGRect(origin: node.labelNode.frame.offsetBy(dx: targetTextBackgroundFrame.origin.x - 4.0, dy: targetTextBackgroundFrame.origin.y - 5.0 + UIScreenPixel).origin, size: textFieldFrame.size) + let targetLabelNodeFrame = CGRect(origin: CGPoint(x: node.labelNode.frame.minX + targetTextBackgroundFrame.origin.x - 4.0, y: targetTextBackgroundFrame.minY + floorToScreenPixels((targetTextBackgroundFrame.size.height - textFieldFrame.size.height) / 2.0)), size: textFieldFrame.size) self.textField.layer.animateFrame(from: self.textField.frame, to: targetLabelNodeFrame, duration: duration, timingFunction: timingFunction, removeOnCompletion: false) + if let snapshot = node.labelNode.layer.snapshotContentTree() { + snapshot.frame = CGRect(origin: self.textField.placeholderLabel.frame.origin.offsetBy(dx: 0.0, dy: UIScreenPixel), size: node.labelNode.frame.size) + self.textField.layer.addSublayer(snapshot) + snapshot.animateAlpha(from: 0.0, to: 1.0, duration: duration * 2.0 / 3.0, timingFunction: kCAMediaTimingFunctionLinear) + self.textField.placeholderLabel.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 3.0 / 2.0, timingFunction: kCAMediaTimingFunctionLinear, removeOnCompletion: false) + + } let iconFrame = self.iconNode.frame let targetIconFrame = CGRect(origin: node.iconNode.frame.offsetBy(dx: targetTextBackgroundFrame.origin.x, dy: targetTextBackgroundFrame.origin.y).origin, size: iconFrame.size) + self.iconNode.image = node.iconNode.image self.iconNode.layer.animateFrame(from: self.iconNode.frame, to: targetIconFrame, duration: duration, timingFunction: timingFunction, removeOnCompletion: false) let cancelButtonFrame = self.cancelButton.frame @@ -308,16 +404,20 @@ class SearchBarNode: ASDisplayNode, UITextFieldDelegate { } @objc func textFieldDidChange(_ textField: UITextField) { - let isEmpty = !(textField.text?.isEmpty ?? true) - if isEmpty != self.textField.placeholderLabel.isHidden { - self.textField.placeholderLabel.isHidden = isEmpty - self.clearButton.isHidden = !isEmpty - } + self.updateIsEmpty() if let textUpdated = self.textUpdated { textUpdated(textField.text ?? "") } } + private func updateIsEmpty() { + let isEmpty = !(textField.text?.isEmpty ?? true) + if isEmpty != self.textField.placeholderLabel.isHidden { + self.textField.placeholderLabel.isHidden = isEmpty + } + self.clearButton.isHidden = !isEmpty && self.prefixString == nil + } + @objc func cancelPressed() { if let cancel = self.cancel { cancel() @@ -325,7 +425,13 @@ class SearchBarNode: ASDisplayNode, UITextFieldDelegate { } @objc func clearPressed() { - self.textField.text = "" - self.textFieldDidChange(self.textField) + if (self.textField.text?.isEmpty ?? true) { + if self.prefixString != nil { + self.clearPrefix?() + } + } else { + self.textField.text = "" + self.textFieldDidChange(self.textField) + } } } diff --git a/TelegramUI/SearchBarPlaceholderNode.swift b/TelegramUI/SearchBarPlaceholderNode.swift index 628cfc6db8..09310f7bdc 100644 --- a/TelegramUI/SearchBarPlaceholderNode.swift +++ b/TelegramUI/SearchBarPlaceholderNode.swift @@ -85,7 +85,7 @@ class SearchBarPlaceholderNode: ASDisplayNode, ASEditableTextNodeDelegate { let currentIconColor = self.iconColor return { placeholderString, constrainedSize, iconColor, foregroundColor, backgroundColor in - let (labelLayoutResult, labelApply) = labelLayout(placeholderString, foregroundColor, 1, .end, constrainedSize, .natural, nil, UIEdgeInsets()) + let (labelLayoutResult, labelApply) = labelLayout(TextNodeLayoutArguments(attributedString: placeholderString, backgroundColor: foregroundColor, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: constrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) var updatedBackgroundImage: UIImage? var updatedIconImage: UIImage? @@ -113,13 +113,14 @@ class SearchBarPlaceholderNode: ASDisplayNode, ASEditableTextNodeDelegate { strongSelf.placeholderString = placeholderString - let labelFrame = CGRect(origin: CGPoint(x: floor((constrainedSize.width - labelLayoutResult.size.width) / 2.0), y: floor((28.0 - labelLayoutResult.size.height) / 2.0) - UIScreenPixel), size: labelLayoutResult.size) + let labelFrame = CGRect(origin: CGPoint(x: floor((constrainedSize.width - labelLayoutResult.size.width) / 2.0), y: floorToScreenPixels((28.0 - labelLayoutResult.size.height) / 2.0)), size: labelLayoutResult.size) strongSelf.labelNode.frame = labelFrame if let iconImage = strongSelf.iconNode.image { let iconSize = iconImage.size - strongSelf.iconNode.frame = CGRect(origin: CGPoint(x: labelFrame.minX - 4.0 - iconSize.width, y: floor((28.0 - iconSize.height) / 2.0) + UIScreenPixel), size: iconSize) + strongSelf.iconNode.frame = CGRect(origin: CGPoint(x: labelFrame.minX - 4.0 - iconSize.width, y: floorToScreenPixels((28.0 - iconSize.height) / 2.0)), size: iconSize) } strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: constrainedSize.width, height: 28.0)) + strongSelf.backgroundColor = backgroundColor } } } diff --git a/TelegramUI/SearchDisplayController.swift b/TelegramUI/SearchDisplayController.swift index 91faa38389..22afd041cc 100644 --- a/TelegramUI/SearchDisplayController.swift +++ b/TelegramUI/SearchDisplayController.swift @@ -36,13 +36,26 @@ final class SearchDisplayController { } func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { - let searchBarFrame = CGRect(origin: CGPoint(x: 0.0, y: (layout.statusBarHeight ?? 0.0) - 20.0), size: CGSize(width: layout.size.width, height: 64.0)) - transition.updateFrame(node: self.searchBar, frame: searchBarFrame) + let statusBarHeight: CGFloat = layout.statusBarHeight ?? 0.0 + let searchBarHeight: CGFloat = max(20.0, statusBarHeight) + 44.0 + let navigationBarOffset: CGFloat + if statusBarHeight.isZero { + navigationBarOffset = -20.0 + } else { + navigationBarOffset = 0.0 + } + var navigationBarFrame = CGRect(origin: CGPoint(x: 0.0, y: navigationBarOffset), size: CGSize(width: layout.size.width, height: searchBarHeight)) + if layout.statusBarHeight == nil { + navigationBarFrame.size.height = 64.0 + } - self.containerLayout = (layout, searchBarFrame.maxY) + transition.updateFrame(node: self.searchBar, frame: navigationBarFrame) + self.searchBar.updateLayout(boundingSize: navigationBarFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: transition) + + self.containerLayout = (layout, navigationBarFrame.maxY) transition.updateFrame(node: self.contentNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - self.contentNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: LayoutMetrics(), intrinsicInsets: layout.intrinsicInsets, statusBarHeight: nil, inputHeight: layout.inputHeight), navigationBarHeight: searchBarFrame.maxY, transition: transition) + self.contentNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: LayoutMetrics(), intrinsicInsets: layout.intrinsicInsets, safeInsets: layout.safeInsets, statusBarHeight: nil, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging), navigationBarHeight: navigationBarFrame.maxY, transition: transition) } func activate(insertSubnode: (ASDisplayNode) -> Void, placeholder: SearchBarPlaceholderNode) { @@ -53,7 +66,7 @@ final class SearchDisplayController { insertSubnode(self.contentNode) self.contentNode.frame = CGRect(origin: CGPoint(), size: layout.size) - self.contentNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: LayoutMetrics(), intrinsicInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil), navigationBarHeight: navigationBarHeight, transition: .immediate) + self.contentNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: LayoutMetrics(), intrinsicInsets: UIEdgeInsets(), safeInsets: layout.safeInsets, statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false), navigationBarHeight: navigationBarHeight, transition: .immediate) let initialTextBackgroundFrame = placeholder.convert(placeholder.backgroundNode.frame, to: self.contentNode.supernode) @@ -62,7 +75,21 @@ final class SearchDisplayController { self.contentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionEaseOut) self.searchBar.placeholderString = placeholder.placeholderString - self.searchBar.frame = CGRect(origin: CGPoint(x: 0.0, y: (layout.statusBarHeight ?? 0.0) - 20.0), size: CGSize(width: layout.size.width, height: 64.0)) + + let statusBarHeight: CGFloat = layout.statusBarHeight ?? 0.0 + let searchBarHeight: CGFloat = max(20.0, statusBarHeight) + 44.0 + let navigationBarOffset: CGFloat + if statusBarHeight.isZero { + navigationBarOffset = -20.0 + } else { + navigationBarOffset = 0.0 + } + var navigationBarFrame = CGRect(origin: CGPoint(x: 0.0, y: navigationBarOffset), size: CGSize(width: layout.size.width, height: searchBarHeight)) + if layout.statusBarHeight == nil { + navigationBarFrame.size.height = 64.0 + } + + self.searchBar.frame = navigationBarFrame insertSubnode(searchBar) self.searchBar.layout() diff --git a/TelegramUI/SecretChatHandshakeStatusInputPanelNode.swift b/TelegramUI/SecretChatHandshakeStatusInputPanelNode.swift index e51ebe7940..37c3c945a7 100644 --- a/TelegramUI/SecretChatHandshakeStatusInputPanelNode.swift +++ b/TelegramUI/SecretChatHandshakeStatusInputPanelNode.swift @@ -39,7 +39,7 @@ final class SecretChatHandshakeStatusInputPanelNode: ChatInputPanelNode { self.interfaceInteraction?.unblockPeer() } - override func updateLayout(width: CGFloat, maxHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { if self.presentationInterfaceState != interfaceState { self.presentationInterfaceState = interfaceState @@ -57,7 +57,7 @@ final class SecretChatHandshakeStatusInputPanelNode: ChatInputPanelNode { let panelHeight: CGFloat = 47.0 - self.button.frame = CGRect(origin: CGPoint(x: floor((width - buttonSize.width) / 2.0), y: floor((panelHeight - buttonSize.height) / 2.0)), size: buttonSize) + self.button.frame = CGRect(origin: CGPoint(x: leftInset + floor((width - leftInset - rightInset - buttonSize.width) / 2.0), y: floor((panelHeight - buttonSize.height) / 2.0)), size: buttonSize) return panelHeight } diff --git a/TelegramUI/SecretMediaPreviewController.swift b/TelegramUI/SecretMediaPreviewController.swift index 4e18f2cdc2..2bfa696f1c 100644 --- a/TelegramUI/SecretMediaPreviewController.swift +++ b/TelegramUI/SecretMediaPreviewController.swift @@ -70,7 +70,10 @@ public final class SecretMediaPreviewController: ViewController { if let messageView = self.messageView, let message = messageView.message { if self.currentNodeMessageId != message.id { self.currentNodeMessageId = message.id - let item = galleryItemForEntry(account: account, theme: self.presentationData.theme, strings: self.presentationData.strings, entry: .MessageEntry(message, false, nil, nil)) + guard let item = galleryItemForEntry(account: account, theme: self.presentationData.theme, strings: self.presentationData.strings, entry: .MessageEntry(message, false, nil, nil), streamVideos: false) else { + self._ready.set(.single(true)) + return + } let itemNode = item.node() self.controllerNode.setItemNode(itemNode) diff --git a/TelegramUI/SelectablePeerNode.swift b/TelegramUI/SelectablePeerNode.swift index 1f5277576e..0c7bf3ffd1 100644 --- a/TelegramUI/SelectablePeerNode.swift +++ b/TelegramUI/SelectablePeerNode.swift @@ -7,17 +7,52 @@ import SwiftSignalKit import LegacyComponents -private let selectionBackgroundImage = generateImage(CGSize(width: 60.0 + 4.0, height: 60.0 + 4.0), rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(UIColor(rgb: 0x007ee5).cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) - context.setFillColor(UIColor.white.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(x: 2.0, y: 2.0), size: CGSize(width: size.width - 4.0, height: size.height - 4.0))) -}) - private let avatarFont: UIFont = UIFont(name: "ArialRoundedMTBold", size: 24.0)! private let textFont = Font.regular(11.0) +final class SelectablePeerNodeTheme { + let textColor: UIColor + let secretTextColor: UIColor + let selectedTextColor: UIColor + let checkBackgroundColor: UIColor + let checkFillColor: UIColor + let checkColor: UIColor + + init(textColor: UIColor, secretTextColor: UIColor, selectedTextColor: UIColor, checkBackgroundColor: UIColor, checkFillColor: UIColor, checkColor: UIColor) { + self.textColor = textColor + self.secretTextColor = secretTextColor + self.selectedTextColor = selectedTextColor + self.checkBackgroundColor = checkBackgroundColor + self.checkFillColor = checkFillColor + self.checkColor = checkColor + } + + func isEqual(to: SelectablePeerNodeTheme) -> Bool { + if self === to { + return true + } + if !self.textColor.isEqual(to.textColor) { + return false + } + if !self.secretTextColor.isEqual(to.secretTextColor) { + return false + } + if !self.selectedTextColor.isEqual(to.selectedTextColor) { + return false + } + if !self.checkBackgroundColor.isEqual(to.checkBackgroundColor) { + return false + } + if !self.checkFillColor.isEqual(to.checkFillColor) { + return false + } + if !self.checkColor.isEqual(to.checkColor) { + return false + } + return true + } +} + final class SelectablePeerNode: ASDisplayNode { private let avatarSelectionNode: ASImageNode private let avatarNodeContainer: ASDisplayNode @@ -26,26 +61,18 @@ final class SelectablePeerNode: ASDisplayNode { private let textNode: ASTextNode var toggleSelection: (() -> Void)? + var longTapAction: (() -> Void)? private var currentSelected = false private var peer: Peer? private var chatPeer: Peer? - var textColor: UIColor = .black { + var theme: SelectablePeerNodeTheme = SelectablePeerNodeTheme(textColor: .black, secretTextColor: .green, selectedTextColor: .blue, checkBackgroundColor: .white, checkFillColor: .blue, checkColor: .white) { didSet { - if !self.textColor.isEqual(oldValue) { + if !self.theme.isEqual(to: oldValue) { if let peer = self.peer { - self.textNode.attributedText = NSAttributedString(string: peer.displayTitle, font: textFont, textColor: self.currentSelected ? self.selectedColor : self.textColor, paragraphAlignment: .center) - } - } - } - } - var selectedColor: UIColor = UIColor(rgb: 0x007ee5) { - didSet { - if !self.selectedColor.isEqual(oldValue) { - if let peer = self.peer { - self.textNode.attributedText = NSAttributedString(string: peer.displayTitle, font: textFont, textColor: self.currentSelected ? self.selectedColor : self.textColor, paragraphAlignment: .center) + self.textNode.attributedText = NSAttributedString(string: peer.displayTitle, font: textFont, textColor: self.currentSelected ? self.theme.selectedTextColor : self.theme.textColor, paragraphAlignment: .center) } } } @@ -55,7 +82,6 @@ final class SelectablePeerNode: ASDisplayNode { self.avatarNodeContainer = ASDisplayNode() self.avatarSelectionNode = ASImageNode() - self.avatarSelectionNode.image = selectionBackgroundImage self.avatarSelectionNode.isLayerBacked = true self.avatarSelectionNode.displayWithoutProcessing = true self.avatarSelectionNode.displaysAsynchronously = false @@ -78,19 +104,26 @@ final class SelectablePeerNode: ASDisplayNode { self.addSubnode(self.textNode) } - func setup(account: Account, peer: Peer, chatPeer: Peer?, numberOfLines: Int = 2) { + func setup(account: Account, strings: PresentationStrings, peer: Peer, chatPeer: Peer?, numberOfLines: Int = 2) { self.peer = peer self.chatPeer = chatPeer - var defaultColor: UIColor = self.textColor + var defaultColor: UIColor = self.theme.textColor if let chatPeer = chatPeer, chatPeer.id.namespace == Namespaces.Peer.SecretChat { - defaultColor = UIColor(rgb: 0x149a1f) + defaultColor = self.theme.secretTextColor } - let text = peer.displayTitle + let text: String + var overrideImage: AvatarNodeImageOverride? + if peer.id == account.peerId { + text = strings.DialogList_SavedMessages + overrideImage = .savedMessagesIcon + } else { + text = peer.compactDisplayTitle + } self.textNode.maximumNumberOfLines = UInt(numberOfLines) - self.textNode.attributedText = NSAttributedString(string: text, font: textFont, textColor: self.currentSelected ? self.selectedColor : defaultColor, paragraphAlignment: .center) - self.avatarNode.setPeer(account: account, peer: peer) + self.textNode.attributedText = NSAttributedString(string: text, font: textFont, textColor: self.currentSelected ? self.theme.selectedTextColor : defaultColor, paragraphAlignment: .center) + self.avatarNode.setPeer(account: account, peer: peer, overrideImage: overrideImage) self.setNeedsLayout() } @@ -98,13 +131,21 @@ final class SelectablePeerNode: ASDisplayNode { if selected != self.currentSelected { self.currentSelected = selected - if let peer = self.peer { - self.textNode.attributedText = NSAttributedString(string: peer.displayTitle, font: textFont, textColor: selected ? self.selectedColor : self.textColor, paragraphAlignment: .center) + if let attributedText = self.textNode.attributedText { + self.textNode.attributedText = NSAttributedString(string: attributedText.string, font: textFont, textColor: selected ? self.theme.selectedTextColor : self.theme.textColor, paragraphAlignment: .center) } if selected { self.avatarNode.transform = CATransform3DMakeScale(0.866666, 0.866666, 1.0) self.avatarSelectionNode.alpha = 1.0 + self.avatarSelectionNode.image = generateImage(CGSize(width: 60.0 + 4.0, height: 60.0 + 4.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(self.theme.selectedTextColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + context.setBlendMode(.copy) + context.setFillColor(UIColor.clear.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 2.0, y: 2.0), size: CGSize(width: size.width - 4.0, height: size.height - 4.0))) + }) if animated { //self.avatarNode.layer.animateSpring(from: 1.0 as NSNumber, to: 0.866666 as NSNumber, keyPath: "transform.scale", duration: 0.5, initialVelocity: 10.0) self.avatarNode.layer.animateScale(from: 1.0, to: 0.866666, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring) @@ -116,11 +157,33 @@ final class SelectablePeerNode: ASDisplayNode { if animated { //self.avatarNode.layer.animateSpring(from: 0.866666 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.6, initialVelocity: 10.0) self.avatarNode.layer.animateScale(from: 0.866666, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) - self.avatarSelectionNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.28) + self.avatarSelectionNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.28, completion: { [weak avatarSelectionNode] _ in + avatarSelectionNode?.image = nil + }) + } else { + self.avatarSelectionNode.image = nil } } - self.checkView?.setSelected(selected, animated: animated) + if selected { + if self.checkView == nil { + let checkView = TGCheckButtonView(style: TGCheckButtonStyleShare, pallete: TGCheckButtonPallete(defaultBackgroundColor: self.theme.checkBackgroundColor, accentBackgroundColor: self.theme.checkBackgroundColor, defaultBorderColor: .clear, mediaBorderColor: .clear, chatBorderColor: .clear, check: self.theme.checkFillColor, blueColor: self.theme.checkFillColor, barBackgroundColor: self.theme.checkBackgroundColor))! + + self.checkView = checkView + checkView.isUserInteractionEnabled = false + self.view.addSubview(checkView) + + let avatarFrame = self.avatarNode.frame + let checkSize = checkView.bounds.size + checkView.frame = CGRect(origin: CGPoint(x: avatarFrame.maxX - 14.0, y: avatarFrame.maxY - 22.0), size: checkSize) + checkView.setSelected(true, animated: animated) + } + } else if let checkView = self.checkView { + self.checkView = nil + checkView.setSelected(false, animated: animated, bump: false, completion: { [weak checkView] in + checkView?.removeFromSuperview() + }) + } } } @@ -129,15 +192,15 @@ final class SelectablePeerNode: ASDisplayNode { self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) - let checkView = TGCheckButtonView(style: TGCheckButtonStyleShare)! - self.checkView = checkView - checkView.isUserInteractionEnabled = false - checkView.setSelected(self.currentSelected, animated: false) - self.view.addSubview(checkView) - - let avatarFrame = self.avatarNode.frame - let checkSize = checkView.bounds.size - checkView.frame = CGRect(origin: CGPoint(x: avatarFrame.maxX - 14.0, y: avatarFrame.maxY - 22.0), size: checkSize) + let longTapRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.longTapGesture(_:))) + longTapRecognizer.minimumPressDuration = 0.3 + self.view.addGestureRecognizer(longTapRecognizer) + } + + @objc func longTapGesture(_ recognizer: UILongPressGestureRecognizer) { + if case .began = recognizer.state { + self.longTapAction?() + } } @objc func tapGesture(_ recognizer: UITapGestureRecognizer) { diff --git a/TelegramUI/SelectivePrivacySettingsPeersController.swift b/TelegramUI/SelectivePrivacySettingsPeersController.swift index c6efa6f07b..d8f4f393e0 100644 --- a/TelegramUI/SelectivePrivacySettingsPeersController.swift +++ b/TelegramUI/SelectivePrivacySettingsPeersController.swift @@ -135,7 +135,7 @@ private enum SelectivePrivacyPeersEntry: ItemListNodeEntry { arguments.removePeer(peerId) }) case let .addItem(theme, text, editing): - return ItemListPeerActionItem(theme: theme, icon: PresentationResourcesItemList.plusIconImage(theme), title: text, sectionId: self.section, editing: editing, action: { + return ItemListPeerActionItem(theme: theme, icon: PresentationResourcesItemList.addPersonIcon(theme), title: text, sectionId: self.section, editing: editing, action: { arguments.addPeer() }) } diff --git a/TelegramUI/SettingsController.swift b/TelegramUI/SettingsController.swift index 8256d78755..ea7233783d 100644 --- a/TelegramUI/SettingsController.swift +++ b/TelegramUI/SettingsController.swift @@ -5,6 +5,21 @@ import Postbox import TelegramCore import LegacyComponents +private final class SettingsItemIcons { + static let savedMessages = UIImage(bundleImageName: "Settings/MenuIcons/SavedMessages")?.precomposed() + static let recentCalls = UIImage(bundleImageName: "Settings/MenuIcons/RecentCalls")?.precomposed() + static let stickers = UIImage(bundleImageName: "Settings/MenuIcons/Stickers")?.precomposed() + + static let notifications = UIImage(bundleImageName: "Settings/MenuIcons/Notifications")?.precomposed() + static let security = UIImage(bundleImageName: "Settings/MenuIcons/Security")?.precomposed() + static let dataAndStorage = UIImage(bundleImageName: "Settings/MenuIcons/DataAndStorage")?.precomposed() + static let appearance = UIImage(bundleImageName: "Settings/MenuIcons/Appearance")?.precomposed() + static let language = UIImage(bundleImageName: "Settings/MenuIcons/Language")?.precomposed() + + static let support = UIImage(bundleImageName: "Settings/MenuIcons/Support")?.precomposed() + static let faq = UIImage(bundleImageName: "Settings/MenuIcons/Faq")?.precomposed() +} + private struct SettingsItemArguments { let account: Account let accountManager: AccountManager @@ -13,57 +28,59 @@ private struct SettingsItemArguments { let avatarTapAction: () -> Void let changeProfilePhoto: () -> Void + let openUsername: () -> Void + let openSavedMessages: () -> Void + let openRecentCalls: () -> Void let openPrivacyAndSecurity: () -> Void let openDataAndStorage: () -> Void let openThemes: () -> Void - let openTheme: (TelegramWallpaper) -> Void let pushController: (ViewController) -> Void let presentController: (ViewController) -> Void - let updateEditingName: (ItemListAvatarAndNameInfoItemName) -> Void - let saveEditingState: () -> Void let openLanguage: () -> Void let openSupport: () -> Void let openFaq: () -> Void - let logout: () -> Void + let openEditing: () -> Void } private enum SettingsSection: Int32 { case info + case media case generalSettings - case accountSettings case help - case logOut + case debug } private enum SettingsEntry: ItemListNodeEntry { - case userInfo(PresentationTheme, PresentationStrings, Peer?, CachedPeerData?, ItemListAvatarAndNameInfoItemState, TelegramMediaImageRepresentation?) + case userInfo(PresentationTheme, PresentationStrings, Peer?, CachedPeerData?, ItemListAvatarAndNameInfoItemState, ItemListAvatarAndNameInfoItemUpdatingAvatar?) case setProfilePhoto(PresentationTheme, String) + case setUsername(PresentationTheme, String) - case notificationsAndSounds(PresentationTheme, String) - case privacyAndSecurity(PresentationTheme, String) - case dataAndStorage(PresentationTheme, String) - case stickers(PresentationTheme, String) - case themes(PresentationTheme, String, [TelegramWallpaper]) - case phoneNumber(PresentationTheme, String, String) - case username(PresentationTheme, String, String) - case language(PresentationTheme, String, String) - case askAQuestion(PresentationTheme, String, Bool) - case faq(PresentationTheme, String) + case savedMessages(PresentationTheme, UIImage?, String) + case recentCalls(PresentationTheme, UIImage?, String) + case stickers(PresentationTheme, UIImage?, String) + + case notificationsAndSounds(PresentationTheme, UIImage?, String) + case privacyAndSecurity(PresentationTheme, UIImage?, String) + case dataAndStorage(PresentationTheme, UIImage?, String) + case themes(PresentationTheme, UIImage?, String) + case language(PresentationTheme, UIImage?, String, String) + + case askAQuestion(PresentationTheme, UIImage?, String) + case faq(PresentationTheme, UIImage?, String) case debug(PresentationTheme, String) - case logOut(PresentationTheme, String) var section: ItemListSectionId { switch self { - case .userInfo, .setProfilePhoto: + case .userInfo, .setProfilePhoto, .setUsername: return SettingsSection.info.rawValue - case .notificationsAndSounds, .privacyAndSecurity, .dataAndStorage, .stickers, .themes: + case .savedMessages, .recentCalls, .stickers: + return SettingsSection.media.rawValue + case .notificationsAndSounds, .privacyAndSecurity, .dataAndStorage, .themes, .language: return SettingsSection.generalSettings.rawValue - case .phoneNumber, .username: - return SettingsSection.accountSettings.rawValue - case .language, .askAQuestion, .faq, .debug: + case .askAQuestion, .faq: return SettingsSection.help.rawValue - case .logOut: - return SettingsSection.logOut.rawValue + case .debug: + return SettingsSection.debug.rawValue } } @@ -73,29 +90,29 @@ private enum SettingsEntry: ItemListNodeEntry { return 0 case .setProfilePhoto: return 1 - case .notificationsAndSounds: + case .setUsername: return 2 - case .privacyAndSecurity: + case .savedMessages: return 3 - case .dataAndStorage: + case .recentCalls: return 4 case .stickers: return 5 - case .themes: + case .notificationsAndSounds: return 6 - case .phoneNumber: + case .privacyAndSecurity: return 7 - case .username: + case .dataAndStorage: return 8 - case .askAQuestion: + case .themes: return 9 case .language: return 10 - case .faq: + case .askAQuestion: return 11 - case .debug: + case .faq: return 12 - case .logOut: + case .debug: return 13 } } @@ -140,62 +157,68 @@ private enum SettingsEntry: ItemListNodeEntry { } else { return false } - case let .notificationsAndSounds(lhsTheme, lhsText): - if case let .notificationsAndSounds(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + case let .setUsername(lhsTheme, lhsText): + if case let .setUsername(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .privacyAndSecurity(lhsTheme, lhsText): - if case let .privacyAndSecurity(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + case let .savedMessages(lhsTheme, lhsImage, lhsText): + if case let .savedMessages(rhsTheme, rhsImage, rhsText) = rhs, lhsTheme === rhsTheme, lhsImage === rhsImage, lhsText == rhsText { return true } else { return false } - case let .dataAndStorage(lhsTheme, lhsText): - if case let .dataAndStorage(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + case let .recentCalls(lhsTheme, lhsImage, lhsText): + if case let .recentCalls(rhsTheme, rhsImage, rhsText) = rhs, lhsTheme === rhsTheme, lhsImage === rhsImage, lhsText == rhsText { return true } else { return false } - case let .stickers(lhsTheme, lhsText): - if case let .stickers(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + case let .stickers(lhsTheme, lhsImage, lhsText): + if case let .stickers(rhsTheme, rhsImage, rhsText) = rhs, lhsTheme === rhsTheme, lhsImage === rhsImage, lhsText == rhsText { return true } else { return false } - case let .themes(lhsTheme, lhsText, lhsWallpapers): - if case let .themes(rhsTheme, rhsText, rhsWallpapers) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsWallpapers == rhsWallpapers { + case let .notificationsAndSounds(lhsTheme, lhsImage, lhsText): + if case let .notificationsAndSounds(rhsTheme, rhsImage, rhsText) = rhs, lhsTheme === rhsTheme, lhsImage === rhsImage, lhsText == rhsText { return true } else { return false } - case let .phoneNumber(lhsTheme, lhsText, lhsNumber): - if case let .phoneNumber(rhsTheme, rhsText, rhsNumber) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsNumber == rhsNumber { + case let .privacyAndSecurity(lhsTheme, lhsImage, lhsText): + if case let .privacyAndSecurity(rhsTheme, rhsImage, rhsText) = rhs, lhsTheme === rhsTheme, lhsImage === rhsImage, lhsText == rhsText { return true } else { return false } - case let .username(lhsTheme, lhsText, lhsAddress): - if case let .username(rhsTheme, rhsText, rhsAddress) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsAddress == rhsAddress { + case let .dataAndStorage(lhsTheme, lhsImage, lhsText): + if case let .dataAndStorage(rhsTheme, rhsImage, rhsText) = rhs, lhsTheme === rhsTheme, lhsImage === rhsImage, lhsText == rhsText { return true } else { return false } - case let .language(lhsTheme, lhsText, lhsValue): - if case let .language(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + case let .themes(lhsTheme, lhsImage, lhsText): + if case let .themes(rhsTheme, rhsImage, rhsText) = rhs, lhsTheme === rhsTheme, lhsImage === rhsImage, lhsText == rhsText { return true } else { return false } - case let .askAQuestion(lhsTheme, lhsText, lhsLoading): - if case let .askAQuestion(rhsTheme, rhsText, rhsLoading) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsLoading == rhsLoading { + case let .language(lhsTheme, lhsImage, lhsText, lhsValue): + if case let .language(rhsTheme, rhsImage, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsImage === rhsImage, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } - case let .faq(lhsTheme, lhsText): - if case let .faq(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + case let .askAQuestion(lhsTheme, lhsImage, lhsText): + if case let .askAQuestion(rhsTheme, rhsImage, rhsText) = rhs, lhsTheme === rhsTheme, lhsImage === rhsImage, lhsText == rhsText { + return true + } else { + return false + } + case let .faq(lhsTheme, lhsImage, lhsText): + if case let .faq(rhsTheme, rhsImage, rhsText) = rhs, lhsTheme === rhsTheme, lhsImage === rhsImage, lhsText == rhsText { return true } else { return false @@ -206,12 +229,6 @@ private enum SettingsEntry: ItemListNodeEntry { } else { return false } - case let .logOut(lhsTheme, lhsText): - if case let .logOut(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { - return true - } else { - return false - } } } @@ -222,146 +239,115 @@ private enum SettingsEntry: ItemListNodeEntry { func item(_ arguments: SettingsItemArguments) -> ListViewItem { switch self { case let .userInfo(theme, strings, peer, cachedData, state, updatingImage): - return ItemListAvatarAndNameInfoItem(account: arguments.account, theme: theme, strings: strings, peer: peer, presence: TelegramUserPresence(status: .present(until: Int32.max)), cachedData: cachedData, state: state, sectionId: ItemListSectionId(self.section), style: .blocks(withTopInset: false), editingNameUpdated: { editingName in - arguments.updateEditingName(editingName) + return ItemListAvatarAndNameInfoItem(account: arguments.account, theme: theme, strings: strings, mode: .settings, peer: peer, presence: TelegramUserPresence(status: .present(until: Int32.max)), cachedData: cachedData, state: state, sectionId: ItemListSectionId(self.section), style: .blocks(withTopInset: false), editingNameUpdated: { _ in }, avatarTapped: { arguments.avatarTapAction() - }, context: arguments.avatarAndNameInfoContext, updatingImage: updatingImage) + }, context: arguments.avatarAndNameInfoContext, updatingImage: updatingImage, action: { + arguments.openEditing() + }) case let .setProfilePhoto(theme, text): return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: ItemListSectionId(self.section), style: .blocks, action: { arguments.changeProfilePhoto() }) - case let .notificationsAndSounds(theme, text): - return ItemListDisclosureItem(theme: theme, title: text, label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { - arguments.pushController(notificationsAndSoundsController(account: arguments.account)) + case let .setUsername(theme, text): + return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: ItemListSectionId(self.section), style: .blocks, action: { + arguments.openUsername() }) - case let .privacyAndSecurity(theme, text): - return ItemListDisclosureItem(theme: theme, title: text, label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { - arguments.openPrivacyAndSecurity() + case let .savedMessages(theme, image, text): + return ItemListDisclosureItem(theme: theme, icon: image, title: text, label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { + arguments.openSavedMessages() }) - case let .dataAndStorage(theme, text): - return ItemListDisclosureItem(theme: theme, title: text, label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { - arguments.openDataAndStorage() + case let .recentCalls(theme, image, text): + return ItemListDisclosureItem(theme: theme, icon: image, title: text, label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { + arguments.openRecentCalls() }) - case let .stickers(theme, text): - return ItemListDisclosureItem(theme: theme, title: text, label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { + case let .stickers(theme, image, text): + return ItemListDisclosureItem(theme: theme, icon: image, title: text, label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { arguments.pushController(installedStickerPacksController(account: arguments.account, mode: .general)) }) - case let .themes(theme, text, wallpapers): - return SettingsThemesItem(account: arguments.account, theme: theme, title: text, sectionId: self.section, action: { + case let .notificationsAndSounds(theme, image, text): + return ItemListDisclosureItem(theme: theme, icon: image, title: text, label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { + arguments.pushController(notificationsAndSoundsController(account: arguments.account)) + }) + case let .privacyAndSecurity(theme, image, text): + return ItemListDisclosureItem(theme: theme, icon: image, title: text, label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { + arguments.openPrivacyAndSecurity() + }) + case let .dataAndStorage(theme, image, text): + return ItemListDisclosureItem(theme: theme, icon: image, title: text, label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { + arguments.openDataAndStorage() + }) + case let .themes(theme, image, text): + return ItemListDisclosureItem(theme: theme, icon: image, title: text, label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { arguments.openThemes() - }, openWallpaper: { wallpaper in - arguments.openTheme(wallpaper) - }, wallpapers: wallpapers) - case let .phoneNumber(theme, text, number): - return ItemListDisclosureItem(theme: theme, title: text, label: number, sectionId: ItemListSectionId(self.section), style: .blocks, action: { - arguments.pushController(ChangePhoneNumberIntroController(account: arguments.account, phoneNumber: number)) }) - case let .username(theme, text, address): - return ItemListDisclosureItem(theme: theme, title: text, label: address, sectionId: ItemListSectionId(self.section), style: .blocks, action: { - arguments.presentController(usernameSetupController(account: arguments.account)) - }) - case let .language(theme, text, value): - return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: ItemListSectionId(self.section), style: .blocks, action: { + case let .language(theme, image, text, value): + return ItemListDisclosureItem(theme: theme, icon: image, title: text, label: value, sectionId: ItemListSectionId(self.section), style: .blocks, action: { arguments.openLanguage() }) - case let .askAQuestion(theme, text, loading): - return ItemListDisclosureItem(theme: theme, title: text, label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { + case let .askAQuestion(theme, image, text): + return ItemListDisclosureItem(theme: theme, icon: image, title: text, label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { arguments.openSupport() }) - case let .faq(theme, text): - return ItemListDisclosureItem(theme: theme, title: text, label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { + case let .faq(theme, image, text): + return ItemListDisclosureItem(theme: theme, icon: image, title: text, label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { arguments.openFaq() }) case let .debug(theme, text): return ItemListDisclosureItem(theme: theme, title: text, label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { arguments.pushController(debugController(account: arguments.account, accountManager: arguments.accountManager)) }) - case let .logOut(theme, text): - return ItemListActionItem(theme: theme, title: text, kind: .destructive, alignment: .center, sectionId: ItemListSectionId(self.section), style: .blocks, action: { - arguments.logout() - }) } } } -private struct SettingsEditingState: Equatable { - let editingName: ItemListAvatarAndNameInfoItemName - - static func ==(lhs: SettingsEditingState, rhs: SettingsEditingState) -> Bool { - if lhs.editingName != rhs.editingName { - return false - } - - return true - } -} - private struct SettingsState: Equatable { - let updatingAvatar: TelegramMediaImageRepresentation? - let editingState: SettingsEditingState? - let updatingName: ItemListAvatarAndNameInfoItemName? - let loadingSupportPeer: Bool + let updatingAvatar: ItemListAvatarAndNameInfoItemUpdatingAvatar? - func withUpdatedUpdatingAvatar(_ updatingAvatar: TelegramMediaImageRepresentation?) -> SettingsState { - return SettingsState(updatingAvatar: updatingAvatar, editingState: editingState, updatingName: self.updatingName, loadingSupportPeer: self.loadingSupportPeer) + init(updatingAvatar: ItemListAvatarAndNameInfoItemUpdatingAvatar? = nil) { + self.updatingAvatar = updatingAvatar } - func withUpdatedEditingState(_ editingState: SettingsEditingState?) -> SettingsState { - return SettingsState(updatingAvatar: self.updatingAvatar, editingState: editingState, updatingName: self.updatingName, loadingSupportPeer: self.loadingSupportPeer) - } - - func withUpdatedUpdatingName(_ updatingName: ItemListAvatarAndNameInfoItemName?) -> SettingsState { - return SettingsState(updatingAvatar: self.updatingAvatar, editingState: self.editingState, updatingName: updatingName, loadingSupportPeer: self.loadingSupportPeer) - } - - func withUpdatedLoadingSupportPeer(_ loadingSupportPeer: Bool) -> SettingsState { - return SettingsState(updatingAvatar: self.updatingAvatar, editingState: self.editingState, updatingName: self.updatingName, loadingSupportPeer: loadingSupportPeer) + func withUpdatedUpdatingAvatar(_ updatingAvatar: ItemListAvatarAndNameInfoItemUpdatingAvatar?) -> SettingsState { + return SettingsState(updatingAvatar: updatingAvatar) } static func ==(lhs: SettingsState, rhs: SettingsState) -> Bool { if lhs.updatingAvatar != rhs.updatingAvatar { return false } - if lhs.editingState != rhs.editingState { - return false - } - if lhs.updatingName != rhs.updatingName { - return false - } - if lhs.loadingSupportPeer != rhs.loadingSupportPeer { - return false - } return true } } -private func settingsEntries(presentationData: PresentationData, state: SettingsState, view: PeerView, wallpapers: [TelegramWallpaper]) -> [SettingsEntry] { +private func settingsEntries(presentationData: PresentationData, state: SettingsState, view: PeerView) -> [SettingsEntry] { var entries: [SettingsEntry] = [] if let peer = peerViewMainPeer(view) as? TelegramUser { - let userInfoState = ItemListAvatarAndNameInfoItemState(editingName: state.editingState?.editingName, updatingName: state.updatingName) + let userInfoState = ItemListAvatarAndNameInfoItemState(editingName: nil, updatingName: nil) entries.append(.userInfo(presentationData.theme, presentationData.strings, peer, view.cachedData, userInfoState, state.updatingAvatar)) - entries.append(.setProfilePhoto(presentationData.theme, presentationData.strings.Settings_SetProfilePhoto)) - - entries.append(.notificationsAndSounds(presentationData.theme, presentationData.strings.Settings_NotificationsAndSounds)) - entries.append(.privacyAndSecurity(presentationData.theme, presentationData.strings.Settings_PrivacySettings)) - entries.append(.dataAndStorage(presentationData.theme, presentationData.strings.Settings_ChatSettings)) - entries.append(.stickers(presentationData.theme, presentationData.strings.ChatSettings_Stickers)) - entries.append(.themes(presentationData.theme, presentationData.strings.Settings_ChatBackground, wallpapers)) - - if let phone = peer.phone { - entries.append(.phoneNumber(presentationData.theme, presentationData.strings.Settings_PhoneNumber, formatPhoneNumber(phone))) + if peer.photo.isEmpty { + entries.append(.setProfilePhoto(presentationData.theme, presentationData.strings.Settings_SetProfilePhoto)) + } + if peer.addressName == nil { + entries.append(.setUsername(presentationData.theme, presentationData.strings.Settings_SetUsername)) } - entries.append(.username(presentationData.theme, presentationData.strings.Settings_Username, peer.addressName == nil ? "" : ("@" + peer.addressName!))) - entries.append(.askAQuestion(presentationData.theme, presentationData.strings.Settings_Support, state.loadingSupportPeer)) - entries.append(.language(presentationData.theme, presentationData.strings.Settings_AppLanguage, presentationData.strings.Localization_LanguageName)) - entries.append(.faq(presentationData.theme, presentationData.strings.Settings_FAQ)) - entries.append(.debug(presentationData.theme, "Debug")) + entries.append(.savedMessages(presentationData.theme, SettingsItemIcons.savedMessages, presentationData.strings.Settings_SavedMessages)) + entries.append(.recentCalls(presentationData.theme, SettingsItemIcons.recentCalls, presentationData.strings.CallSettings_RecentCalls)) + entries.append(.stickers(presentationData.theme, SettingsItemIcons.stickers, presentationData.strings.ChatSettings_Stickers)) - if let _ = state.editingState { - entries.append(.logOut(presentationData.theme, presentationData.strings.Settings_Logout)) + entries.append(.notificationsAndSounds(presentationData.theme, SettingsItemIcons.notifications, presentationData.strings.Settings_NotificationsAndSounds)) + entries.append(.privacyAndSecurity(presentationData.theme, SettingsItemIcons.security, presentationData.strings.Settings_PrivacySettings)) + entries.append(.dataAndStorage(presentationData.theme, SettingsItemIcons.dataAndStorage, presentationData.strings.Settings_ChatSettings)) + entries.append(.themes(presentationData.theme, SettingsItemIcons.appearance, presentationData.strings.ChatSettings_Appearance.lowercased().capitalized)) + entries.append(.language(presentationData.theme, SettingsItemIcons.language, presentationData.strings.Settings_AppLanguage, presentationData.strings.Localization_LanguageName)) + + entries.append(.askAQuestion(presentationData.theme, SettingsItemIcons.support, presentationData.strings.Settings_Support)) + entries.append(.faq(presentationData.theme, SettingsItemIcons.faq, presentationData.strings.Settings_FAQ)) + + if !GlobalExperimentalSettings.isAppStoreBuild { + entries.append(.debug(presentationData.theme, "Debug")) } } @@ -369,8 +355,8 @@ private func settingsEntries(presentationData: PresentationData, state: Settings } public func settingsController(account: Account, accountManager: AccountManager) -> ViewController { - let statePromise = ValuePromise(SettingsState(updatingAvatar: nil, editingState: nil, updatingName: nil, loadingSupportPeer: false), ignoreRepeated: true) - let stateValue = Atomic(value: SettingsState(updatingAvatar: nil, editingState: nil, updatingName: nil, loadingSupportPeer: false)) + let statePromise = ValuePromise(SettingsState(), ignoreRepeated: true) + let stateValue = Atomic(value: SettingsState()) let updateState: ((SettingsState) -> SettingsState) -> Void = { f in statePromise.set(stateValue.modify { f($0) }) } @@ -397,9 +383,8 @@ public func settingsController(account: Account, accountManager: AccountManager) var avatarGalleryTransitionArguments: ((AvatarGalleryEntry) -> GalleryTransitionArguments?)? let avatarAndNameInfoContext = ItemListAvatarAndNameInfoItemContext() var updateHiddenAvatarImpl: (() -> Void)? - - let wallpapersPromise = Promise<[TelegramWallpaper]>() - wallpapersPromise.set(telegramWallpapers(account: account)) + var changeProfilePhotoImpl: (() -> Void)? + var openSavedMessagesImpl: (() -> Void)? let arguments = SettingsItemArguments(account: account, accountManager: accountManager, avatarAndNameInfoContext: avatarAndNameInfoContext, avatarTapAction: { var updating = false @@ -413,171 +398,163 @@ public func settingsController(account: Account, accountManager: AccountManager) } let _ = (account.postbox.loadedPeerWithId(account.peerId) |> take(1) |> deliverOnMainQueue).start(next: { peer in - let galleryController = AvatarGalleryController(account: account, peer: peer, replaceRootController: { controller, ready in - - }) - hiddenAvatarRepresentationDisposable.set((galleryController.hiddenMedia |> deliverOnMainQueue).start(next: { entry in - avatarAndNameInfoContext.hiddenAvatarRepresentation = entry?.representations.first - updateHiddenAvatarImpl?() - })) - presentControllerImpl?(galleryController, AvatarGalleryControllerPresentationArguments(transitionArguments: { entry in - return avatarGalleryTransitionArguments?(entry) - })) + if peer.smallProfileImage != nil { + let galleryController = AvatarGalleryController(account: account, peer: peer, replaceRootController: { controller, ready in + + }) + hiddenAvatarRepresentationDisposable.set((galleryController.hiddenMedia |> deliverOnMainQueue).start(next: { entry in + avatarAndNameInfoContext.hiddenAvatarRepresentation = entry?.representations.first + updateHiddenAvatarImpl?() + })) + presentControllerImpl?(galleryController, AvatarGalleryControllerPresentationArguments(transitionArguments: { entry in + return avatarGalleryTransitionArguments?(entry) + })) + } else { + changeProfilePhotoImpl?() + } }) }, changeProfilePhoto: { - let legacyController = LegacyController(presentation: .custom) - legacyController.statusBar.statusBarStyle = .Ignore - - let emptyController = LegacyEmptyController(context: legacyController.context)! - let navigationController = makeLegacyNavigationController(rootController: emptyController) - navigationController.setNavigationBarHidden(true, animated: false) - navigationController.navigationBar.transform = CGAffineTransform(translationX: -1000.0, y: 0.0) - - legacyController.bind(controller: navigationController) - - presentControllerImpl?(legacyController, nil) - - let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasDeleteButton: false, personalPhoto: true, saveEditedPhotos: false, saveCapturedMedia: false)! - let _ = currentAvatarMixin.swap(mixin) - mixin.didFinishWithImage = { image in - if let image = image { - if let data = UIImageJPEGRepresentation(image, 0.6) { - let resource = LocalFileMediaResource(fileId: arc4random64()) - account.postbox.mediaBox.storeResourceData(resource.id, data: data) - let representation = TelegramMediaImageRepresentation(dimensions: CGSize(width: 640.0, height: 640.0), resource: resource) - updateState { - $0.withUpdatedUpdatingAvatar(representation) - } - updateAvatarDisposable.set((updateAccountPhoto(account: account, resource: resource) |> deliverOnMainQueue).start(next: { result in - switch result { - case .complete: - updateState { - $0.withUpdatedUpdatingAvatar(nil) - } - case .progress: - break - } - })) - } - } - } - mixin.didDismiss = { [weak legacyController] in - let _ = currentAvatarMixin.swap(nil) - legacyController?.dismiss() - } - let menuController = mixin.present() - if let menuController = menuController { - menuController.customRemoveFromParentViewController = { [weak legacyController] in - legacyController?.dismiss() - } - } + changeProfilePhotoImpl?() + }, openUsername: { + presentControllerImpl?(usernameSetupController(account: account), nil) + }, openSavedMessages: { + openSavedMessagesImpl?() + }, openRecentCalls: { + pushControllerImpl?(CallListController(account: account, mode: .navigation)) }, openPrivacyAndSecurity: { pushControllerImpl?(privacyAndSecurityController(account: account, initialSettings: .single(nil) |> then(requestAccountPrivacySettings(account: account) |> map { Optional($0) }))) }, openDataAndStorage: { pushControllerImpl?(dataAndStorageController(account: account)) }, openThemes: { - pushControllerImpl?(ThemeGridController(account: account)) - }, openTheme: { wallpaper in - let _ = (wallpapersPromise.get() |> take(1) |> deliverOnMainQueue).start(next: { wallpapers in - let controller = ThemeGalleryController(account: account, wallpapers: wallpapers, at: wallpaper) - presentControllerImpl?(controller, ThemePreviewControllerPresentationArguments(transitionArguments: { entry -> GalleryTransitionArguments? in - return nil - })) - }) + pushControllerImpl?(themeSettingsController(account: account)) }, pushController: { controller in pushControllerImpl?(controller) }, presentController: { controller in presentControllerImpl?(controller, nil) - }, updateEditingName: { editingName in - updateState { state in - if let _ = state.editingState { - return state.withUpdatedEditingState(SettingsEditingState(editingName: editingName)) - } else { - return state - } - } - }, saveEditingState: { - var updateName: ItemListAvatarAndNameInfoItemName? - updateState { state in - if let editingState = state.editingState { - updateName = editingState.editingName - return state.withUpdatedEditingState(nil).withUpdatedUpdatingName(editingState.editingName) - } else { - return state - } - } - if let updateName = updateName, case let .personName(firstName, lastName) = updateName { - updatePeerNameDisposable.set((updateAccountPeerName(account: account, firstName: firstName, lastName: lastName) |> afterDisposed { - Queue.mainQueue().async { - updateState { state in - return state.withUpdatedUpdatingName(nil) - } - } - }).start()) - } }, openLanguage: { let controller = LanguageSelectionController(account: account) presentControllerImpl?(controller, nil) }, openSupport: { - var load = false - updateState { state in - if !state.loadingSupportPeer { - load = true + supportPeerDisposable.set((supportPeerId(account: account) |> deliverOnMainQueue).start(next: { peerId in + if let peerId = peerId { + pushControllerImpl?(ChatController(account: account, chatLocation: .peer(peerId))) } - return state.withUpdatedLoadingSupportPeer(true) - } - if load { - supportPeerDisposable.set((supportPeerId(account: account) |> deliverOnMainQueue).start(next: { peerId in - updateState { state in - return state.withUpdatedLoadingSupportPeer(false) - } - if let peerId = peerId { - pushControllerImpl?(ChatController(account: account, peerId: peerId)) - } - })) - } + })) }, openFaq: { - var faqUrl = NSLocalizedString("Settings.FAQ_URL", comment: "") - if faqUrl == "Settings.FAQ_URL" { + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + var faqUrl = presentationData.strings.Settings_FAQ_URL + if faqUrl == "Settings.FAQ_URL" || faqUrl.isEmpty { faqUrl = "http://telegram.org/faq#general" } if let applicationContext = account.applicationContext as? TelegramApplicationContext { applicationContext.applicationBindings.openUrl(faqUrl) } - }, logout: { - let alertController = standardTextAlertController(title: NSLocalizedString("Settings.LogoutConfirmationTitle", comment: ""), text: NSLocalizedString("Settings.LogoutConfirmationText", comment: ""), actions: [ - TextAlertAction(type: .genericAction, title: "Cancel", action: { - }), - TextAlertAction(type: .defaultAction, title: "OK", action: { - let _ = logoutFromAccount(id: account.id, accountManager: accountManager).start() - }) - ]) - presentControllerImpl?(alertController, nil) + }, openEditing: { + let _ = (account.postbox.modify { modifier -> (Peer?, CachedPeerData?) in + return (modifier.getPeer(account.peerId), modifier.getPeerCachedData(peerId: account.peerId)) + } |> deliverOnMainQueue).start(next: { peer, cachedData in + if let peer = peer as? TelegramUser, let cachedData = cachedData as? CachedUserData { + pushControllerImpl?(editSettingsController(account: account, currentName: .personName(firstName: peer.firstName ?? "", lastName: peer.lastName ?? ""), currentBioText: cachedData.about ?? "", accountManager: accountManager)) + } + }) }) + changeProfilePhotoImpl = { + let _ = (account.postbox.modify { modifier -> Peer? in + return modifier.getPeer(account.peerId) + } |> deliverOnMainQueue).start(next: { peer in + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + + let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme) + legacyController.statusBar.statusBarStyle = .Ignore + + let emptyController = LegacyEmptyController(context: legacyController.context)! + let navigationController = makeLegacyNavigationController(rootController: emptyController) + navigationController.setNavigationBarHidden(true, animated: false) + navigationController.navigationBar.transform = CGAffineTransform(translationX: -1000.0, y: 0.0) + + legacyController.bind(controller: navigationController) + + presentControllerImpl?(legacyController, nil) + + let theme = (account.telegramApplicationContext.currentPresentationData.with { $0 }).theme + + var hasPhotos = false + if let peer = peer, !peer.profileImageRepresentations.isEmpty { + hasPhotos = true + } + + let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasDeleteButton: hasPhotos, personalPhoto: true, saveEditedPhotos: false, saveCapturedMedia: false)! + let _ = currentAvatarMixin.swap(mixin) + mixin.didFinishWithImage = { image in + if let image = image { + if let data = UIImageJPEGRepresentation(image, 0.6) { + let resource = LocalFileMediaResource(fileId: arc4random64()) + account.postbox.mediaBox.storeResourceData(resource.id, data: data) + let representation = TelegramMediaImageRepresentation(dimensions: CGSize(width: 640.0, height: 640.0), resource: resource) + updateState { + $0.withUpdatedUpdatingAvatar(.image(representation)) + } + updateAvatarDisposable.set((updateAccountPhoto(account: account, resource: resource) |> deliverOnMainQueue).start(next: { result in + switch result { + case .complete: + updateState { + $0.withUpdatedUpdatingAvatar(nil) + } + case .progress: + break + } + })) + } + } + } + mixin.didFinishWithDelete = { + let _ = currentAvatarMixin.swap(nil) + updateState { + if let profileImage = peer?.smallProfileImage { + return $0.withUpdatedUpdatingAvatar(.image(profileImage)) + } else { + return $0.withUpdatedUpdatingAvatar(.none) + } + } + updateAvatarDisposable.set((updateAccountPhoto(account: account, resource: nil) |> deliverOnMainQueue).start(next: { result in + switch result { + case .complete: + updateState { + $0.withUpdatedUpdatingAvatar(nil) + } + case .progress: + break + } + })) + } + mixin.didDismiss = { [weak legacyController] in + let _ = currentAvatarMixin.swap(nil) + legacyController?.dismiss() + } + let menuController = mixin.present() + if let menuController = menuController { + menuController.customRemoveFromParentViewController = { [weak legacyController] in + legacyController?.dismiss() + } + } + }) + } + let peerView = account.viewTracker.peerView(account.peerId) - let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get(), peerView, wallpapersPromise.get()) - |> map { presentationData, state, view, wallpapers -> (ItemListControllerState, (ItemListNodeState, SettingsEntry.ItemGenerationArguments)) in + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get(), peerView) + |> map { presentationData, state, view -> (ItemListControllerState, (ItemListNodeState, SettingsEntry.ItemGenerationArguments)) in let peer = peerViewMainPeer(view) - let rightNavigationButton: ItemListNavigationButton - if let _ = state.editingState { - rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Done, style: .bold, enabled: true, action: { - arguments.saveEditingState() - }) - } else { - rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Edit, style: .regular, enabled: true, action: { - if let peer = peer as? TelegramUser { - updateState { state in - return state.withUpdatedEditingState(SettingsEditingState(editingName: ItemListAvatarAndNameInfoItemName(peer.indexName))) - } - } - }) - } + let rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Edit, style: .regular, enabled: true, action: { + if let peer = peer as? TelegramUser, let cachedData = view.cachedData as? CachedUserData { + arguments.openEditing() + } + }) let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.Settings_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) - let listState = ItemListNodeState(entries: settingsEntries(presentationData: presentationData, state: state, view: view, wallpapers: wallpapers), style: .blocks) + let listState = ItemListNodeState(entries: settingsEntries(presentationData: presentationData, state: state, view: view), style: .blocks) return (controllerState, (listState, arguments)) } |> afterDisposed { @@ -617,5 +594,11 @@ public func settingsController(account: Account, accountManager: AccountManager) } } } + openSavedMessagesImpl = { [weak controller] in + if let controller = controller, let navigationController = controller.navigationController as? NavigationController { + navigateToChatController(navigationController: navigationController, account: account, chatLocation: .peer(account.peerId)) + } + } return controller } + diff --git a/TelegramUI/SettingsThemeWallpaperNode.swift b/TelegramUI/SettingsThemeWallpaperNode.swift index 26d89979f1..4487e572ef 100644 --- a/TelegramUI/SettingsThemeWallpaperNode.swift +++ b/TelegramUI/SettingsThemeWallpaperNode.swift @@ -15,6 +15,7 @@ final class SettingsThemeWallpaperNode: ASDisplayNode { var pressed: (() -> Void)? override init() { + self.imageNode.contentAnimations = [.subsequentUpdates] super.init() self.addSubnode(self.backgroundNode) @@ -35,7 +36,7 @@ final class SettingsThemeWallpaperNode: ASDisplayNode { case .builtin: self.imageNode.isHidden = false self.backgroundNode.isHidden = true - self.imageNode.setSignal(account: account, signal: settingsBuiltinWallpaperImage(account: account)) + self.imageNode.setSignal(settingsBuiltinWallpaperImage(account: account)) let apply = self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: CGSize(), boundingSize: size, intrinsicInsets: UIEdgeInsets())) apply() case let .color(color): @@ -45,7 +46,7 @@ final class SettingsThemeWallpaperNode: ASDisplayNode { case let .image(representations): self.imageNode.isHidden = false self.backgroundNode.isHidden = true - self.imageNode.setSignal(account: account, signal: chatAvatarGalleryPhoto(account: account, representations: representations, autoFetchFullSize: true)) + self.imageNode.setSignal(chatAvatarGalleryPhoto(account: account, representations: representations, autoFetchFullSize: true)) let apply = self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: largestImageRepresentation(representations)!.dimensions.aspectFilled(size), boundingSize: size, intrinsicInsets: UIEdgeInsets())) apply() } diff --git a/TelegramUI/SettingsThemesItem.swift b/TelegramUI/SettingsThemesItem.swift deleted file mode 100644 index ec48913ce6..0000000000 --- a/TelegramUI/SettingsThemesItem.swift +++ /dev/null @@ -1,321 +0,0 @@ -import Foundation -import Display -import AsyncDisplayKit -import SwiftSignalKit -import TelegramCore - - - -class SettingsThemesItem: ListViewItem, ItemListItem { - let account: Account - let theme: PresentationTheme - let title: String - let sectionId: ItemListSectionId - let action: () -> Void - let openWallpaper: (TelegramWallpaper) -> Void - let wallpapers: [TelegramWallpaper] - - init(account: Account, theme: PresentationTheme, title: String, sectionId: ItemListSectionId, action: @escaping () -> Void, openWallpaper: @escaping (TelegramWallpaper) -> Void, wallpapers: [TelegramWallpaper]) { - self.account = account - self.theme = theme - self.title = title - self.sectionId = sectionId - self.action = action - self.openWallpaper = openWallpaper - self.wallpapers = wallpapers - } - - func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { - async { - let node = SettingsThemesItemNode() - let (layout, apply) = node.asyncLayout()(self, width, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) - - node.contentSize = layout.contentSize - node.insets = layout.insets - - completion(node, { - return (nil, { apply() }) - }) - } - } - - func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { - if let node = node as? SettingsThemesItemNode { - Queue.mainQueue().async { - let makeLayout = node.asyncLayout() - - async { - let (layout, apply) = makeLayout(self, width, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) - Queue.mainQueue().async { - completion(layout, { - apply() - }) - } - } - } - } - } - - var selectable: Bool = true - - func selected(listView: ListView){ - listView.clearHighlightAnimated(true) - self.action() - } -} - -private let titleFont = Font.regular(17.0) - -class SettingsThemesItemNode: ListViewItemNode { - private let backgroundNode: ASDisplayNode - private let topStripeNode: ASDisplayNode - private let bottomStripeNode: ASDisplayNode - private let highlightedBackgroundNode: ASDisplayNode - - private let titleNode: TextNode - let arrowNode: ASImageNode - - private var item: SettingsThemesItem? - - private var thumbnailNodes: [SettingsThemeWallpaperNode] = [] - - var tag: Any? { - return self.item?.tag - } - - init() { - self.backgroundNode = ASDisplayNode() - self.backgroundNode.isLayerBacked = true - self.backgroundNode.backgroundColor = .white - - self.topStripeNode = ASDisplayNode() - self.topStripeNode.isLayerBacked = true - - self.bottomStripeNode = ASDisplayNode() - self.bottomStripeNode.isLayerBacked = true - - self.titleNode = TextNode() - self.titleNode.isLayerBacked = true - self.titleNode.contentMode = .left - self.titleNode.contentsScale = UIScreen.main.scale - - self.arrowNode = ASImageNode() - self.arrowNode.displayWithoutProcessing = true - self.arrowNode.displaysAsynchronously = false - self.arrowNode.isLayerBacked = true - - self.highlightedBackgroundNode = ASDisplayNode() - self.highlightedBackgroundNode.isLayerBacked = true - - super.init(layerBacked: false, dynamicBounce: false) - - self.addSubnode(self.titleNode) - self.addSubnode(self.arrowNode) - - for i in 0 ..< 5 { - let imageNode = SettingsThemeWallpaperNode() - self.thumbnailNodes.append(imageNode) - self.addSubnode(imageNode) - let index = i - imageNode.pressed = { [weak self] in - if let strongSelf = self, let item = strongSelf.item { - if index < item.wallpapers.count { - item.openWallpaper(item.wallpapers[index]) - } - } - } - } - } - - func asyncLayout() -> (_ item: SettingsThemesItem, _ width: CGFloat, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { - let makeTitleLayout = TextNode.asyncLayout(self.titleNode) - - let currentItem = self.item - - return { item, width, neighbors in - let textColor: UIColor = item.theme.list.itemPrimaryTextColor - - let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: item.title, font: titleFont, textColor: textColor), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) - - var updateArrowImage: UIImage? - var updatedTheme: PresentationTheme? - - if currentItem?.theme !== item.theme { - updatedTheme = item.theme - updateArrowImage = PresentationResourcesItemList.disclosureArrowImage(item.theme) - } - - let contentSize: CGSize - let insets: UIEdgeInsets - let separatorHeight = UIScreenPixel - - let referenceImageSize = CGSize(width: 108.0, height: 163.0) - - let leftInset: CGFloat = 16.0 - let padding: CGFloat = 16.0 - let minSpacing: CGFloat = 7.0 - - let imageCount = Int((width - padding * 2.0 + minSpacing) / (referenceImageSize.width + minSpacing)) - - let imageSize = referenceImageSize.aspectFilled(CGSize(width: floor((width - padding * 2.0 - max(0.0, CGFloat(imageCount - 1) * minSpacing)) / CGFloat(imageCount)), height: referenceImageSize.height)) - - let spacing = floor((width - padding * 2.0 - CGFloat(imageCount) * imageSize.width) / CGFloat(imageCount - 1)) - - contentSize = CGSize(width: width, height: imageSize.height + 58.0) - insets = itemListNeighborsGroupedInsets(neighbors) - - let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) - let layoutSize = layout.size - - return (layout, { [weak self] in - if let strongSelf = self { - strongSelf.item = item - - if let updateArrowImage = updateArrowImage { - strongSelf.arrowNode.image = updateArrowImage - } - - if let _ = updatedTheme { - strongSelf.topStripeNode.backgroundColor = item.theme.list.itemSeparatorColor - strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemSeparatorColor - strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBackgroundColor - strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor - } - - let _ = titleApply() - - if strongSelf.backgroundNode.supernode == nil { - strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) - } - if strongSelf.topStripeNode.supernode == nil { - strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) - } - if strongSelf.bottomStripeNode.supernode == nil { - strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) - } - switch neighbors.top { - case .sameSection(false): - strongSelf.topStripeNode.isHidden = true - default: - strongSelf.topStripeNode.isHidden = false - } - let bottomStripeInset: CGFloat - let bottomStripeOffset: CGFloat - switch neighbors.bottom { - case .sameSection(false): - bottomStripeInset = 16.0 - bottomStripeOffset = -separatorHeight - default: - bottomStripeInset = 0.0 - bottomStripeOffset = 0.0 - } - strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) - strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) - strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) - - strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - titleLayout.size.height - 10.0), size: titleLayout.size) - if let arrowImage = strongSelf.arrowNode.image { - strongSelf.arrowNode.frame = CGRect(origin: CGPoint(x: width - 15.0 - arrowImage.size.width, y: contentSize.height - 26.0), size: arrowImage.size) - } - - let bounds = CGRect(origin: CGPoint(), size: contentSize) - - for i in 0 ..< strongSelf.thumbnailNodes.count { - - /*if (i >= (int)_imageViews.count) - { - imageView = [[TGRemoteImageView alloc] init]; - imageView.fadeTransition = true; - imageView.fadeTransitionDuration = 0.2; - imageView.clipsToBounds = true; - imageView.contentMode = UIViewContentModeScaleAspectFill; - - imageViewContainer = [[UIButton alloc] init]; - imageView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; - [imageViewContainer addSubview:imageView]; - - UIImageView *checkView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"ModernWallpaperSelectedIndicator.png"]]; - checkView.frame = CGRectOffset(checkView.frame, imageView.frame.size.width - 5.0f - checkView.frame.size.width, imageView.frame.size.height - 4.0f - checkView.frame.size.height); - checkView.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleTopMargin; - checkView.tag = 100; - [imageView addSubview:checkView]; - - [self addSubview:imageViewContainer]; - [_imageViews addObject:imageViewContainer]; - - [imageViewContainer addTarget:self action:@selector(imageViewTapped:) forControlEvents:UIControlEventTouchUpInside]; - } - else - { - imageViewContainer = _imageViews[i]; - imageView = [imageViewContainer.subviews firstObject]; - } - - imageView.contentHints = _syncLoad ? TGRemoteImageContentHintLoadFromDiskSynchronously : 0; - - imageViewContainer.hidden = false;*/ - - let itemFrame = CGRect(x: (i == imageCount - 1 && item.wallpapers.count >= 3) ? (bounds.size.width - padding - imageSize.width) : (padding + CGFloat(i) * (imageSize.width + spacing)), y: 15.0, width: imageSize.width, height: imageSize.height) - strongSelf.thumbnailNodes[i].frame = itemFrame - - let imageNode = strongSelf.thumbnailNodes[i] - if i >= item.wallpapers.count || i >= imageCount { - imageNode.isHidden = true - } else { - imageNode.isHidden = false - imageNode.setWallpaper(account: item.account, wallpaper: item.wallpapers[i], size: itemFrame.size) - } - } - - strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: width, height: contentSize.height + UIScreenPixel + UIScreenPixel)) - } - }) - } - } - - override func setHighlighted(_ highlighted: Bool, animated: Bool) { - super.setHighlighted(highlighted, animated: animated) - - if highlighted { - self.highlightedBackgroundNode.alpha = 1.0 - if self.highlightedBackgroundNode.supernode == nil { - var anchorNode: ASDisplayNode? - if self.bottomStripeNode.supernode != nil { - anchorNode = self.bottomStripeNode - } else if self.topStripeNode.supernode != nil { - anchorNode = self.topStripeNode - } else if self.backgroundNode.supernode != nil { - anchorNode = self.backgroundNode - } - if let anchorNode = anchorNode { - self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode) - } else { - self.addSubnode(self.highlightedBackgroundNode) - } - } - } else { - if self.highlightedBackgroundNode.supernode != nil { - if animated { - self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in - if let strongSelf = self { - if completed { - strongSelf.highlightedBackgroundNode.removeFromSupernode() - } - } - }) - self.highlightedBackgroundNode.alpha = 0.0 - } else { - self.highlightedBackgroundNode.removeFromSupernode() - } - } - } - } - - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { - self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) - } - - override func animateRemoved(_ currentTimestamp: Double, duration: Double) { - self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) - } -} diff --git a/TelegramUI/ShareActionButtonNode.swift b/TelegramUI/ShareActionButtonNode.swift index 31c10e2f1f..2620a510e9 100644 --- a/TelegramUI/ShareActionButtonNode.swift +++ b/TelegramUI/ShareActionButtonNode.swift @@ -2,9 +2,9 @@ import Foundation import AsyncDisplayKit import Display -private let badgeBackgroundImage = generateStretchableFilledCircleImage(diameter: 22.0, color: UIColor(rgb: 0x007ee5)) - final class ShareActionButtonNode: HighlightTrackingButtonNode { + private let badgeTextColor: UIColor + private let badgeLabel: ASTextNode private let badgeBackground: ASImageNode @@ -12,7 +12,7 @@ final class ShareActionButtonNode: HighlightTrackingButtonNode { didSet { if self.badge != oldValue { if let badge = self.badge { - self.badgeLabel.attributedText = NSAttributedString(string: badge, font: Font.regular(14.0), textColor: .white, paragraphAlignment: .center) + self.badgeLabel.attributedText = NSAttributedString(string: badge, font: Font.regular(14.0), textColor: self.badgeTextColor, paragraphAlignment: .center) self.badgeLabel.isHidden = false self.badgeBackground.isHidden = false } else { @@ -26,7 +26,9 @@ final class ShareActionButtonNode: HighlightTrackingButtonNode { } } - override init() { + init(badgeBackgroundColor: UIColor, badgeTextColor: UIColor) { + self.badgeTextColor = badgeTextColor + self.badgeLabel = ASTextNode() self.badgeLabel.isHidden = true self.badgeLabel.isLayerBacked = true @@ -38,22 +40,12 @@ final class ShareActionButtonNode: HighlightTrackingButtonNode { self.badgeBackground.displaysAsynchronously = false self.badgeBackground.displayWithoutProcessing = true - self.badgeBackground.image = badgeBackgroundImage + self.badgeBackground.image = generateStretchableFilledCircleImage(diameter: 22.0, color: badgeBackgroundColor) super.init() self.addSubnode(self.badgeBackground) self.addSubnode(self.badgeLabel) - - /*self.highligthedChanged = { [weak self] value in - if highlighted { - strongSelf.backgroundNode.backgroundColor = ActionSheetItemNode.highlightedBackgroundColor - } else { - UIView.animate(withDuration: 0.3, animations: { - strongSelf.backgroundNode.backgroundColor = ActionSheetItemNode.defaultBackgroundColor - }) - } - }*/ } override func layout() { diff --git a/TelegramUI/ShareController.swift b/TelegramUI/ShareController.swift index 4771c8dbe3..cdf0fef611 100644 --- a/TelegramUI/ShareController.swift +++ b/TelegramUI/ShareController.swift @@ -5,31 +5,144 @@ import Postbox import TelegramCore import SwiftSignalKit -private func canSendMessagesToPeer(_ peer: Peer) -> Bool { - if peer is TelegramUser || peer is TelegramGroup { - return true - } else if let peer = peer as? TelegramSecretChat { - return peer.embeddedState == .active - } else if let peer = peer as? TelegramChannel { - switch peer.info { - case .broadcast: - return peer.hasAdminRights(.canPostMessages) - case .group: - return true - } - } else { - return false - } -} - public struct ShareControllerAction { let title: String let action: () -> Void } +public enum ShareControllerExternalStatus { + case preparing + case done +} + public enum ShareControllerSubject { case url(String) - case message(Message) + case messages([Message]) + case fromExternal(([PeerId], String) -> Signal) +} + +private enum ExternalShareItem { + case text(String) + case url(URL) + case image(UIImage) + case file(URL, String, String) +} + +private enum ExternalShareItemStatus { + case progress + case done(ExternalShareItem) +} + +private enum ExternalShareResourceStatus { + case progress + case done(MediaResourceData) +} + +private func collectExternalShareResource(postbox: Postbox, resource: MediaResource, tag: MediaResourceFetchTag) -> Signal { + return Signal { subscriber in + let fetched = postbox.mediaBox.fetchedResource(resource, tag: tag).start() + let data = postbox.mediaBox.resourceData(resource, option: .complete(waitUntilFetchStatus: false)).start(next: { value in + if value.complete { + subscriber.putNext(.done(value)) + } else { + subscriber.putNext(.progress) + } + }) + + return ActionDisposable { + fetched.dispose() + data.dispose() + } + } +} + +private enum ExternalShareItemsState { + case progress + case done([ExternalShareItem]) +} + +private struct CollectableExternalShareItem { + let url: String? + let text: String + let media: Media? +} + +private func collectExternalShareItems(postbox: Postbox, collectableItems: [CollectableExternalShareItem]) -> Signal { + var signals: [Signal] = [] + for item in collectableItems { + if let file = item.media as? TelegramMediaFile { + signals.append(collectExternalShareResource(postbox: postbox, resource: file.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .file)) + |> map { next -> ExternalShareItemStatus in + switch next { + case .progress: + return .progress + case let .done(data): + let fileName: String + if let value = file.fileName { + fileName = value + } else if file.isVideo { + fileName = "telegram_video.mp4" + } else { + fileName = "file" + } + let randomDirectory = UUID() + let safeFileName = fileName.replacingOccurrences(of: "/", with: "_") + let fileDirectory = NSTemporaryDirectory() + "\(randomDirectory)" + let _ = try? FileManager.default.createDirectory(at: URL(fileURLWithPath: fileDirectory), withIntermediateDirectories: true, attributes: nil) + let filePath = fileDirectory + "/\(safeFileName)" + if let _ = try? FileManager.default.copyItem(at: URL(fileURLWithPath: data.path), to: URL(fileURLWithPath: filePath)) { + return .done(.file(URL(fileURLWithPath: filePath), fileName, file.mimeType)) + } else { + return .progress + } + } + }) + } else if let image = item.media as? TelegramMediaImage, let largest = largestImageRepresentation(image.representations) { + signals.append(collectExternalShareResource(postbox: postbox, resource: largest.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .image)) + |> map { next -> ExternalShareItemStatus in + switch next { + case .progress: + return .progress + case let .done(data): + if let fileData = try? Data(contentsOf: URL(fileURLWithPath: data.path)), let image = UIImage(data: fileData) { + return .done(.image(image)) + } else { + return .progress + } + } + }) + } + if let url = item.url, let parsedUrl = URL(string: url) { + if signals.isEmpty { + signals.append(.single(.done(.url(parsedUrl)))) + } + } + if !item.text.isEmpty { + if signals.isEmpty { + signals.append(.single(.done(.text(item.text)))) + } + } + } + return combineLatest(signals) + |> map { statuses -> ExternalShareItemsState in + var items: [ExternalShareItem] = [] + for status in statuses { + switch status { + case .progress: + return .progress + case let .done(item): + items.append(item) + } + } + return .done(items) + } + |> distinctUntilChanged(isEqual: { lhs, rhs in + if case .progress = lhs, case .progress = rhs { + return true + } else { + return false + } + }) } public final class ShareController: ViewController { @@ -42,6 +155,7 @@ public final class ShareController: ViewController { private let account: Account private var presentationData: PresentationData private let externalShare: Bool + private let immediateExternalShare: Bool private let subject: ShareControllerSubject private let peers = Promise<[Peer]>() @@ -51,9 +165,10 @@ public final class ShareController: ViewController { public var dismissed: (() -> Void)? - public init(account: Account, subject: ShareControllerSubject, saveToCameraRoll: Bool = false, externalShare: Bool = true) { + public init(account: Account, subject: ShareControllerSubject, saveToCameraRoll: Bool = false, showInChat: ((Message) -> Void)? = nil, externalShare: Bool = true, immediateExternalShare: Bool = false) { self.account = account self.externalShare = externalShare + self.immediateExternalShare = immediateExternalShare self.subject = subject self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } @@ -62,31 +177,41 @@ public final class ShareController: ViewController { switch subject { case let .url(text): - self.defaultAction = ShareControllerAction(title: self.presentationData.strings.Web_CopyLink, action: { [weak self] in + self.defaultAction = ShareControllerAction(title: self.presentationData.strings.ShareMenu_CopyShareLink, action: { [weak self] in UIPasteboard.general.string = text self?.controllerNode.cancel?() }) - case let .message(message): - if saveToCameraRoll { - self.defaultAction = ShareControllerAction(title: self.presentationData.strings.Preview_SaveToCameraRoll, action: { [weak self] in - self?.saveToCameraRoll(message) - }) - } else if let chatPeer = message.peers[message.id.peerId] as? TelegramChannel { - if message.id.namespace == Namespaces.Message.Cloud, let addressName = chatPeer.addressName, !addressName.isEmpty { - self.defaultAction = ShareControllerAction(title: self.presentationData.strings.Web_CopyLink, action: { [weak self] in - UIPasteboard.general.string = "https://t.me/\(addressName)/\(message.id.id)" - self?.controllerNode.cancel?() + case let .messages(messages): + if messages.count == 1, let message = messages.first { + if saveToCameraRoll { + self.defaultAction = ShareControllerAction(title: self.presentationData.strings.Preview_SaveToCameraRoll, action: { [weak self] in + self?.saveToCameraRoll(message) }) + } else if let showInChat = showInChat { + self.defaultAction = ShareControllerAction(title: self.presentationData.strings.SharedMedia_ViewInChat, action: { [weak self] in + self?.controllerNode.cancel?() + showInChat(message) + }) + } else if let chatPeer = message.peers[message.id.peerId] as? TelegramChannel { + if message.id.namespace == Namespaces.Message.Cloud, let addressName = chatPeer.addressName, !addressName.isEmpty { + self.defaultAction = ShareControllerAction(title: self.presentationData.strings.ShareMenu_CopyShareLink, action: { [weak self] in + UIPasteboard.general.string = "https://t.me/\(addressName)/\(message.id.id)" + self?.controllerNode.cancel?() + }) + } } } + case .fromExternal: + break } - self.peers.set(account.viewTracker.tailChatListView(count: 150) |> take(1) |> map { view -> [Peer] in + self.peers.set(combineLatest(account.postbox.loadedPeerWithId(account.peerId) |> take(1), account.viewTracker.tailChatListView(groupId: nil, count: 150) |> take(1)) |> map { accountPeer, view -> [Peer] in var peers: [Peer] = [] + peers.append(accountPeer) for entry in view.0.entries.reversed() { switch entry { case let .MessageEntry(_, message, _, _, _, renderedPeer, _): - if let peer = renderedPeer.chatMainPeer { + if let peer = renderedPeer.chatMainPeer, peer.id != accountPeer.id { if canSendMessagesToPeer(peer) { peers.append(peer) } @@ -110,7 +235,7 @@ public final class ShareController: ViewController { override public func loadDisplayNode() { self.displayNode = ShareControllerNode(account: self.account, defaultAction: self.defaultAction, requestLayout: { [weak self] transition in self?.requestLayout(transition: transition) - }, externalShare: self.externalShare) + }, externalShare: self.externalShare, immediateExternalShare: self.immediateExternalShare) self.controllerNode.dismiss = { [weak self] in self?.presentingViewController?.dismiss(animated: false, completion: nil) self?.dismissed?() @@ -125,52 +250,100 @@ public final class ShareController: ViewController { for peerId in peerIds { var messages: [EnqueueMessage] = [] if !text.isEmpty { - messages.append(.message(text: text, attributes: [], media: nil, replyToMessageId: nil)) + messages.append(.message(text: text, attributes: [], media: nil, replyToMessageId: nil, localGroupingKey: nil)) } - messages.append(.message(text: url, attributes: [], media: nil, replyToMessageId: nil)) + messages.append(.message(text: url, attributes: [], media: nil, replyToMessageId: nil, localGroupingKey: nil)) let _ = enqueueMessages(account: strongSelf.account, peerId: peerId, messages: messages).start() } - case let .message(message): + return .complete() + case let .messages(messages): for peerId in peerIds { - var messages: [EnqueueMessage] = [] + var messagesToEnqueue: [EnqueueMessage] = [] if !text.isEmpty { - messages.append(.message(text: text, attributes: [], media: nil, replyToMessageId: nil)) + messagesToEnqueue.append(.message(text: text, attributes: [], media: nil, replyToMessageId: nil, localGroupingKey: nil)) } - messages.append(.forward(source: message.id)) - let _ = enqueueMessages(account: strongSelf.account, peerId: peerId, messages: messages).start() + for message in messages { + messagesToEnqueue.append(.forward(source: message.id, grouping: .auto)) + } + let _ = enqueueMessages(account: strongSelf.account, peerId: peerId, messages: messagesToEnqueue).start() } + return .complete() + case let .fromExternal(f): + return f(peerIds, text) + |> mapToSignal { _ -> Signal in + return .complete() + } } } return .complete() } self.controllerNode.shareExternal = { [weak self] in if let strongSelf = self { - var activityItems: [Any] = [] + var collectableItems: [CollectableExternalShareItem] = [] switch strongSelf.subject { case let .url(text): - if let url = URL(string: text) { - activityItems.append(url) - } - case let .message(message): - if let chatPeer = message.peers[message.id.peerId] as? TelegramChannel { - if message.id.namespace == Namespaces.Message.Cloud, let addressName = chatPeer.addressName, !addressName.isEmpty { - if let url = URL(string: "https://t.me/\(addressName)/\(message.id.id)") { - activityItems.append("https://t.me/\(addressName)/\(message.id.id)" as NSString) + collectableItems.append(CollectableExternalShareItem(url: text, text: "", media: nil)) + case let .messages(messages): + for message in messages { + var url: String? + var selectedMedia: Media? + loop: for media in message.media { + switch media { + case _ as TelegramMediaImage, _ as TelegramMediaFile: + selectedMedia = media + break loop + case let webpage as TelegramMediaWebpage: + if case let .Loaded(content) = webpage.content { + if let file = content.file { + selectedMedia = file + } else if let image = content.image { + selectedMedia = image + } + } + default: + break } } + if let chatPeer = message.peers[message.id.peerId] as? TelegramChannel { + if message.id.namespace == Namespaces.Message.Cloud, let addressName = chatPeer.addressName, !addressName.isEmpty { + url = "https://t.me/\(addressName)/\(message.id.id)" + } + } + collectableItems.append(CollectableExternalShareItem(url: url, text: message.text, media: selectedMedia)) } + case .fromExternal: + break } - let activityController = UIActivityViewController(activityItems: activityItems, applicationActivities: nil) - if let window = strongSelf.view.window { - let legacyController = LegacyController(presentation: .modal(animateIn: false)) - let navigationController = UINavigationController() - legacyController.bind(controller: navigationController) - strongSelf.present(legacyController, in: .window(.root)) - navigationController.present(activityController, animated: true, completion: nil) - /*window.rootViewController?.present(activityController, animated: true, completion: { - - })*/ + return (collectExternalShareItems(postbox: strongSelf.account.postbox, collectableItems: collectableItems) |> deliverOnMainQueue) |> map { state in + switch state { + case .progress: + return .preparing + case let .done(items): + if let strongSelf = self, !items.isEmpty { + strongSelf.ready.set(.single(true)) + var activityItems: [Any] = [] + for item in items { + switch item { + case let .url(url): + activityItems.append(url as NSURL) + case let .text(text): + activityItems.append(text as NSString) + case let .image(image): + activityItems.append(image) + case let .file(url, fileName, mimeType): + activityItems.append(url) + } + } + let activityController = UIActivityViewController(activityItems: activityItems, applicationActivities: nil) + if let window = strongSelf.view.window { + window.rootViewController?.present(activityController, animated: true, completion: nil) + } + } + return .done + } } + } else { + return .single(.done) } } self.displayNodeDidLoad() @@ -198,6 +371,7 @@ public final class ShareController: ViewController { } override public func dismiss(completion: (() -> Void)? = nil) { + self.controllerNode.view.endEditing(true) self.controllerNode.animateOut(completion: completion) } diff --git a/TelegramUI/ShareControllerNode.swift b/TelegramUI/ShareControllerNode.swift index 25597d60fd..e2b380652e 100644 --- a/TelegramUI/ShareControllerNode.swift +++ b/TelegramUI/ShareControllerNode.swift @@ -5,15 +5,21 @@ import SwiftSignalKit import Postbox import TelegramCore +enum ShareExternalState { + case preparing + case done +} + final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate { private let account: Account private var presentationData: PresentationData private let externalShare: Bool + private let immediateExternalShare: Bool private let defaultAction: ShareControllerAction? private let requestLayout: (ContainedViewLayoutTransition) -> Void - private var containerLayout: (ContainerViewLayout, CGFloat)? + private var containerLayout: (ContainerViewLayout, CGFloat, CGFloat)? private let dimNode: ASDisplayNode @@ -35,7 +41,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate var dismiss: (() -> Void)? var cancel: (() -> Void)? var share: ((String, [PeerId]) -> Signal)? - var shareExternal: (() -> Void)? + var shareExternal: (() -> Signal)? let ready = Promise() private var didSetReady = false @@ -49,27 +55,29 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate private let shareDisposable = MetaDisposable() - init(account: Account, defaultAction: ShareControllerAction?, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void, externalShare: Bool) { + init(account: Account, defaultAction: ShareControllerAction?, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void, externalShare: Bool, immediateExternalShare: Bool) { self.account = account self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } self.externalShare = externalShare + self.immediateExternalShare = immediateExternalShare self.defaultAction = defaultAction self.requestLayout = requestLayout - let roundedBackground = generateStretchableFilledCircleImage(radius: 16.0, color: .white) - let highlightedRoundedBackground = generateStretchableFilledCircleImage(radius: 16.0, color: UIColor(white: 0.9, alpha: 1.0)) + let roundedBackground = generateStretchableFilledCircleImage(radius: 16.0, color: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor) + let highlightedRoundedBackground = generateStretchableFilledCircleImage(radius: 16.0, color: self.presentationData.theme.actionSheet.opaqueItemHighlightedBackgroundColor) + let theme = self.presentationData.theme let halfRoundedBackground = generateImage(CGSize(width: 32.0, height: 32.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(UIColor.white.cgColor) + context.setFillColor(theme.actionSheet.opaqueItemBackgroundColor.cgColor) context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height))) context.fill(CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height / 2.0))) })?.stretchableImage(withLeftCapWidth: 16, topCapHeight: 1) let highlightedHalfRoundedBackground = generateImage(CGSize(width: 32.0, height: 32.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(UIColor(white: 0.9, alpha: 1.0).cgColor) + context.setFillColor(theme.actionSheet.opaqueItemHighlightedBackgroundColor.cgColor) context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height))) context.fill(CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height / 2.0))) })?.stretchableImage(withLeftCapWidth: 16, topCapHeight: 1) @@ -102,23 +110,24 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate self.actionsBackgroundNode.displaysAsynchronously = false self.actionsBackgroundNode.image = halfRoundedBackground - self.actionButtonNode = ShareActionButtonNode() + self.actionButtonNode = ShareActionButtonNode(badgeBackgroundColor: self.presentationData.theme.actionSheet.controlAccentColor, badgeTextColor: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor) self.actionButtonNode.displaysAsynchronously = false self.actionButtonNode.titleNode.displaysAsynchronously = false self.actionButtonNode.setBackgroundImage(highlightedHalfRoundedBackground, for: .highlighted) - self.inputFieldNode = ShareInputFieldNode(placeholder: self.presentationData.strings.ShareMenu_Comment) + self.inputFieldNode = ShareInputFieldNode(theme: self.presentationData.theme, placeholder: self.presentationData.strings.ShareMenu_Comment) self.inputFieldNode.alpha = 0.0 self.actionSeparatorNode = ASDisplayNode() self.actionSeparatorNode.isLayerBacked = true self.actionSeparatorNode.displaysAsynchronously = false - self.actionSeparatorNode.backgroundColor = UIColor(white: 0.9, alpha: 1.0) + self.actionSeparatorNode.backgroundColor = self.presentationData.theme.actionSheet.opaqueItemSeparatorColor super.init() self.controllerInteraction = ShareControllerInteraction(togglePeer: { [weak self] peer in if let strongSelf = self { + var added = false if strongSelf.controllerInteraction!.selectedPeerIds.contains(peer.id) { strongSelf.controllerInteraction!.selectedPeerIds.remove(peer.id) strongSelf.controllerInteraction!.selectedPeers = strongSelf.controllerInteraction!.selectedPeers.filter({ $0.id != peer.id }) @@ -127,6 +136,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate strongSelf.controllerInteraction!.selectedPeers.append(peer) strongSelf.contentNode?.setEnsurePeerVisibleOnLayout(peer.id) + added = true } let inputNodeAlpha: CGFloat = strongSelf.controllerInteraction!.selectedPeers.isEmpty ? 0.0 : 1.0 @@ -142,11 +152,18 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate strongSelf.updateButton() + strongSelf.peersContentNode?.updateSelectedPeers() strongSelf.contentNode?.updateSelectedPeers() - if let (layout, navigationBarHeight) = strongSelf.containerLayout { + if let (layout, navigationBarHeight, _) = strongSelf.containerLayout { strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .animated(duration: 0.4, curve: .spring)) } + + if added, strongSelf.contentNode is ShareSearchContainerNode { + if let peersContentNode = strongSelf.peersContentNode { + strongSelf.transitionToContentNode(peersContentNode) + } + } } }) @@ -159,7 +176,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate self.wrappingScrollNode.view.delegate = self self.addSubnode(self.wrappingScrollNode) - self.cancelButtonNode.setTitle(self.presentationData.strings.Common_Cancel, with: Font.medium(20.0), with: UIColor(rgb: 0x007ee5), for: .normal) + self.cancelButtonNode.setTitle(self.presentationData.strings.Common_Cancel, with: Font.medium(20.0), with: self.presentationData.theme.actionSheet.standardActionTextColor, for: .normal) self.wrappingScrollNode.addSubnode(self.cancelButtonNode) self.cancelButtonNode.addTarget(self, action: #selector(self.cancelButtonPressed), forControlEvents: .touchUpInside) @@ -176,7 +193,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate self.inputFieldNode.updateHeight = { [weak self] in if let strongSelf = self { - if let (layout, navigationBarHeight) = strongSelf.containerLayout { + if let (layout, navigationBarHeight, _) = strongSelf.containerLayout { strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .animated(duration: 0.15, curve: .spring)) } } @@ -220,15 +237,17 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate transition = .immediate } self.contentNode = contentNode - if let contentNode = contentNode { - contentNode.setContentOffsetUpdated({ [weak self] contentOffset, transition in - self?.contentNodeOffsetUpdated(contentOffset, transition: transition) - }) - self.contentContainerNode.insertSubnode(contentNode, at: 0) - } - if let (layout, navigationBarHeight) = self.containerLayout { + if let (layout, navigationBarHeight, bottomGridInset) = self.containerLayout { if let contentNode = contentNode, let previous = previous { + contentNode.frame = previous.frame + contentNode.updateLayout(size: previous.bounds.size, bottomInset: bottomGridInset, transition: .immediate) + + contentNode.setContentOffsetUpdated({ [weak self] contentOffset, transition in + self?.contentNodeOffsetUpdated(contentOffset, transition: transition) + }) + self.contentContainerNode.insertSubnode(contentNode, at: 0) + contentNode.alpha = 1.0 let animation = contentNode.layer.makeAnimation(from: 0.0 as NSNumber, to: 1.0 as NSNumber, keyPath: "opacity", timingFunction: kCAMediaTimingFunctionEaseInEaseOut, duration: 0.35) animation.fillMode = kCAFillModeBoth @@ -237,50 +256,45 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate } contentNode.layer.add(animation, forKey: "opacity") - contentNode.frame = previous.frame - var bottomGridInset: CGFloat = 57.0 - - let inputHeight = self.inputFieldNode.bounds.size.height - - if !self.controllerInteraction!.selectedPeers.isEmpty { - bottomGridInset += inputHeight - } self.animateContentNodeOffsetFromBackgroundOffset = self.contentBackgroundNode.frame.minY self.scheduleInteractiveTransition(transition) + contentNode.activate() previous.deactivate() - //self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) } else { + if let contentNode = self.contentNode { + contentNode.setContentOffsetUpdated({ [weak self] contentOffset, transition in + self?.contentNodeOffsetUpdated(contentOffset, transition: transition) + }) + self.contentContainerNode.insertSubnode(contentNode, at: 0) + } + self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) } + } else if let contentNode = contentNode { + contentNode.setContentOffsetUpdated({ [weak self] contentOffset, transition in + self?.contentNodeOffsetUpdated(contentOffset, transition: transition) + }) + self.contentContainerNode.insertSubnode(contentNode, at: 0) } } } func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { - self.containerLayout = (layout, navigationBarHeight) - self.scheduledLayoutTransitionRequest = nil - - transition.updateFrame(node: self.wrappingScrollNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - var insets = layout.insets(options: [.statusBar, .input]) + let cleanInsets = layout.insets(options: [.statusBar]) insets.top = max(10.0, insets.top) - transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - - let bottomInset: CGFloat = 10.0 + let bottomInset: CGFloat = 10.0 + cleanInsets.bottom let buttonHeight: CGFloat = 57.0 let sectionSpacing: CGFloat = 8.0 let titleAreaHeight: CGFloat = 64.0 - let width = min(layout.size.width, layout.size.height) - 20.0 - - let sideInset = floor((layout.size.width - width) / 2.0) - - transition.updateFrame(node: self.cancelButtonNode, frame: CGRect(origin: CGPoint(x: sideInset, y: layout.size.height - bottomInset - buttonHeight), size: CGSize(width: width, height: buttonHeight))) - let maximumContentHeight = layout.size.height - insets.top - max(bottomInset + buttonHeight, insets.bottom) - sectionSpacing + let width = min(layout.size.width, layout.size.height) - 20.0 + let sideInset = floor((layout.size.width - width) / 2.0) + let contentContainerFrame = CGRect(origin: CGPoint(x: sideInset, y: insets.top), size: CGSize(width: width, height: maximumContentHeight)) let contentFrame = contentContainerFrame.insetBy(dx: 0.0, dy: 0.0) @@ -292,6 +306,15 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate bottomGridInset += inputHeight } + self.containerLayout = (layout, navigationBarHeight, bottomGridInset) + self.scheduledLayoutTransitionRequest = nil + + transition.updateFrame(node: self.wrappingScrollNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + + transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + + transition.updateFrame(node: self.cancelButtonNode, frame: CGRect(origin: CGPoint(x: sideInset, y: layout.size.height - bottomInset - buttonHeight), size: CGSize(width: width, height: buttonHeight))) + transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame) transition.updateFrame(node: self.actionsBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentContainerFrame.size.height - bottomGridInset), size: CGSize(width: contentContainerFrame.size.width, height: bottomGridInset))) @@ -311,11 +334,12 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate } private func contentNodeOffsetUpdated(_ contentOffset: CGFloat, transition: ContainedViewLayoutTransition) { - if let (layout, _) = self.containerLayout { + if let (layout, _, _) = self.containerLayout { var insets = layout.insets(options: [.statusBar, .input]) insets.top = max(10.0, insets.top) + let cleanInsets = layout.insets(options: [.statusBar]) - let bottomInset: CGFloat = 10.0 + let bottomInset: CGFloat = 10.0 + cleanInsets.bottom let buttonHeight: CGFloat = 57.0 let sectionSpacing: CGFloat = 8.0 @@ -376,10 +400,10 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate transition.updateAlpha(node: self.actionsBackgroundNode, alpha: 0.0) if let signal = self.share?(self.inputFieldNode.text, self.controllerInteraction!.selectedPeers.map { $0.id }) { - self.transitionToContentNode(ShareLoadingContainerNode(), fastOut: true) + self.transitionToContentNode(ShareLoadingContainerNode(theme: self.presentationData.theme), fastOut: true) let timestamp = CACurrentMediaTime() self.shareDisposable.set(signal.start(completed: { [weak self] in - let minDelay = 1.2 + let minDelay = 0.6 let delay = max(0.0, (timestamp + minDelay) - CACurrentMediaTime()) Queue.mainQueue().after(delay, { if let strongSelf = self { @@ -392,44 +416,49 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate } func animateIn() { - self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) - - let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY - - let dimPosition = self.dimNode.layer.position - self.dimNode.layer.animatePosition(from: CGPoint(x: dimPosition.x, y: dimPosition.y - offset), to: dimPosition, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) - self.layer.animateBoundsOriginYAdditive(from: -offset, to: 0.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + if self.contentNode != nil { + self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + + let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY + + let dimPosition = self.dimNode.layer.position + self.dimNode.layer.animatePosition(from: CGPoint(x: dimPosition.x, y: dimPosition.y - offset), to: dimPosition, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + self.layer.animateBoundsOriginYAdditive(from: -offset, to: 0.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + } } func animateOut(completion: (() -> Void)? = nil) { - var dimCompleted = false - var offsetCompleted = false - - let internalCompletion: () -> Void = { [weak self] in - if let strongSelf = self, dimCompleted && offsetCompleted { - strongSelf.dismiss?() + if self.contentNode != nil { + var dimCompleted = false + var offsetCompleted = false + + let internalCompletion: () -> Void = { [weak self] in + if let strongSelf = self, dimCompleted && offsetCompleted { + strongSelf.dismiss?() + } + completion?() } + + self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in + dimCompleted = true + internalCompletion() + }) + + let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY + let dimPosition = self.dimNode.layer.position + self.dimNode.layer.animatePosition(from: dimPosition, to: CGPoint(x: dimPosition.x, y: dimPosition.y - offset), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + self.layer.animateBoundsOriginYAdditive(from: 0.0, to: -offset, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in + offsetCompleted = true + internalCompletion() + }) + } else { + self.dismiss?() completion?() } - - self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in - dimCompleted = true - internalCompletion() - }) - - let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY - let dimPosition = self.dimNode.layer.position - self.dimNode.layer.animatePosition(from: dimPosition, to: CGPoint(x: dimPosition.x, y: dimPosition.y - offset), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) - self.layer.animateBoundsOriginYAdditive(from: 0.0, to: -offset, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in - offsetCompleted = true - internalCompletion() - }) } func updatePeers(peers: [Peer], defaultAction: ShareControllerAction?) { - self.ready.set(.single(true)) - - let peersContentNode = SharePeersContainerNode(account: self.account, strings: self.presentationData.strings, peers: peers, controllerInteraction: self.controllerInteraction!, externalShare: false && self.externalShare) + let peersContentNode = SharePeersContainerNode(account: self.account, theme: self.presentationData.theme, strings: self.presentationData.strings, peers: peers, controllerInteraction: self.controllerInteraction!, externalShare: self.externalShare) self.peersContentNode = peersContentNode peersContentNode.openSearch = { [weak self] in if let strongSelf = self { @@ -448,26 +477,55 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate }) } } - peersContentNode.openShare = { [weak self] in - self?.shareExternal?() + let openShare: (Bool) -> Void = { [weak self] reportReady in + if let strongSelf = self, let shareExternal = strongSelf.shareExternal { + var loadingTimestamp: Double? + strongSelf.shareDisposable.set((shareExternal() |> deliverOnMainQueue).start(next: { state in + if let strongSelf = self { + switch state { + case .preparing: + if loadingTimestamp == nil { + strongSelf.inputFieldNode.deactivateInput() + let transition = ContainedViewLayoutTransition.animated(duration: 0.12, curve: .easeInOut) + transition.updateAlpha(node: strongSelf.actionButtonNode, alpha: 0.0) + transition.updateAlpha(node: strongSelf.inputFieldNode, alpha: 0.0) + transition.updateAlpha(node: strongSelf.actionSeparatorNode, alpha: 0.0) + transition.updateAlpha(node: strongSelf.actionsBackgroundNode, alpha: 0.0) + strongSelf.transitionToContentNode(ShareLoadingContainerNode(theme: strongSelf.presentationData.theme), fastOut: true) + loadingTimestamp = CACurrentMediaTime() + if reportReady { + strongSelf.ready.set(.single(true)) + } + } + case .done: + if let loadingTimestamp = loadingTimestamp { + let minDelay = 0.6 + let delay = max(0.0, (loadingTimestamp + minDelay) - CACurrentMediaTime()) + Queue.mainQueue().after(delay, { + if let strongSelf = self { + strongSelf.cancel?() + } + }) + } else { + if reportReady { + strongSelf.ready.set(.single(true)) + } + strongSelf.cancel?() + } + } + } + })) + } } - self.transitionToContentNode(peersContentNode) - - /*self.defaultAction = defaultAction - - self.peersContainerNode.peers = peers - self.peersUpdated = true - if let _ = self.containerLayout { - self.dequeueUpdatePeers() + peersContentNode.openShare = { + openShare(false) } - - self.actionSeparatorNode.alpha = 1.0 - - if let defaultAction = defaultAction { - self.actionButtonNode.setTitle(defaultAction.title, with: Font.regular(20.0), with: UIColor(rgb: 0x007ee5), for: .normal) + if self.immediateExternalShare { + openShare(true) } else { - self.actionButtonNode.setTitle(self.presentationData.strings.ShareMenu_Send, with: Font.medium(20.0), with: .gray, for: .normal) - }*/ + self.transitionToContentNode(peersContentNode) + self.ready.set(.single(true)) + } } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { @@ -522,13 +580,13 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate private func updateButton() { if self.controllerInteraction!.selectedPeers.isEmpty { if let defaultAction = self.defaultAction { - self.actionButtonNode.setTitle(defaultAction.title, with: Font.regular(20.0), with: UIColor(rgb: 0x007ee5), for: .normal) + self.actionButtonNode.setTitle(defaultAction.title, with: Font.regular(20.0), with: self.presentationData.theme.actionSheet.standardActionTextColor, for: .normal) } else { - self.actionButtonNode.setTitle(self.presentationData.strings.ShareMenu_Send, with: Font.medium(20.0), with: UIColor(rgb: 0x787878), for: .normal) + self.actionButtonNode.setTitle(self.presentationData.strings.ShareMenu_Send, with: Font.medium(20.0), with: self.presentationData.theme.actionSheet.disabledActionTextColor, for: .normal) } self.actionButtonNode.badge = nil } else { - self.actionButtonNode.setTitle(self.presentationData.strings.ShareMenu_Send, with: Font.medium(20.0), with: UIColor(rgb: 0x007ee5), for: .normal) + self.actionButtonNode.setTitle(self.presentationData.strings.ShareMenu_Send, with: Font.medium(20.0), with: self.presentationData.theme.actionSheet.standardActionTextColor, for: .normal) self.actionButtonNode.badge = "\(self.controllerInteraction!.selectedPeers.count)" } } @@ -541,10 +599,10 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate transition.updateAlpha(node: self.actionSeparatorNode, alpha: 0.0) transition.updateAlpha(node: self.actionsBackgroundNode, alpha: 0.0) - self.transitionToContentNode(ShareLoadingContainerNode(), fastOut: true) + self.transitionToContentNode(ShareLoadingContainerNode(theme: self.presentationData.theme), fastOut: true) let timestamp = CACurrentMediaTime() self.shareDisposable.set(signal.start(completed: { [weak self] in - let minDelay = 1.2 + let minDelay = 0.6 let delay = max(0.0, (timestamp + minDelay) - CACurrentMediaTime()) Queue.mainQueue().after(delay, { if let strongSelf = self { diff --git a/TelegramUI/ShareControllerPeerGridItem.swift b/TelegramUI/ShareControllerPeerGridItem.swift index d2b2c04071..d3536111f8 100644 --- a/TelegramUI/ShareControllerPeerGridItem.swift +++ b/TelegramUI/ShareControllerPeerGridItem.swift @@ -19,13 +19,15 @@ final class ShareControllerGridSection: GridSection { let height: CGFloat = 33.0 private let title: String + private let theme: PresentationTheme var hashValue: Int { return 1 } - init(title: String) { + init(title: String, theme: PresentationTheme) { self.title = title + self.theme = theme } func isEqual(to: GridSection) -> Bool { @@ -37,7 +39,7 @@ final class ShareControllerGridSection: GridSection { } func node() -> ASDisplayNode { - return ShareControllerGridSectionNode(title: self.title) + return ShareControllerGridSectionNode(title: self.title, theme: self.theme) } } @@ -47,14 +49,14 @@ final class ShareControllerGridSectionNode: ASDisplayNode { let backgroundNode: ASDisplayNode let titleNode: ASTextNode - init(title: String) { + init(title: String, theme: PresentationTheme) { self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true - self.backgroundNode.backgroundColor = UIColor(rgb: 0xf7f7f7) + self.backgroundNode.backgroundColor = theme.chatList.sectionHeaderFillColor self.titleNode = ASTextNode() self.titleNode.isLayerBacked = true - self.titleNode.attributedText = NSAttributedString(string: title.uppercased(), font: sectionTitleFont, textColor: UIColor(rgb: 0x8e8e93)) + self.titleNode.attributedText = NSAttributedString(string: title.uppercased(), font: sectionTitleFont, textColor: theme.list.sectionHeaderTextColor) self.titleNode.maximumNumberOfLines = 1 self.titleNode.truncationMode = .byTruncatingTail @@ -72,26 +74,30 @@ final class ShareControllerGridSectionNode: ASDisplayNode { self.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: bounds.size.width, height: 27.0)) let titleSize = self.titleNode.measure(CGSize(width: bounds.size.width - 24.0, height: CGFloat.greatestFiniteMagnitude)) - self.titleNode.frame = CGRect(origin: CGPoint(x: 9.0, y: 6.0), size: titleSize) + self.titleNode.frame = CGRect(origin: CGPoint(x: 9.0, y: 7.0), size: titleSize) } } final class ShareControllerPeerGridItem: GridItem { let account: Account + let theme: PresentationTheme + let strings: PresentationStrings let peer: Peer let chatPeer: Peer? let controllerInteraction: ShareControllerInteraction let section: GridSection? - init(account: Account, peer: Peer, chatPeer: Peer?, controllerInteraction: ShareControllerInteraction, sectionTitle: String? = nil) { + init(account: Account, theme: PresentationTheme, strings: PresentationStrings, peer: Peer, chatPeer: Peer?, controllerInteraction: ShareControllerInteraction, sectionTitle: String? = nil) { self.account = account + self.theme = theme + self.strings = strings self.peer = peer self.chatPeer = chatPeer self.controllerInteraction = controllerInteraction if let sectionTitle = sectionTitle { - self.section = ShareControllerGridSection(title: sectionTitle) + self.section = ShareControllerGridSection(title: sectionTitle, theme: self.theme) } else { self.section = nil } @@ -100,7 +106,7 @@ final class ShareControllerPeerGridItem: GridItem { func node(layout: GridNodeLayout) -> GridItemNode { let node = ShareControllerPeerGridItemNode() node.controllerInteraction = self.controllerInteraction - node.setup(account: self.account, peer: self.peer, chatPeer: self.chatPeer) + node.setup(account: self.account, theme: self.theme, strings: self.strings, peer: self.peer, chatPeer: self.chatPeer) return node } @@ -110,7 +116,7 @@ final class ShareControllerPeerGridItem: GridItem { return } node.controllerInteraction = self.controllerInteraction - node.setup(account: self.account, peer: self.peer, chatPeer: self.chatPeer) + node.setup(account: self.account, theme: self.theme, strings: self.strings, peer: self.peer, chatPeer: self.chatPeer) } } @@ -136,9 +142,11 @@ final class ShareControllerPeerGridItemNode: GridItemNode { self.addSubnode(self.peerNode) } - func setup(account: Account, peer: Peer, chatPeer: Peer?) { + func setup(account: Account, theme: PresentationTheme, strings: PresentationStrings, peer: Peer, chatPeer: Peer?) { if self.currentState == nil || self.currentState!.0 !== account || !arePeersEqual(self.currentState!.1, peer) { - self.peerNode.setup(account: account, peer: peer, chatPeer: chatPeer) + let itemTheme = SelectablePeerNodeTheme(textColor: theme.actionSheet.primaryTextColor, secretTextColor: .green, selectedTextColor: theme.actionSheet.controlAccentColor, checkBackgroundColor: theme.actionSheet.opaqueItemBackgroundColor, checkFillColor: theme.actionSheet.controlAccentColor, checkColor: theme.actionSheet.opaqueItemBackgroundColor) + self.peerNode.theme = itemTheme + self.peerNode.setup(account: account, strings: strings, peer: peer, chatPeer: chatPeer) self.currentState = (account, peer, chatPeer) self.setNeedsLayout() } diff --git a/TelegramUI/ShareControllerRecentPeersGridItem.swift b/TelegramUI/ShareControllerRecentPeersGridItem.swift index 8042f3d66a..f0374ba392 100644 --- a/TelegramUI/ShareControllerRecentPeersGridItem.swift +++ b/TelegramUI/ShareControllerRecentPeersGridItem.swift @@ -58,7 +58,7 @@ final class ShareControllerRecentPeersGridItemNode: GridItemNode { } else { peersNode = ChatListSearchRecentPeersNode(account: account, theme: theme, mode: .actionSheet, strings: strings, peerSelected: { [weak self] peer in self?.controllerInteraction?.togglePeer(peer) - }, isPeerSelected: { [weak self] peerId in + }, peerLongTapped: {_ in }, isPeerSelected: { [weak self] peerId in return self?.controllerInteraction?.selectedPeerIds.contains(peerId) ?? false }, share: true) self.peersNode = peersNode @@ -80,5 +80,6 @@ final class ShareControllerRecentPeersGridItemNode: GridItemNode { let bounds = self.bounds self.peersNode?.frame = CGRect(origin: CGPoint(), size: bounds.size) + self.peersNode?.updateLayout(size: bounds.size, leftInset: 0.0, rightInset: 0.0) } } diff --git a/TelegramUI/ShareInputFieldNode.swift b/TelegramUI/ShareInputFieldNode.swift index cac6a4ff4a..f92d3c62f4 100644 --- a/TelegramUI/ShareInputFieldNode.swift +++ b/TelegramUI/ShareInputFieldNode.swift @@ -6,9 +6,8 @@ private func generateClearIcon(color: UIColor) -> UIImage? { return generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: color) } -private let backgroundImage = generateStretchableFilledCircleImage(diameter: 6.0, color: UIColor(rgb: 0xe9e9e9)) - final class ShareInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegate { + private let theme: PresentationTheme private let backgroundNode: ASImageNode private let textInputNode: ASEditableTextNode private let placeholderNode: ASTextNode @@ -17,39 +16,42 @@ final class ShareInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegate { var updateHeight: (() -> Void)? private let backgroundInsets = UIEdgeInsets(top: 16.0, left: 16.0, bottom: 1.0, right: 16.0) - private let inputInsets = UIEdgeInsets(top: 10.0, left: 8.0, bottom: 10.0, right: 8.0) + private let inputInsets = UIEdgeInsets(top: 10.0, left: 8.0, bottom: 10.0, right: 16.0) private let accessoryButtonsWidth: CGFloat = 10.0 var text: String { return self.textInputNode.attributedText?.string ?? "" } - init(placeholder: String) { + init(theme: PresentationTheme, placeholder: String) { + self.theme = theme + self.backgroundNode = ASImageNode() self.backgroundNode.isLayerBacked = true self.backgroundNode.displaysAsynchronously = false self.backgroundNode.displayWithoutProcessing = true - self.backgroundNode.image = backgroundImage + self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 6.0, color: theme.actionSheet.inputBackgroundColor) self.textInputNode = ASEditableTextNode() - let textColor: UIColor = .black + let textColor: UIColor = theme.actionSheet.inputTextColor let keyboardAppearance: UIKeyboardAppearance = UIKeyboardAppearance.default textInputNode.typingAttributes = [NSAttributedStringKey.font.rawValue: Font.regular(17.0), NSAttributedStringKey.foregroundColor.rawValue: textColor] textInputNode.clipsToBounds = true textInputNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0) textInputNode.keyboardAppearance = keyboardAppearance textInputNode.textContainerInset = UIEdgeInsets(top: self.inputInsets.top, left: 0.0, bottom: self.inputInsets.bottom, right: 0.0) + textInputNode.keyboardAppearance = theme.chatList.searchBarKeyboardColor.keyboardAppearance self.placeholderNode = ASTextNode() self.placeholderNode.isLayerBacked = true self.placeholderNode.displaysAsynchronously = false - self.placeholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: UIColor(rgb: 0x818086)) + self.placeholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: theme.actionSheet.inputPlaceholderColor) self.clearButton = HighlightableButtonNode() self.clearButton.imageNode.displaysAsynchronously = false self.clearButton.imageNode.displayWithoutProcessing = true self.clearButton.displaysAsynchronously = false - self.clearButton.setImage(generateClearIcon(color: UIColor(rgb: 0x7b7b81)), for: []) + self.clearButton.setImage(generateClearIcon(color: theme.actionSheet.inputClearButtonColor), for: []) self.clearButton.isHidden = true super.init() @@ -101,7 +103,7 @@ final class ShareInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegate { } func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) { - self.placeholderNode.isHidden = false + self.placeholderNode.isHidden = !(editableTextNode.textView.text ?? "").isEmpty self.clearButton.isHidden = true } diff --git a/TelegramUI/ShareLoadingContainerNode.swift b/TelegramUI/ShareLoadingContainerNode.swift index 68b6a134f6..e00e10443a 100644 --- a/TelegramUI/ShareLoadingContainerNode.swift +++ b/TelegramUI/ShareLoadingContainerNode.swift @@ -8,8 +8,8 @@ final class ShareLoadingContainerNode: ASDisplayNode, ShareContentContainerNode private let activityIndicator: ActivityIndicator - override init() { - self.activityIndicator = ActivityIndicator(type: ActivityIndicatorType.custom(UIColor(rgb: 0x007ee5))) + init(theme: PresentationTheme) { + self.activityIndicator = ActivityIndicator(type: ActivityIndicatorType.custom(theme.actionSheet.controlAccentColor, 50.0)) super.init() diff --git a/TelegramUI/SharePeersContainerNode.swift b/TelegramUI/SharePeersContainerNode.swift index f2c69bff01..1d7aaa0fcf 100644 --- a/TelegramUI/SharePeersContainerNode.swift +++ b/TelegramUI/SharePeersContainerNode.swift @@ -5,13 +5,11 @@ import TelegramCore import SwiftSignalKit import Display -private let separatorColor: UIColor = UIColor(rgb: 0xbcbbc1) - private let subtitleFont = Font.regular(12.0) -private let subtitleColor = UIColor(rgb: 0x7b7b81) final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode { private let account: Account + private let theme: PresentationTheme private let strings: PresentationStrings private let controllerInteraction: ShareControllerInteraction var peers: [Peer]? @@ -32,8 +30,9 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode { private var validLayout: (CGSize, CGFloat)? private var overrideGridOffsetTransition: ContainedViewLayoutTransition? - init(account: Account, strings: PresentationStrings, peers: [Peer], controllerInteraction: ShareControllerInteraction, externalShare: Bool) { + init(account: Account, theme: PresentationTheme, strings: PresentationStrings, peers: [Peer], controllerInteraction: ShareControllerInteraction, externalShare: Bool) { self.account = account + self.theme = theme self.strings = strings self.controllerInteraction = controllerInteraction self.peers = peers @@ -41,26 +40,26 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode { self.contentGridNode = GridNode() self.contentTitleNode = ASTextNode() - self.contentTitleNode.attributedText = NSAttributedString(string: strings.ShareMenu_ShareTo, font: Font.medium(20.0), textColor: .black) + self.contentTitleNode.attributedText = NSAttributedString(string: strings.ShareMenu_ShareTo, font: Font.medium(20.0), textColor: self.theme.actionSheet.primaryTextColor) self.contentSubtitleNode = ASTextNode() self.contentSubtitleNode.maximumNumberOfLines = 1 self.contentSubtitleNode.isLayerBacked = true self.contentSubtitleNode.displaysAsynchronously = false self.contentSubtitleNode.truncationMode = .byTruncatingTail - self.contentSubtitleNode.attributedText = NSAttributedString(string: strings.ShareMenu_SelectChats, font: subtitleFont, textColor: subtitleColor) + self.contentSubtitleNode.attributedText = NSAttributedString(string: strings.ShareMenu_SelectChats, font: subtitleFont, textColor: self.theme.actionSheet.secondaryTextColor) self.searchButtonNode = HighlightableButtonNode() - self.searchButtonNode.setImage(UIImage(bundleImageName: "Share/SearchIcon")?.precomposed(), for: []) + self.searchButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Share/SearchIcon"), color: self.theme.actionSheet.controlAccentColor), for: []) self.shareButtonNode = HighlightableButtonNode() - self.shareButtonNode.setImage(UIImage(bundleImageName: "Share/ShareIcon")?.precomposed(), for: []) + self.shareButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Share/ShareIcon"), color: self.theme.actionSheet.controlAccentColor), for: []) self.shareButtonNode.isHidden = !externalShare self.contentSeparatorNode = ASDisplayNode() self.contentSeparatorNode.isLayerBacked = true self.contentSeparatorNode.displaysAsynchronously = false - self.contentSeparatorNode.backgroundColor = separatorColor + self.contentSeparatorNode.backgroundColor = self.theme.actionSheet.opaqueItemSeparatorColor super.init() @@ -74,7 +73,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode { var insertItems: [GridNodeInsertItem] = [] for i in 0 ..< peers.count { - insertItems.append(GridNodeInsertItem(index: i, item: ShareControllerPeerGridItem(account: self.account, peer: peers[i], chatPeer: nil, controllerInteraction: self.controllerInteraction), previousIndex: nil)) + insertItems.append(GridNodeInsertItem(index: i, item: ShareControllerPeerGridItem(account: self.account, theme: self.theme, strings: self.strings, peer: peers[i], chatPeer: nil, controllerInteraction: self.controllerInteraction), previousIndex: nil)) } self.contentGridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: insertItems, updateItems: [], scrollToItem: nil, updateLayout: nil, itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) @@ -234,7 +233,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode { } }) } - self.contentSubtitleNode.attributedText = NSAttributedString(string: subtitleText, font: subtitleFont, textColor: subtitleColor) + self.contentSubtitleNode.attributedText = NSAttributedString(string: subtitleText, font: subtitleFont, textColor: self.theme.actionSheet.secondaryTextColor) self.contentGridNode.forEachItemNode { itemNode in if let itemNode = itemNode as? ShareControllerPeerGridItemNode { diff --git a/TelegramUI/ShareSearchBarNode.swift b/TelegramUI/ShareSearchBarNode.swift index c156b50754..fa232fd06b 100644 --- a/TelegramUI/ShareSearchBarNode.swift +++ b/TelegramUI/ShareSearchBarNode.swift @@ -2,10 +2,6 @@ import Foundation import AsyncDisplayKit import Display -private let searchIconImage = UIImage(bundleImageName: "Share/SearchBarSearchIcon")?.precomposed() -private let backgroundImage = generateStretchableFilledCircleImage(diameter: 6.0, color: UIColor(rgb: 0xe2e2e2)) -private let placeholderColor = UIColor(rgb: 0x7b7b81) - private func generateClearIcon(color: UIColor) -> UIImage? { return generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: color) } @@ -20,35 +16,37 @@ final class ShareSearchBarNode: ASDisplayNode, UITextFieldDelegate { var textUpdated: ((String) -> Void)? - init(placeholder: String) { + init(theme: PresentationTheme, placeholder: String) { self.backgroundNode = ASImageNode() self.backgroundNode.isLayerBacked = true self.backgroundNode.displaysAsynchronously = false self.backgroundNode.displayWithoutProcessing = true - self.backgroundNode.image = backgroundImage + self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 6.0, color: theme.actionSheet.inputBackgroundColor) self.searchIconNode = ASImageNode() self.searchIconNode.isLayerBacked = true self.searchIconNode.displaysAsynchronously = false self.searchIconNode.displayWithoutProcessing = true - self.searchIconNode.image = searchIconImage + self.searchIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Share/SearchBarSearchIcon"), color: theme.actionSheet.inputPlaceholderColor) self.clearButton = HighlightableButtonNode() self.clearButton.imageNode.displaysAsynchronously = false self.clearButton.imageNode.displayWithoutProcessing = true self.clearButton.displaysAsynchronously = false - self.clearButton.setImage(generateClearIcon(color: UIColor(rgb: 0x7b7b81)), for: []) + self.clearButton.setImage(generateClearIcon(color: theme.actionSheet.inputClearButtonColor), for: []) self.clearButton.isHidden = true self.textInputNode = TextFieldNode() self.textInputNode.fixOffset = false - let textColor: UIColor = .black + let textColor: UIColor = theme.actionSheet.inputTextColor let keyboardAppearance: UIKeyboardAppearance = UIKeyboardAppearance.default textInputNode.textField.font = Font.regular(16.0) + textInputNode.textField.textColor = textColor textInputNode.textField.typingAttributes = [NSAttributedStringKey.font.rawValue: Font.regular(16.0), NSAttributedStringKey.foregroundColor.rawValue: textColor] textInputNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0) textInputNode.textField.keyboardAppearance = keyboardAppearance - textInputNode.textField.attributedPlaceholder = NSAttributedString(string: placeholder, font: Font.regular(16.0), textColor: placeholderColor) + textInputNode.textField.attributedPlaceholder = NSAttributedString(string: placeholder, font: Font.regular(16.0), textColor: theme.actionSheet.inputPlaceholderColor) + textInputNode.textField.keyboardAppearance = theme.chatList.searchBarKeyboardColor.keyboardAppearance super.init() diff --git a/TelegramUI/ShareSearchContainerNode.swift b/TelegramUI/ShareSearchContainerNode.swift index d04fae2027..3f6cbc2103 100644 --- a/TelegramUI/ShareSearchContainerNode.swift +++ b/TelegramUI/ShareSearchContainerNode.swift @@ -5,12 +5,8 @@ import TelegramCore import SwiftSignalKit import Display -private let separatorColor: UIColor = UIColor(rgb: 0xbcbbc1) - private let cancelFont = Font.regular(17.0) -private let cancelColor = UIColor(rgb: 0x007ee5) private let subtitleFont = Font.regular(12.0) -private let subtitleColor = UIColor(rgb: 0x7b7b81) private enum ShareSearchRecentEntryStableId: Hashable { case topPeers @@ -45,13 +41,13 @@ private enum ShareSearchRecentEntryStableId: Hashable { private enum ShareSearchRecentEntry: Comparable, Identifiable { case topPeers(PresentationTheme, PresentationStrings) - case peer(index: Int, peer: Peer, associatedPeer: Peer?, PresentationStrings) + case peer(index: Int, theme: PresentationTheme, peer: Peer, associatedPeer: Peer?, PresentationStrings) var stableId: ShareSearchRecentEntryStableId { switch self { case .topPeers: return .topPeers - case let .peer(_, peer, _, _): + case let .peer(_, _, peer, _, _): return .peerId(peer.id) } } @@ -70,8 +66,8 @@ private enum ShareSearchRecentEntry: Comparable, Identifiable { } else { return false } - case let .peer(lhsIndex, lhsPeer, lhsAssociatedPeer, lhsStrings): - if case let .peer(rhsIndex, rhsPeer, rhsAssociatedPeer, rhsStrings) = rhs, lhsPeer.isEqual(rhsPeer) && arePeersEqual(lhsAssociatedPeer, rhsAssociatedPeer) && lhsIndex == rhsIndex && lhsStrings === rhsStrings { + case let .peer(lhsIndex, lhsTheme, lhsPeer, lhsAssociatedPeer, lhsStrings): + if case let .peer(rhsIndex, rhsTheme, rhsPeer, rhsAssociatedPeer, rhsStrings) = rhs, lhsPeer.isEqual(rhsPeer) && arePeersEqual(lhsAssociatedPeer, rhsAssociatedPeer) && lhsIndex == rhsIndex && lhsStrings === rhsStrings && lhsTheme === rhsTheme { return true } else { return false @@ -83,11 +79,11 @@ private enum ShareSearchRecentEntry: Comparable, Identifiable { switch lhs { case .topPeers: return true - case let .peer(lhsIndex, _, _, _): + case let .peer(lhsIndex, _, _, _, _): switch rhs { case .topPeers: return false - case let .peer(rhsIndex, _, _, _): + case let .peer(rhsIndex, _, _, _, _): return lhsIndex <= rhsIndex } } @@ -97,7 +93,7 @@ private enum ShareSearchRecentEntry: Comparable, Identifiable { switch self { case let .topPeers(theme, strings): return ShareControllerRecentPeersGridItem(account: account, theme: theme, strings: strings, controllerInteraction: interfaceInteraction) - case let .peer(_, peer, associatedPeer, strings): + case let .peer(_, theme, peer, associatedPeer, strings): let primaryPeer: Peer var chatPeer: Peer? if let associatedPeer = associatedPeer { @@ -107,7 +103,7 @@ private enum ShareSearchRecentEntry: Comparable, Identifiable { primaryPeer = peer chatPeer = associatedPeer } - return ShareControllerPeerGridItem(account: account, peer: primaryPeer, chatPeer: chatPeer, controllerInteraction: interfaceInteraction, sectionTitle: strings.DialogList_SearchSectionRecent) + return ShareControllerPeerGridItem(account: account, theme: theme, strings: strings, peer: primaryPeer, chatPeer: chatPeer, controllerInteraction: interfaceInteraction, sectionTitle: strings.DialogList_SearchSectionRecent) } } } @@ -115,6 +111,8 @@ private enum ShareSearchRecentEntry: Comparable, Identifiable { private struct ShareSearchPeerEntry: Comparable, Identifiable { let index: Int32 let peer: Peer + let theme: PresentationTheme + let strings: PresentationStrings var stableId: Int64 { return self.peer.id.toInt64() @@ -135,7 +133,7 @@ private struct ShareSearchPeerEntry: Comparable, Identifiable { } func item(account: Account, interfaceInteraction: ShareControllerInteraction) -> GridItem { - return ShareControllerPeerGridItem(account: account, peer: self.peer, chatPeer: nil, controllerInteraction: interfaceInteraction) + return ShareControllerPeerGridItem(account: account, theme: self.theme, strings: self.strings, peer: self.peer, chatPeer: nil, controllerInteraction: interfaceInteraction) } } @@ -206,16 +204,16 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode { self.contentGridNode = GridNode() self.contentGridNode.isHidden = true - self.searchNode = ShareSearchBarNode(placeholder: strings.Common_Search) + self.searchNode = ShareSearchBarNode(theme: theme, placeholder: strings.Common_Search) self.cancelButtonNode = HighlightableButtonNode() - self.cancelButtonNode.setTitle(strings.Common_Cancel, with: cancelFont, with: cancelColor, for: []) + self.cancelButtonNode.setTitle(strings.Common_Cancel, with: cancelFont, with: theme.actionSheet.controlAccentColor, for: []) self.cancelButtonNode.hitTestSlop = UIEdgeInsets(top: -8.0, left: -8.0, bottom: -8.0, right: -8.0) self.contentSeparatorNode = ASDisplayNode() self.contentSeparatorNode.isLayerBacked = true self.contentSeparatorNode.displaysAsynchronously = false - self.contentSeparatorNode.backgroundColor = separatorColor + self.contentSeparatorNode.backgroundColor = theme.actionSheet.opaqueItemSeparatorColor super.init() @@ -243,30 +241,56 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode { let foundItems = searchQuery.get() |> mapToSignal { query -> Signal<[ShareSearchPeerEntry]?, NoError> in if !query.isEmpty { - let foundLocalPeers = account.postbox.searchPeers(query: query.lowercased()) - let foundRemotePeers: Signal<[Peer], NoError> = .single([]) |> then(searchPeers(account: account, query: query) + let accountPeer = account.postbox.loadedPeerWithId(account.peerId) |> take(1) + let foundLocalPeers = account.postbox.searchPeers(query: query.lowercased(), groupId: nil) + let foundRemotePeers: Signal<([FoundPeer], [FoundPeer]), NoError> = .single(([], [])) |> then(searchPeers(account: account, query: query) |> delay(0.2, queue: Queue.concurrentDefaultQueue())) - return combineLatest(foundLocalPeers, foundRemotePeers) - |> map { foundLocalPeers, foundRemotePeers -> [ShareSearchPeerEntry]? in + return combineLatest(accountPeer, foundLocalPeers, foundRemotePeers) + |> map { accountPeer, foundLocalPeers, foundRemotePeers -> [ShareSearchPeerEntry]? in var entries: [ShareSearchPeerEntry] = [] var index: Int32 = 0 - for renderedPeer in foundLocalPeers { - if let peer = renderedPeer.peers[renderedPeer.peerId] { - var associatedPeer: Peer? - if let associatedPeerId = peer.associatedPeerId { - associatedPeer = renderedPeer.peers[associatedPeerId] - } - //entries.append(.localPeer(peer, associatedPeer, index, themeAndStrings.0, themeAndStrings.1)) - entries.append(ShareSearchPeerEntry(index: index, peer: peer)) + + var existingPeerIds = Set() + + if strings.DialogList_SavedMessages.lowercased().hasPrefix(query.lowercased()) { + if !existingPeerIds.contains(accountPeer.id) { + existingPeerIds.insert(accountPeer.id) + entries.append(ShareSearchPeerEntry(index: index, peer: accountPeer, theme: theme, strings: strings)) index += 1 } } - for peer in foundRemotePeers { - entries.append(ShareSearchPeerEntry(index: index, peer: peer)) - //entries.append(.globalPeer(peer, index, themeAndStrings.0, themeAndStrings.1)) - index += 1 + for renderedPeer in foundLocalPeers { + if let peer = renderedPeer.peers[renderedPeer.peerId], peer.id != accountPeer.id { + if !existingPeerIds.contains(peer.id) { + existingPeerIds.insert(peer.id) + var associatedPeer: Peer? + if let associatedPeerId = peer.associatedPeerId { + associatedPeer = renderedPeer.peers[associatedPeerId] + } + entries.append(ShareSearchPeerEntry(index: index, peer: peer, theme: theme, strings: strings)) + index += 1 + } + } + } + + for foundPeer in foundRemotePeers.0 { + let peer = foundPeer.peer + if !existingPeerIds.contains(peer.id) { + existingPeerIds.insert(peer.id) + entries.append(ShareSearchPeerEntry(index: index, peer: foundPeer.peer, theme: theme, strings: strings)) + index += 1 + } + } + + for foundPeer in foundRemotePeers.1 { + let peer = foundPeer.peer + if !existingPeerIds.contains(peer.id) { + existingPeerIds.insert(peer.id) + entries.append(ShareSearchPeerEntry(index: index, peer: peer, theme: theme, strings: strings)) + index += 1 + } } return entries @@ -310,7 +334,7 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode { var index = 0 for peer in recentPeers { if let mainPeer = peer.peers[peer.peerId] { - recentItemList.append(.peer(index: index, peer: mainPeer, associatedPeer: mainPeer.associatedPeerId.flatMap { peer.peers[$0] }, strings)) + recentItemList.append(.peer(index: index, theme: theme, peer: mainPeer, associatedPeer: mainPeer.associatedPeerId.flatMap { peer.peers[$0] }, strings)) index += 1 } } @@ -406,7 +430,7 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode { switch $0 { case .topPeers: return false - case let .peer(_, peer, _, _): + case let .peer(_, _, peer, _, _): return peer.id == ensurePeerVisibleOnLayout } }) { @@ -472,7 +496,7 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode { let titleOffset = max(-titleAreaHeight, rawTitleOffset) let cancelButtonSize = self.cancelButtonNode.measure(CGSize(width: 320.0, height: 100.0)) - let cancelButtonFrame = CGRect(origin: CGPoint(x: bounds.size.width - cancelButtonSize.width - 12.0, y: titleOffset + 25.0), size: cancelButtonSize) + let cancelButtonFrame = CGRect(origin: CGPoint(x: size.width - cancelButtonSize.width - 12.0, y: titleOffset + 25.0), size: cancelButtonSize) transition.updateFrame(node: self.cancelButtonNode, frame: cancelButtonFrame) let searchNodeFrame = CGRect(origin: CGPoint(x: 16.0, y: titleOffset + 16.0), size: CGSize(width: cancelButtonFrame.minX - 16.0 - 10.0, height: 40.0)) diff --git a/TelegramUI/SharedMediaPlayer.swift b/TelegramUI/SharedMediaPlayer.swift index d39e1d2fde..f5910165b0 100644 --- a/TelegramUI/SharedMediaPlayer.swift +++ b/TelegramUI/SharedMediaPlayer.swift @@ -3,14 +3,23 @@ import SwiftSignalKit import Postbox import TelegramCore -enum SharedMediaPlayerControlAction { - case next - case previous +import TelegramUIPrivateModule + +enum SharedMediaPlayerPlaybackControlAction { case play case pause case togglePlayPause } +enum SharedMediaPlayerControlAction { + case next + case previous + case playback(SharedMediaPlayerPlaybackControlAction) + case seek(Double) + case setOrder(MusicPlaybackSettingsOrder) + case setLooping(MusicPlaybackSettingsLooping) +} + enum SharedMediaPlaylistControlAction { case next case previous @@ -46,15 +55,32 @@ struct SharedMediaPlaybackData: Equatable { } } +struct SharedMediaPlaybackAlbumArt: Equatable { + let thumbnailResource: TelegramMediaResource + let fullSizeResource: TelegramMediaResource + + static func ==(lhs: SharedMediaPlaybackAlbumArt, rhs: SharedMediaPlaybackAlbumArt) -> Bool { + if !lhs.thumbnailResource.isEqual(to: rhs.thumbnailResource) { + return false + } + + if !lhs.fullSizeResource.isEqual(to: rhs.fullSizeResource) { + return false + } + + return true + } +} + enum SharedMediaPlaybackDisplayData: Equatable { - case music(title: String?, performer: String?) + case music(title: String?, performer: String?, albumArt: SharedMediaPlaybackAlbumArt?) case voice(author: Peer?, peer: Peer?) case instantVideo(author: Peer?, peer: Peer?) static func ==(lhs: SharedMediaPlaybackDisplayData, rhs: SharedMediaPlaybackDisplayData) -> Bool { switch lhs { - case let .music(lhsTitle, lhsPerformer): - if case let .music(rhsTitle, rhsPerformer) = rhs, lhsTitle == rhsTitle, lhsPerformer == rhsPerformer { + case let .music(lhsTitle, lhsPerformer, lhsAlbumArt): + if case let .music(rhsTitle, rhsPerformer, rhsAlbumArt) = rhs, lhsTitle == rhsTitle, lhsPerformer == rhsPerformer, lhsAlbumArt == rhsAlbumArt { return true } else { return false @@ -77,29 +103,199 @@ enum SharedMediaPlaybackDisplayData: Equatable { protocol SharedMediaPlaylistItem { var stableId: AnyHashable { get } + var id: SharedMediaPlaylistItemId { get } var playbackData: SharedMediaPlaybackData? { get } var displayData: SharedMediaPlaybackDisplayData? { get } } -final class SharedMediaPlaylistState { +func arePlaylistItemsEqual(_ lhs: SharedMediaPlaylistItem?, _ rhs: SharedMediaPlaylistItem?) -> Bool { + if lhs?.stableId != rhs?.stableId { + return false + } + if lhs?.playbackData != rhs?.playbackData { + return false + } + if lhs?.displayData != rhs?.displayData { + return false + } + return true +} + +final class SharedMediaPlaylistState: Equatable { let loading: Bool + let playedToEnd: Bool let item: SharedMediaPlaylistItem? + let order: MusicPlaybackSettingsOrder + let looping: MusicPlaybackSettingsLooping - init(loading: Bool, item: SharedMediaPlaylistItem?) { + init(loading: Bool, playedToEnd: Bool, item: SharedMediaPlaylistItem?, order: MusicPlaybackSettingsOrder, looping: MusicPlaybackSettingsLooping) { self.loading = loading + self.playedToEnd = playedToEnd self.item = item + self.order = order + self.looping = looping + } + + static func ==(lhs: SharedMediaPlaylistState, rhs: SharedMediaPlaylistState) -> Bool { + if lhs.loading != rhs.loading { + return false + } + if !arePlaylistItemsEqual(lhs.item, rhs.item) { + return false + } + if lhs.order != rhs.order { + return false + } + if lhs.looping != rhs.looping { + return false + } + return true } } -protocol SharedMediaPlaylist { - var state: Signal { get } - - func control(_ action: SharedMediaPlaylistControlAction) +protocol SharedMediaPlaylistId { + func isEqual(to: SharedMediaPlaylistId) -> Bool } -private enum SharedMediaPlaybackItem { +protocol SharedMediaPlaylistItemId { + func isEqual(to: SharedMediaPlaylistItemId) -> Bool +} + +func areSharedMediaPlaylistItemIdsEqual(_ lhs: SharedMediaPlaylistItemId?, _ rhs: SharedMediaPlaylistItemId?) -> Bool { + if let lhs = lhs, let rhs = rhs { + return lhs.isEqual(to: rhs) + } else if (lhs != nil) != (rhs != nil) { + return false + } else { + return true + } +} + +protocol SharedMediaPlaylistLocation { + func isEqual(to: SharedMediaPlaylistLocation) -> Bool +} + +protocol SharedMediaPlaylist { + var id: SharedMediaPlaylistId { get } + var location: SharedMediaPlaylistLocation { get } + var state: Signal { get } + var looping: MusicPlaybackSettingsLooping { get } + + func control(_ action: SharedMediaPlaylistControlAction) + func setOrder(_ order: MusicPlaybackSettingsOrder) + func setLooping(_ looping: MusicPlaybackSettingsLooping) + + func onItemPlaybackStarted(_ item: SharedMediaPlaylistItem) +} + +final class SharedMediaPlayerItemPlaybackState: Equatable { + let playlistId: SharedMediaPlaylistId + let playlistLocation: SharedMediaPlaylistLocation + let item: SharedMediaPlaylistItem + let status: MediaPlayerStatus + let order: MusicPlaybackSettingsOrder + let looping: MusicPlaybackSettingsLooping + let playerIndex: Int32 + + init(playlistId: SharedMediaPlaylistId, playlistLocation: SharedMediaPlaylistLocation, item: SharedMediaPlaylistItem, status: MediaPlayerStatus, order: MusicPlaybackSettingsOrder, looping: MusicPlaybackSettingsLooping, playerIndex: Int32) { + self.playlistId = playlistId + self.playlistLocation = playlistLocation + self.item = item + self.status = status + self.order = order + self.looping = looping + self.playerIndex = playerIndex + } + + static func ==(lhs: SharedMediaPlayerItemPlaybackState, rhs: SharedMediaPlayerItemPlaybackState) -> Bool { + if !lhs.playlistId.isEqual(to: rhs.playlistId) { + return false + } + if !arePlaylistItemsEqual(lhs.item, rhs.item) { + return false + } + if lhs.status != rhs.status { + return false + } + if lhs.playerIndex != rhs.playerIndex { + return false + } + if lhs.order != rhs.order { + return false + } + if lhs.looping != rhs.looping { + return false + } + return true + } +} + +enum SharedMediaPlayerState: Equatable { + case loading + case item(SharedMediaPlayerItemPlaybackState) + + static func ==(lhs: SharedMediaPlayerState, rhs: SharedMediaPlayerState) -> Bool { + switch lhs { + case .loading: + if case .loading = rhs { + return true + } else { + return false + } + case let .item(item): + if case .item(item) = rhs { + return true + } else { + return false + } + } + } +} + +private enum SharedMediaPlaybackItem: Equatable { case audio(MediaPlayer) - case instantVideo(InstantVideoNode) + case instantVideo(OverlayInstantVideoNode) + + var playbackStatus: Signal { + switch self { + case let .audio(player): + return player.status + case let .instantVideo(node): + return node.status |> map { status in + if let status = status { + return status + } else { + return MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, timestamp: 0.0, seekId: 0, status: .paused) + } + } + } + } + + static func ==(lhs: SharedMediaPlaybackItem, rhs: SharedMediaPlaybackItem) -> Bool { + switch lhs { + case let .audio(lhsPlayer): + if case let .audio(rhsPlayer) = rhs, lhsPlayer === rhsPlayer { + return true + } else { + return false + } + case let .instantVideo(lhsNode): + if case let .instantVideo(rhsNode) = rhs, lhsNode === rhsNode { + return true + } else { + return false + } + } + } + + func setActionAtEnd(_ f: @escaping () -> Void) { + switch self { + case let .audio(player): + player.actionAtEnd = .action(f) + case let .instantVideo(node): + node.playbackEnded = f + } + } func play() { switch self { @@ -145,33 +341,81 @@ private enum SharedMediaPlaybackItem { node.setSoundEnabled(value) } } + + func setForceAudioToSpeaker(_ value: Bool) { + switch self { + case let .audio(player): + player.setForceAudioToSpeaker(value) + case let .instantVideo(node): + node.setForceAudioToSpeaker(value) + } + } } final class SharedMediaPlayer { - private let account: Account - private let manager: MediaManager + private weak var mediaManager: MediaManager? + private let postbox: Postbox + private let audioSession: ManagedAudioSession + private let overlayMediaManager: OverlayMediaManager + private let playerIndex: Int32 private let playlist: SharedMediaPlaylist + private var proximityManagerIndex: Int? + private let controlPlaybackWithProximity: Bool + private var forceAudioToSpeaker = false + private var stateDisposable: Disposable? - private var stateValue: SharedMediaPlaylistState? - private var playbackItem: SharedMediaPlaybackItem? + private var stateValue: SharedMediaPlaylistState? { + didSet { + if self.stateValue != oldValue { + self.state.set(.single(self.stateValue)) + } + } + } + private let state = Promise(nil) - init(account: Account, manager: MediaManager, playlist: SharedMediaPlaylist) { - self.account = account - self.manager = manager + private let playbackStateValue = Promise(nil) + var playbackState: Signal { + return self.playbackStateValue.get() + } + + private var playbackItem: SharedMediaPlaybackItem? + private var currentPlayedToEnd = false + private var scheduledPlaybackAction: SharedMediaPlayerPlaybackControlAction? + + private let markItemAsPlayedDisposable = MetaDisposable() + + var playedToEnd: (() -> Void)? + + private var inForegroundDisposable: Disposable? + + init(mediaManager: MediaManager, inForeground: Signal, postbox: Postbox, audioSession: ManagedAudioSession, overlayMediaManager: OverlayMediaManager, playlist: SharedMediaPlaylist, initialOrder: MusicPlaybackSettingsOrder, initialLooping: MusicPlaybackSettingsLooping, playerIndex: Int32, controlPlaybackWithProximity: Bool) { + self.mediaManager = mediaManager + self.postbox = postbox + self.audioSession = audioSession + self.overlayMediaManager = overlayMediaManager + playlist.setOrder(initialOrder) + playlist.setLooping(initialLooping) self.playlist = playlist + self.playerIndex = playerIndex + self.controlPlaybackWithProximity = controlPlaybackWithProximity + + if controlPlaybackWithProximity { + self.forceAudioToSpeaker = !DeviceProximityManager.shared().currentValue() + } self.stateDisposable = (playlist.state |> deliverOnMainQueue).start(next: { [weak self] state in if let strongSelf = self { + let previousPlaybackItem = strongSelf.playbackItem if state.item?.playbackData != strongSelf.stateValue?.item?.playbackData { - strongSelf.playbackItem?.pause() if let playbackItem = strongSelf.playbackItem { switch playbackItem { case .audio: - break + playbackItem.pause() case let .instantVideo(node): - strongSelf.manager.overlayMediaManager.controller?.removeNode(node) + node.setSoundEnabled(false) + strongSelf.overlayMediaManager.controller?.removeNode(node) } } strongSelf.playbackItem = nil @@ -180,42 +424,196 @@ final class SharedMediaPlayer { case .voice, .music: switch playbackData.source { case let .telegramFile(file): - strongSelf.playbackItem = .audio(MediaPlayer(audioSessionManager: strongSelf.manager.audioSession, postbox: strongSelf.account.postbox, resource: file.resource, streamable: true, video: false, preferSoftwareDecoding: false, enableSound: true)) + strongSelf.playbackItem = .audio(MediaPlayer(audioSessionManager: strongSelf.audioSession, postbox: strongSelf.postbox, resource: file.resource, streamable: playbackData.type == .music, video: false, preferSoftwareDecoding: false, enableSound: true, playAndRecord: controlPlaybackWithProximity)) } case .instantVideo: - let presentationData = strongSelf.account.telegramApplicationContext.currentPresentationData.with { $0 } - switch playbackData.source { - case let .telegramFile(file): - strongSelf.playbackItem = .instantVideo(InstantVideoNode(theme: presentationData.theme, manager: strongSelf.manager, account: strongSelf.account, source: .messageMedia(stableId: item.stableId, file: file), priority: 0, withSound: true)) + if let mediaManager = strongSelf.mediaManager, let item = item as? MessageMediaPlaylistItem { + switch playbackData.source { + case let .telegramFile(file): + let videoNode = OverlayInstantVideoNode(postbox: strongSelf.postbox, audioSession: strongSelf.audioSession, manager: mediaManager.universalVideoManager, content: NativeVideoContent(id: .message(item.message.id, file.fileId), file: file, streamVideo: false, enableSound: false), close: { [weak mediaManager] in + mediaManager?.setPlaylist(nil, type: .voice) + }) + strongSelf.playbackItem = .instantVideo(videoNode) + videoNode.setSoundEnabled(true) + } } } } if let playbackItem = strongSelf.playbackItem { + playbackItem.setForceAudioToSpeaker(strongSelf.forceAudioToSpeaker) + playbackItem.setActionAtEnd({ + Queue.mainQueue().async { + if let strongSelf = self { + switch strongSelf.playlist.looping { + case .item: + strongSelf.playbackItem?.seek(0.0) + strongSelf.playbackItem?.play() + default: + strongSelf.scheduledPlaybackAction = .play + strongSelf.control(.next) + } + } + } + }) switch playbackItem { case .audio: break case let .instantVideo(node): - strongSelf.manager.overlayMediaManager.controller?.addNode(node) + strongSelf.overlayMediaManager.controller?.addNode(node) + } + + if let scheduledPlaybackAction = strongSelf.scheduledPlaybackAction { + strongSelf.scheduledPlaybackAction = nil + switch scheduledPlaybackAction { + case .play: + switch playbackItem { + case let .audio(player): + player.play() + case let .instantVideo(node): + node.playOnceWithSound(playAndRecord: controlPlaybackWithProximity) + } + case .pause: + playbackItem.pause() + case .togglePlayPause: + playbackItem.togglePlayPause() + } } } } + + if strongSelf.currentPlayedToEnd != state.playedToEnd { + strongSelf.currentPlayedToEnd = state.playedToEnd + if state.playedToEnd { + if let playbackItem = strongSelf.playbackItem { + switch playbackItem { + case let .audio(player): + player.pause() + case let .instantVideo(node): + node.setSoundEnabled(false) + } + } + //strongSelf.playbackItem?.seek(0.0) + strongSelf.playedToEnd?() + } + } + + let updatePlaybackState = strongSelf.stateValue != state || strongSelf.playbackItem != previousPlaybackItem strongSelf.stateValue = state + + if updatePlaybackState { + let playlistId = strongSelf.playlist.id + let playlistLocation = strongSelf.playlist.location + let playerIndex = strongSelf.playerIndex + if let playbackItem = strongSelf.playbackItem, let item = state.item { + strongSelf.playbackStateValue.set(playbackItem.playbackStatus |> map { itemStatus in + return .item(SharedMediaPlayerItemPlaybackState(playlistId: playlistId, playlistLocation: playlistLocation, item: item, status: itemStatus, order: state.order, looping: state.looping, playerIndex: playerIndex)) + }) + strongSelf.markItemAsPlayedDisposable.set((playbackItem.playbackStatus + |> filter { status in + if case .playing = status.status { + return true + } else { + return false + } + } + |> take(1) + |> deliverOnMainQueue).start(next: { next in + if let strongSelf = self { + strongSelf.playlist.onItemPlaybackStarted(item) + } + })) + } else { + if let _ = state.item { + strongSelf.playbackStateValue.set(.single(.loading)) + } else { + strongSelf.playbackStateValue.set(.single(nil)) + if !state.loading { + if let proximityManagerIndex = strongSelf.proximityManagerIndex { + DeviceProximityManager.shared().remove(proximityManagerIndex) + } + } + } + } + } } }) + + if controlPlaybackWithProximity { + self.proximityManagerIndex = DeviceProximityManager.shared().add { [weak self] value in + let forceAudioToSpeaker = !value + if let strongSelf = self, strongSelf.forceAudioToSpeaker != forceAudioToSpeaker { + strongSelf.forceAudioToSpeaker = forceAudioToSpeaker + strongSelf.playbackItem?.setForceAudioToSpeaker(forceAudioToSpeaker) + if !forceAudioToSpeaker { + strongSelf.control(.playback(.play)) + } else { + strongSelf.control(.playback(.pause)) + } + } + } + } } deinit { self.stateDisposable?.dispose() + self.markItemAsPlayedDisposable.dispose() + self.inForegroundDisposable?.dispose() + + if let proximityManagerIndex = self.proximityManagerIndex { + DeviceProximityManager.shared().remove(proximityManagerIndex) + } + + if let playbackItem = self.playbackItem { + switch playbackItem { + case .audio: + playbackItem.pause() + case let .instantVideo(node): + node.setSoundEnabled(false) + self.overlayMediaManager.controller?.removeNode(node) + } + } } func control(_ action: SharedMediaPlayerControlAction) { switch action { case .next: + self.scheduledPlaybackAction = .play self.playlist.control(.next) case .previous: + self.scheduledPlaybackAction = .play self.playlist.control(.previous) - case .play, .pause, .togglePlayPause: - break + case let .playback(action): + if let playbackItem = self.playbackItem { + switch action { + case .play: + playbackItem.play() + case .pause: + playbackItem.pause() + case .togglePlayPause: + playbackItem.togglePlayPause() + } + } else { + self.scheduledPlaybackAction = action + } + case let .seek(timestamp): + if let playbackItem = self.playbackItem { + playbackItem.seek(timestamp) + } + case let .setOrder(order): + self.playlist.setOrder(order) + case let .setLooping(looping): + self.playlist.setLooping(looping) + } + } + + func stop() { + if let playbackItem = self.playbackItem { + switch playbackItem { + case let .audio(player): + player.pause() + case let .instantVideo(node): + node.setSoundEnabled(false) + } } } } diff --git a/TelegramUI/SoftwareVideoLayerFrameManager.swift b/TelegramUI/SoftwareVideoLayerFrameManager.swift index 3e509114ba..763615769d 100644 --- a/TelegramUI/SoftwareVideoLayerFrameManager.swift +++ b/TelegramUI/SoftwareVideoLayerFrameManager.swift @@ -23,12 +23,19 @@ final class SoftwareVideoLayerFrameManager { private let queue: ThreadPoolQueue private let layerHolder: SampleBufferLayer + private var rotationAngle: CGFloat = 0.0 + private var aspect: CGFloat = 1.0 + + private var layerRotationAngleAndAspect: (CGFloat, CGFloat)? + init(account: Account, resource: MediaResource, layerHolder: SampleBufferLayer) { nextWorker += 1 self.account = account self.resource = resource self.queue = ThreadPoolQueue(threadPool: workers) self.layerHolder = layerHolder + layerHolder.layer.videoGravity = .resizeAspectFill + layerHolder.layer.masksToBounds = true self.fetchDisposable = account.postbox.mediaBox.fetchedResource(resource, tag: TelegramMediaResourceFetchTag(statsCategory: .video)).start() } @@ -70,6 +77,14 @@ final class SoftwareVideoLayerFrameManager { if self.layerHolder.layer.status == .failed { self.layerHolder.layer.flush() } + /*if self.layerRotationAngleAndAspect?.0 != self.rotationAngle || self.layerRotationAngleAndAspect?.1 != self.aspect { + self.layerRotationAngleAndAspect = (self.rotationAngle, self.aspect) + var transform = CGAffineTransform(rotationAngle: CGFloat(self.rotationAngle)) + if !self.rotationAngle.isZero { + transform = transform.scaledBy(x: CGFloat(self.aspect), y: CGFloat(1.0 / self.aspect)) + } + self.layerHolder.layer.setAffineTransform(transform) + }*/ self.layerHolder.layer.enqueue(frame.sampleBuffer) } } @@ -94,13 +109,17 @@ final class SoftwareVideoLayerFrameManager { applyQueue.async { if let strongSelf = self { strongSelf.polling = false + if let (_, rotationAngle, aspect, _) = frameAndLoop { + strongSelf.rotationAngle = rotationAngle + strongSelf.aspect = aspect + } if let frame = frameAndLoop?.0 { if strongSelf.minPts == nil || CMTimeCompare(strongSelf.minPts!, frame.position) < 0 { strongSelf.minPts = frame.position } strongSelf.frames.append(frame) } - if let loop = frameAndLoop?.1, loop { + if let loop = frameAndLoop?.3, loop { strongSelf.maxPts = strongSelf.minPts strongSelf.minPts = nil } diff --git a/TelegramUI/SoftwareVideoSource.swift b/TelegramUI/SoftwareVideoSource.swift index e91647933b..ce7f658b0b 100644 --- a/TelegramUI/SoftwareVideoSource.swift +++ b/TelegramUI/SoftwareVideoSource.swift @@ -31,14 +31,16 @@ private final class SoftwareVideoStream { let duration: CMTime let decoder: FFMpegMediaVideoFrameDecoder let rotationAngle: Double + let aspect: Double - init(index: Int, fps: CMTime, timebase: CMTime, duration: CMTime, decoder: FFMpegMediaVideoFrameDecoder, rotationAngle: Double) { + init(index: Int, fps: CMTime, timebase: CMTime, duration: CMTime, decoder: FFMpegMediaVideoFrameDecoder, rotationAngle: Double, aspect: Double) { self.index = index self.fps = fps self.timebase = timebase self.duration = duration self.decoder = decoder self.rotationAngle = rotationAngle + self.aspect = aspect } } @@ -116,7 +118,9 @@ final class SoftwareVideoSource { } } - videoStream = SoftwareVideoStream(index: streamIndex, fps: fps, timebase: timebase, duration: duration, decoder: FFMpegMediaVideoFrameDecoder(codecContext: codecContext), rotationAngle: rotationAngle) + let aspect = Double(codecPar.pointee.width) / Double(codecPar.pointee.height) + + videoStream = SoftwareVideoStream(index: streamIndex, fps: fps, timebase: timebase, duration: duration, decoder: FFMpegMediaVideoFrameDecoder(codecContext: codecContext), rotationAngle: rotationAngle, aspect: aspect) break } else { var codecContextRef: UnsafeMutablePointer? = codecContext @@ -212,7 +216,7 @@ final class SoftwareVideoSource { return (frames.first, endOfStream) } - func readFrame(maxPts: CMTime?) -> (MediaTrackFrame?, Bool) { + func readFrame(maxPts: CMTime?) -> (MediaTrackFrame?, CGFloat, CGFloat, Bool) { if let videoStream = self.videoStream { let (decodableFrame, loop) = self.readDecodableFrame() if let decodableFrame = decodableFrame { @@ -220,12 +224,12 @@ final class SoftwareVideoSource { if let maxPts = maxPts, CMTimeCompare(decodableFrame.pts, maxPts) < 0 { ptsOffset = maxPts } - return (videoStream.decoder.decode(frame: decodableFrame, ptsOffset: ptsOffset), loop) + return (videoStream.decoder.decode(frame: decodableFrame, ptsOffset: ptsOffset), CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect), loop) } else { - return (nil, loop) + return (nil, CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect), loop) } } else { - return (nil, false) + return (nil, 0.0, 1.0, false) } } } diff --git a/TelegramUI/SoftwareVideoThumbnailLayer.swift b/TelegramUI/SoftwareVideoThumbnailLayer.swift index a678438d34..88db5a3f5b 100644 --- a/TelegramUI/SoftwareVideoThumbnailLayer.swift +++ b/TelegramUI/SoftwareVideoThumbnailLayer.swift @@ -18,12 +18,12 @@ final class SoftwareVideoThumbnailLayer: CALayer { init(account: Account, file: TelegramMediaFile) { super.init() - self.backgroundColor = UIColor.black.cgColor + self.backgroundColor = UIColor.white.cgColor self.contentsGravity = "resizeAspectFill" self.masksToBounds = true if let dimensions = file.dimensions { - self.disposable = (mediaGridMessageVideo(account: account, video: file) |> deliverOn(account.graphicsThreadPool)).start(next: { [weak self] transform in + self.disposable = (mediaGridMessageVideo(postbox: account.postbox, video: file) |> deliverOn(account.graphicsThreadPool)).start(next: { [weak self] transform in var boundingSize = dimensions.aspectFilled(CGSize(width: 93.0, height: 93.0)) let imageSize = boundingSize diff --git a/TelegramUI/StickerPackPreviewControllerNode.swift b/TelegramUI/StickerPackPreviewControllerNode.swift index 8a2dd3f8d5..253a7b7dd2 100644 --- a/TelegramUI/StickerPackPreviewControllerNode.swift +++ b/TelegramUI/StickerPackPreviewControllerNode.swift @@ -5,29 +5,9 @@ import SwiftSignalKit import Postbox import TelegramCore -private let defaultBackgroundColor: UIColor = UIColor(white: 1.0, alpha: 1.0) -private let highlightedBackgroundColor: UIColor = UIColor(white: 0.9, alpha: 1.0) -private let separatorColor: UIColor = UIColor(rgb: 0xbcbbc1) - -private let roundedBackground = generateStretchableFilledCircleImage(radius: 16.0, color: .white) -private let highlightedRoundedBackground = generateStretchableFilledCircleImage(radius: 16.0, color: highlightedBackgroundColor) - -private let halfRoundedBackground = generateImage(CGSize(width: 32.0, height: 32.0), rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(UIColor.white.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height))) - context.fill(CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height / 2.0))) -})?.stretchableImage(withLeftCapWidth: 16, topCapHeight: 1) - -private let highlightedHalfRoundedBackground = generateImage(CGSize(width: 32.0, height: 32.0), rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(highlightedBackgroundColor.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height))) - context.fill(CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height / 2.0))) -})?.stretchableImage(withLeftCapWidth: 16, topCapHeight: 1) - final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrollViewDelegate { private let account: Account + private let presentationData: PresentationData private var containerLayout: (ContainerViewLayout, CGFloat)? @@ -67,6 +47,25 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol init(account: Account) { self.account = account + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + + let theme = self.presentationData.theme + let halfRoundedBackground = generateImage(CGSize(width: 32.0, height: 32.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(theme.actionSheet.opaqueItemBackgroundColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height))) + context.fill(CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height / 2.0))) + })?.stretchableImage(withLeftCapWidth: 16, topCapHeight: 1) + + let highlightedHalfRoundedBackground = generateImage(CGSize(width: 32.0, height: 32.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(theme.actionSheet.opaqueItemHighlightedBackgroundColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height))) + context.fill(CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height / 2.0))) + })?.stretchableImage(withLeftCapWidth: 16, topCapHeight: 1) + + let roundedBackground = generateStretchableFilledCircleImage(radius: 16.0, color: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor) + let highlightedRoundedBackground = generateStretchableFilledCircleImage(radius: 16.0, color: self.presentationData.theme.actionSheet.opaqueItemHighlightedBackgroundColor) self.wrappingScrollNode = ASScrollNode() self.wrappingScrollNode.view.alwaysBounceVertical = true @@ -80,20 +79,14 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol self.cancelButtonNode.displaysAsynchronously = false self.cancelButtonNode.setBackgroundImage(roundedBackground, for: .normal) self.cancelButtonNode.setBackgroundImage(highlightedRoundedBackground, for: .highlighted) - //self.cancelButtonNode.cornerRadius = 16.0 - //self.cancelButtonNode.clipsToBounds = true self.contentContainerNode = ASDisplayNode() - //self.contentContainerNode.cornerRadius = 16.0 - //self.contentContainerNode.clipsToBounds = true self.contentContainerNode.isOpaque = false self.contentBackgroundNode = ASImageNode() self.contentBackgroundNode.displaysAsynchronously = false self.contentBackgroundNode.displayWithoutProcessing = true self.contentBackgroundNode.image = roundedBackground - //self.contentBackgroundNode.cornerRadius = 16.0 - //self.contentBackgroundNode.clipsToBounds = true self.contentGridNode = GridNode() @@ -108,12 +101,12 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol self.contentSeparatorNode = ASDisplayNode() self.contentSeparatorNode.isLayerBacked = true self.contentSeparatorNode.displaysAsynchronously = false - self.contentSeparatorNode.backgroundColor = separatorColor + self.contentSeparatorNode.backgroundColor = self.presentationData.theme.actionSheet.opaqueItemSeparatorColor self.installActionSeparatorNode = ASDisplayNode() self.installActionSeparatorNode.isLayerBacked = true self.installActionSeparatorNode.displaysAsynchronously = false - self.installActionSeparatorNode.backgroundColor = separatorColor + self.installActionSeparatorNode.backgroundColor = self.presentationData.theme.actionSheet.opaqueItemSeparatorColor super.init() @@ -133,32 +126,7 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol self.wrappingScrollNode.view.delegate = self self.addSubnode(self.wrappingScrollNode) - self.cancelButtonNode.setTitle("Cancel", with: Font.medium(20.0), with: UIColor(rgb: 0x007ee5), for: .normal) - /*self.cancelButtonNode.backgroundColor = defaultBackgroundColor - self.cancelButtonNode.highligthedChanged = { [weak self] highlighted in - if let strongSelf = self { - if highlighted { - strongSelf.cancelButtonNode.backgroundColor = highlightedBackgroundColor - } else { - UIView.animate(withDuration: 0.3, animations: { - strongSelf.cancelButtonNode.backgroundColor = defaultBackgroundColor - }) - } - } - }*/ - - /*self.installActionButtonNode.backgroundColor = defaultBackgroundColor - self.installActionButtonNode.highligthedChanged = { [weak self] highlighted in - if let strongSelf = self { - if highlighted { - strongSelf.installActionButtonNode.backgroundColor = highlightedBackgroundColor - } else { - UIView.animate(withDuration: 0.3, animations: { - strongSelf.installActionButtonNode.backgroundColor = defaultBackgroundColor - }) - } - } - }*/ + self.cancelButtonNode.setTitle(self.presentationData.strings.Common_Cancel, with: Font.medium(20.0), with: self.presentationData.theme.actionSheet.standardActionTextColor, for: .normal) self.wrappingScrollNode.addSubnode(self.cancelButtonNode) self.cancelButtonNode.addTarget(self, action: #selector(self.cancelButtonPressed), forControlEvents: .touchUpInside) @@ -203,10 +171,11 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol var insets = layout.insets(options: [.statusBar]) insets.top = max(10.0, insets.top) + let cleanInsets = layout.insets(options: [.statusBar]) transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - let bottomInset: CGFloat = 10.0 + let bottomInset: CGFloat = 10.0 + cleanInsets.bottom let buttonHeight: CGFloat = 57.0 let sectionSpacing: CGFloat = 8.0 let titleAreaHeight: CGFloat = 51.0 @@ -243,7 +212,7 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol } itemCount = items.count if !self.didSetItems { - self.contentTitleNode.attributedText = NSAttributedString(string: info.title, font: Font.medium(20.0), textColor: .black) + self.contentTitleNode.attributedText = NSAttributedString(string: info.title, font: Font.medium(20.0), textColor: self.presentationData.theme.actionSheet.primaryTextColor) self.didSetItems = true animateIn = true @@ -315,8 +284,9 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol if let (layout, _) = self.containerLayout { var insets = layout.insets(options: [.statusBar]) insets.top = max(10.0, insets.top) + let cleanInsets = layout.insets(options: [.statusBar]) - let bottomInset: CGFloat = 10.0 + let bottomInset: CGFloat = 10.0 + cleanInsets.bottom let buttonHeight: CGFloat = 57.0 let sectionSpacing: CGFloat = 8.0 let titleAreaHeight: CGFloat = 51.0 @@ -433,25 +403,25 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol switch stickerPack { case .none, .fetching: self.installActionSeparatorNode.alpha = 0.0 - self.installActionButtonNode.setTitle("", with: Font.medium(20.0), with: UIColor(rgb: 0x007ee5), for: .normal) + self.installActionButtonNode.setTitle("", with: Font.medium(20.0), with: self.presentationData.theme.actionSheet.standardActionTextColor, for: .normal) case let .result(info, _, installed): self.installActionSeparatorNode.alpha = 1.0 if installed { let text: String if info.id.namespace == Namespaces.ItemCollection.CloudStickerPacks { - text = "Remove \(info.count) stickers" + text = self.presentationData.strings.StickerPack_RemoveStickerCount(info.count) } else { - text = "Remove \(info.count) masks" + text = self.presentationData.strings.StickerPack_RemoveMaskCount(info.count) } - self.installActionButtonNode.setTitle(text, with: Font.regular(20.0), with: UIColor(rgb: 0xff3b30), for: .normal) + self.installActionButtonNode.setTitle(text, with: Font.regular(20.0), with: self.presentationData.theme.actionSheet.controlAccentColor, for: .normal) } else { let text: String if info.id.namespace == Namespaces.ItemCollection.CloudStickerPacks { - text = "Add \(info.count) stickers" + text = self.presentationData.strings.StickerPack_AddStickerCount(info.count) } else { - text = "Add \(info.count) masks" + text = self.presentationData.strings.StickerPack_AddMaskCount(info.count) } - self.installActionButtonNode.setTitle(text, with: Font.regular(20.0), with: UIColor(rgb: 0x007ee5), for: .normal) + self.installActionButtonNode.setTitle(text, with: Font.regular(20.0), with: self.presentationData.theme.actionSheet.controlAccentColor, for: .normal) } } } diff --git a/TelegramUI/StickerPackPreviewGridItem.swift b/TelegramUI/StickerPackPreviewGridItem.swift index 7c6c6b5d28..079f22bbcb 100644 --- a/TelegramUI/StickerPackPreviewGridItem.swift +++ b/TelegramUI/StickerPackPreviewGridItem.swift @@ -100,7 +100,7 @@ final class StickerPackPreviewGridItemNode: GridItemNode { } self.textNode.attributedText = NSAttributedString(string: text, font: textFont, textColor: .black, paragraphAlignment: .right) if let dimensions = stickerItem.file.dimensions { - self.imageNode.setSignal(account: account, signal: chatMessageSticker(account: account, file: stickerItem.file, small: true)) + self.imageNode.setSignal(chatMessageSticker(account: account, file: stickerItem.file, small: true)) self.stickerFetchedDisposable.set(freeMediaFileInteractiveFetched(account: account, file: stickerItem.file).start()) self.currentState = (account, stickerItem, dimensions) diff --git a/TelegramUI/StickerPreviewController.swift b/TelegramUI/StickerPreviewController.swift index 54d65daeba..bd7d857273 100644 --- a/TelegramUI/StickerPreviewController.swift +++ b/TelegramUI/StickerPreviewController.swift @@ -28,6 +28,8 @@ final class StickerPreviewController: ViewController { self.item = item super.init(navigationBarTheme: nil) + + self.statusBar.statusBarStyle = .Ignore } required init(coder aDecoder: NSCoder) { diff --git a/TelegramUI/StickerPreviewControllerNode.swift b/TelegramUI/StickerPreviewControllerNode.swift index f6dd4772c4..ba35c64832 100644 --- a/TelegramUI/StickerPreviewControllerNode.swift +++ b/TelegramUI/StickerPreviewControllerNode.swift @@ -7,6 +7,7 @@ import TelegramCore final class StickerPreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { private let account: Account + private let presentationData: PresentationData private let dimNode: ASDisplayNode @@ -21,9 +22,10 @@ final class StickerPreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { init(account: Account) { self.account = account + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } self.dimNode = ASDisplayNode() - self.dimNode.backgroundColor = UIColor(white: 1.0, alpha: 0.6) + self.dimNode.backgroundColor = presentationData.theme.list.plainBackgroundColor.withAlphaComponent(0.6) self.textNode = ASTextNode() self.imageNode = TransformImageNode() @@ -135,7 +137,7 @@ final class StickerPreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(32.0), textColor: .black) break } - self.imageNode.setSignal(account: account, signal: chatMessageSticker(account: account, file: item.file, small: false)) + self.imageNode.setSignal(chatMessageSticker(account: account, file: item.file, small: false)) if let (layout, navigationBarHeight) = self.containerLayout { self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) diff --git a/TelegramUI/StorageUsageController.swift b/TelegramUI/StorageUsageController.swift index 93df70b5fa..28ddba4b09 100644 --- a/TelegramUI/StorageUsageController.swift +++ b/TelegramUI/StorageUsageController.swift @@ -116,7 +116,7 @@ private enum StorageUsageEntry: ItemListNodeEntry { case let .keepMediaInfo(theme, text): return ItemListTextItem(theme: theme, text: .markdown(text), sectionId: self.section) case let .collecting(theme, text): - return ItemListActivityTextItem(displayActivity: true, text: NSAttributedString(string: text, textColor: theme.list.freeTextColor), sectionId: self.section) + return CalculatingCacheSizeItem(theme: theme, title: text, sectionId: self.section, style: .blocks) case let .peersHeader(theme, text): return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) case let .peer(_, theme, strings, peer, value): diff --git a/TelegramUI/StoredMessageFromSearchPeer.swift b/TelegramUI/StoredMessageFromSearchPeer.swift index 6a5b2e6da6..941e42d212 100644 --- a/TelegramUI/StoredMessageFromSearchPeer.swift +++ b/TelegramUI/StoredMessageFromSearchPeer.swift @@ -24,7 +24,7 @@ func storedMessageFromSearch(account: Account, message: Message) -> Signal UIScrollView? { + for subview in view.subviews { + let subviewPoint = view.convert(point, to: subview) + if subview.frame.contains(point), let result = traceScrollView(view: subview, point: subviewPoint) { + return result + } + } + if let scrollView = view as? UIScrollView { + return scrollView + } + return nil +} + +class SwipeToDismissGestureRecognizer: UIGestureRecognizer, UIGestureRecognizerDelegate { + private var beginPosition = CGPoint() + + override init(target: Any?, action: Selector?) { + super.init(target: target, action: action) + + self.delegate = self + } + + override func reset() { + super.reset() + + self.state = .possible + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } + + override func touchesBegan(_ touches: Set, with event: UIEvent) { + super.touchesBegan(touches, with: event) + + guard let touch = touches.first, let view = self.view else { + self.state = .failed + return + } + + var found = false + let point = touch.location(in: self.view) + if let scrollView = traceScrollView(view: view, point: point) { + let contentOffset = scrollView.contentOffset + let contentInset = scrollView.contentInset + if contentOffset.y.isLessThanOrEqualTo(contentInset.top) { + found = true + } + } + if found { + self.beginPosition = point + } else { + self.state = .failed + } + } + + override func touchesMoved(_ touches: Set, with event: UIEvent) { + super.touchesMoved(touches, with: event) + + guard let touch = touches.first, let view = self.view else { + self.state = .failed + return + } + + let point = touch.location(in: self.view) + + let translation = point.offsetBy(dx: -self.beginPosition.x, dy: -self.beginPosition.y) + + if self.state == .possible { + if abs(translation.x) > 5.0 { + self.state = .failed + return + } + var lockDown = false + let point = touch.location(in: self.view) + if let scrollView = traceScrollView(view: view, point: point) { + let contentOffset = scrollView.contentOffset + let contentInset = scrollView.contentInset + if contentOffset.y.isLessThanOrEqualTo(contentInset.top) { + lockDown = true + } + } + if lockDown { + if translation.y > 2.0 { + self.state = .began + } + } else { + self.state = .failed + } + } else { + self.state = .changed + } + } + + override func touchesEnded(_ touches: Set, with event: UIEvent) { + super.touchesEnded(touches, with: event) + + self.state = .failed + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool { + if otherGestureRecognizer is UIPanGestureRecognizer { + return true + } + return false + } +} diff --git a/TelegramUI/SystemVideoContent.swift b/TelegramUI/SystemVideoContent.swift new file mode 100644 index 0000000000..813157bea0 --- /dev/null +++ b/TelegramUI/SystemVideoContent.swift @@ -0,0 +1,244 @@ +import Foundation +import AsyncDisplayKit +import Display +import SwiftSignalKit +import Postbox +import TelegramCore + +import LegacyComponents + +final class SystemVideoContent: UniversalVideoContent { + let id: AnyHashable + let url: String + let image: TelegramMediaImage + let dimensions: CGSize + let duration: Int32 + + init(url: String, image: TelegramMediaImage, dimensions: CGSize, duration: Int32) { + self.id = AnyHashable(url) + self.url = url + self.image = image + self.dimensions = dimensions + self.duration = duration + } + + func makeContentNode(postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode { + return SystemVideoContentNode(postbox: postbox, audioSessionManager: audioSession, url: self.url, image: self.image, intrinsicDimensions: self.dimensions, approximateDuration: self.duration) + } +} + +private final class SystemVideoContentNode: ASDisplayNode, UniversalVideoContentNode { + private let url: String + private let intrinsicDimensions: CGSize + private let approximateDuration: Int32 + + private let audioSessionManager: ManagedAudioSession + private let audioSessionDisposable = MetaDisposable() + private var hasAudioSession = false + + private let playbackCompletedListeners = Bag<() -> Void>() + + private var initializedStatus = false + private var statusValue = MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, timestamp: 0.0, seekId: 0, status: .paused) + private var isBuffering = false + private let _status = ValuePromise() + var status: Signal { + return self._status.get() + } + + private let _ready = Promise() + var ready: Signal { + return self._ready.get() + } + + private let _preloadCompleted = ValuePromise() + var preloadCompleted: Signal { + return self._preloadCompleted.get() + } + + private let imageNode: TransformImageNode + private let playerItem: AVPlayerItem + private let player: AVPlayer + private let playerNode: ASDisplayNode + + private var loadProgressDisposable: Disposable? + private var statusDisposable: Disposable? + + private var didPlayToEndTimeObserver: NSObjectProtocol? + + init(postbox: Postbox, audioSessionManager: ManagedAudioSession, url: String, image: TelegramMediaImage, intrinsicDimensions: CGSize, approximateDuration: Int32) { + self.audioSessionManager = audioSessionManager + + self.url = url + self.intrinsicDimensions = intrinsicDimensions + self.approximateDuration = approximateDuration + + self.imageNode = TransformImageNode() + self.imageNode.isLayerBacked = true + + self.playerItem = AVPlayerItem(url: URL(string: url)!) + let player = AVPlayer(playerItem: self.playerItem) + self.player = player + + self.playerNode = ASDisplayNode() + self.playerNode.setLayerBlock({ + return AVPlayerLayer(player: player) + }) + + self.playerNode.frame = CGRect(origin: CGPoint(), size: intrinsicDimensions) + self.isBuffering = true + + super.init() + + self.imageNode.setSignal(chatMessagePhoto(postbox: postbox, photo: image)) + + self.addSubnode(self.imageNode) + self.addSubnode(self.playerNode) + self.player.actionAtItemEnd = .pause + + self.didPlayToEndTimeObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: self.player.currentItem, queue: nil, using: { [weak self] notification in + if let strongSelf = self { + strongSelf.player.seek(to: CMTime(seconds: 0.0, preferredTimescale: 30)) + strongSelf.play() + } + }) + + self.imageNode.imageUpdated = { [weak self] in + self?._ready.set(.single(Void())) + } + + self.player.addObserver(self, forKeyPath: "rate", options: [], context: nil) + playerItem.addObserver(self, forKeyPath: "playbackBufferEmpty", options: .new, context: nil) + playerItem.addObserver(self, forKeyPath: "playbackLikelyToKeepUp", options: .new, context: nil) + playerItem.addObserver(self, forKeyPath: "playbackBufferFull", options: .new, context: nil) + } + + deinit { + self.player.removeObserver(self, forKeyPath: "rate") + self.playerItem.removeObserver(self, forKeyPath: "playbackBufferEmpty") + self.playerItem.removeObserver(self, forKeyPath: "playbackLikelyToKeepUp") + self.playerItem.removeObserver(self, forKeyPath: "playbackBufferFull") + + self.audioSessionDisposable.dispose() + + self.loadProgressDisposable?.dispose() + self.statusDisposable?.dispose() + + if let didPlayToEndTimeObserver = self.didPlayToEndTimeObserver { + NotificationCenter.default.removeObserver(didPlayToEndTimeObserver) + } + } + + override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { + if keyPath == "rate" { + let isPlaying = !self.player.rate.isZero + let status: MediaPlayerPlaybackStatus + if self.isBuffering { + status = .buffering(initial: false, whilePlaying: isPlaying) + } else { + status = isPlaying ? .playing : .paused + } + self.statusValue = MediaPlayerStatus(generationTimestamp: 0.0, duration: Double(self.approximateDuration), timestamp: 0.0, seekId: 0, status: status) + self._status.set(self.statusValue) + } else if keyPath == "playbackBufferEmpty" { + let isPlaying = !self.player.rate.isZero + let status: MediaPlayerPlaybackStatus + self.isBuffering = true + if self.isBuffering { + status = .buffering(initial: false, whilePlaying: isPlaying) + } else { + status = isPlaying ? .playing : .paused + } + self.statusValue = MediaPlayerStatus(generationTimestamp: 0.0, duration: Double(self.approximateDuration), timestamp: 0.0, seekId: 0, status: status) + self._status.set(self.statusValue) + } else if keyPath == "playbackLikelyToKeepUp" || keyPath == "playbackBufferFull" { + let isPlaying = !self.player.rate.isZero + let status: MediaPlayerPlaybackStatus + self.isBuffering = false + if self.isBuffering { + status = .buffering(initial: false, whilePlaying: isPlaying) + } else { + status = isPlaying ? .playing : .paused + } + self.statusValue = MediaPlayerStatus(generationTimestamp: 0.0, duration: Double(self.approximateDuration), timestamp: 0.0, seekId: 0, status: status) + self._status.set(self.statusValue) + } + } + + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + transition.updatePosition(node: self.playerNode, position: CGPoint(x: size.width / 2.0, y: size.height / 2.0)) + transition.updateTransformScale(node: self.playerNode, scale: size.width / self.intrinsicDimensions.width) + + transition.updateFrame(node: self.imageNode, frame: CGRect(origin: CGPoint(), size: size)) + + let makeImageLayout = self.imageNode.asyncLayout() + let applyImageLayout = makeImageLayout(TransformImageArguments(corners: ImageCorners(), imageSize: size, boundingSize: size, intrinsicInsets: UIEdgeInsets())) + applyImageLayout() + } + + func play() { + assert(Queue.mainQueue().isCurrent()) + if !self.initializedStatus { + self._status.set(MediaPlayerStatus(generationTimestamp: 0.0, duration: Double(self.approximateDuration), timestamp: 0.0, seekId: 0, status: .buffering(initial: true, whilePlaying: true))) + } + if !self.hasAudioSession { + self.audioSessionDisposable.set(self.audioSessionManager.push(audioSessionType: .play, activate: { [weak self] _ in + self?.hasAudioSession = true + self?.player.play() + }, deactivate: { [weak self] in + self?.hasAudioSession = false + self?.player.pause() + return .complete() + })) + } else { + self.player.play() + } + } + + func pause() { + assert(Queue.mainQueue().isCurrent()) + if !self.initializedStatus { + self._status.set(MediaPlayerStatus(generationTimestamp: 0.0, duration: Double(self.approximateDuration), timestamp: 0.0, seekId: 0, status: .paused)) + } + self.player.pause() + } + + func togglePlayPause() { + assert(Queue.mainQueue().isCurrent()) + if self.player.rate.isZero { + self.play() + } else { + self.pause() + } + } + + func setSoundEnabled(_ value: Bool) { + assert(Queue.mainQueue().isCurrent()) + } + + func seek(_ timestamp: Double) { + assert(Queue.mainQueue().isCurrent()) + //self.playerView.seek(toPosition: timestamp) + } + + func playOnceWithSound(playAndRecord: Bool) { + } + + func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) { + } + + func continuePlayingWithoutSound() { + } + + func addPlaybackCompleted(_ f: @escaping () -> Void) -> Int { + return self.playbackCompletedListeners.add(f) + } + + func removePlaybackCompleted(_ index: Int) { + self.playbackCompletedListeners.remove(index) + } + + func fetchControl(_ control: UniversalVideoNodeFetchControl) { + } +} + diff --git a/TelegramUI/TelegramAccountAuxiliaryMethods.swift b/TelegramUI/TelegramAccountAuxiliaryMethods.swift index 505ba76e62..28e2de34ed 100644 --- a/TelegramUI/TelegramAccountAuxiliaryMethods.swift +++ b/TelegramUI/TelegramAccountAuxiliaryMethods.swift @@ -19,6 +19,13 @@ public let telegramAccountAuxiliaryMethods = AccountAuxiliaryMethods(updatePeerC return fetchPhotoLibraryResource(localIdentifier: photoLibraryResource.localIdentifier) } else if let mapSnapshotResource = resource as? MapSnapshotMediaResource { return fetchMapSnapshotResource(resource: mapSnapshotResource) + } else if let resource = resource as? ExternalMusicAlbumArtResource { + return fetchExternalMusicAlbumArtResource(account: account, resource: resource) } return nil +}, fetchResourceMediaReferenceHash: { resource in + if let resource = resource as? VideoLibraryMediaResource { + return fetchVideoLibraryMediaResourceHash(resource: resource) + } + return .single(nil) }) diff --git a/TelegramUI/TelegramApplicationContext.swift b/TelegramUI/TelegramApplicationContext.swift index c2764b0619..03121f69f5 100644 --- a/TelegramUI/TelegramApplicationContext.swift +++ b/TelegramUI/TelegramApplicationContext.swift @@ -6,13 +6,15 @@ import TelegramCore public final class TelegramApplicationBindings { public let openUrl: (String) -> Void + public let canOpenUrl: (String) -> Bool public let getTopWindow: () -> UIWindow? public let displayNotification: (String) -> Void public let applicationInForeground: Signal public let applicationIsActive: Signal - public init(openUrl: @escaping (String) -> Void, getTopWindow: @escaping () -> UIWindow?, displayNotification: @escaping (String) -> Void, applicationInForeground: Signal, applicationIsActive: Signal) { + public init(openUrl: @escaping (String) -> Void, canOpenUrl: @escaping (String) -> Bool, getTopWindow: @escaping () -> UIWindow?, displayNotification: @escaping (String) -> Void, applicationInForeground: Signal, applicationIsActive: Signal) { self.openUrl = openUrl + self.canOpenUrl = canOpenUrl self.getTopWindow = getTopWindow self.displayNotification = displayNotification self.applicationInForeground = applicationInForeground @@ -26,7 +28,7 @@ public final class TelegramApplicationContext { let fetchManager: FetchManager public var callManager: PresentationCallManager? - public let mediaManager = MediaManager() + public let mediaManager: MediaManager public let contactsManager = DeviceContactsManager() @@ -49,6 +51,7 @@ public final class TelegramApplicationContext { public var hasOngoingCall: Signal? public init(applicationBindings: TelegramApplicationBindings, accountManager: AccountManager, currentPresentationData: PresentationData, presentationData: Signal, currentMediaDownloadSettings: AutomaticMediaDownloadSettings, automaticMediaDownloadSettings: Signal, postbox: Postbox) { + self.mediaManager = MediaManager(postbox: postbox, inForeground: applicationBindings.applicationInForeground) self.applicationBindings = applicationBindings self.accountManager = accountManager self.fetchManager = FetchManager(postbox: postbox) diff --git a/TelegramUI/TelegramController.swift b/TelegramUI/TelegramController.swift index 16fe9bfba1..4d55e5082a 100644 --- a/TelegramUI/TelegramController.swift +++ b/TelegramUI/TelegramController.swift @@ -2,14 +2,19 @@ import Foundation import Display import TelegramCore import SwiftSignalKit +import Postbox public class TelegramController: ViewController { private let account: Account + let enableMediaAccessoryPanel: Bool + private var mediaStatusDisposable: Disposable? - private var playlistStateAndStatus: AudioPlaylistStateAndStatus? - private var mediaAccessoryPanel: MediaNavigationAccessoryPanel? + private(set) var playlistStateAndType: (SharedMediaPlaylistItem, MusicPlaybackSettingsOrder, MediaManagerPlayerType)? + private var mediaAccessoryPanel: (MediaNavigationAccessoryPanel, MediaManagerPlayerType)? + + private var dismissingPanel: ASDisplayNode? override public var navigationHeight: CGFloat { var height = super.navigationHeight @@ -19,17 +24,23 @@ public class TelegramController: ViewController { return height } - init(account: Account) { + init(account: Account, navigationBarTheme: NavigationBarTheme?, enableMediaAccessoryPanel: Bool) { self.account = account + self.enableMediaAccessoryPanel = enableMediaAccessoryPanel - super.init(navigationBarTheme: NavigationBarTheme(rootControllerTheme: (account.telegramApplicationContext.currentPresentationData.with { $0 }).theme)) + super.init(navigationBarTheme: navigationBarTheme) if let applicationContext = account.applicationContext as? TelegramApplicationContext { - self.mediaStatusDisposable = (applicationContext.mediaManager.playlistPlayerStateAndStatus - |> deliverOnMainQueue).start(next: { [weak self] playlistStateAndStatus in - if let strongSelf = self { - if strongSelf.playlistStateAndStatus != playlistStateAndStatus { - strongSelf.playlistStateAndStatus = playlistStateAndStatus + self.mediaStatusDisposable = (applicationContext.mediaManager.globalMediaPlayerState + |> deliverOnMainQueue).start(next: { [weak self] playlistStateAndType in + if let strongSelf = self, strongSelf.enableMediaAccessoryPanel { + if !arePlaylistItemsEqual(strongSelf.playlistStateAndType?.0, playlistStateAndType?.0.item) || + strongSelf.playlistStateAndType?.1 != playlistStateAndType?.0.order || strongSelf.playlistStateAndType?.2 != playlistStateAndType?.1 { + if let playlistStateAndType = playlistStateAndType { + strongSelf.playlistStateAndType = (playlistStateAndType.0.item, playlistStateAndType.0.order, playlistStateAndType.1) + } else { + strongSelf.playlistStateAndType = nil + } strongSelf.requestLayout(transition: .animated(duration: 0.4, curve: .spring)) } } @@ -48,66 +59,78 @@ public class TelegramController: ViewController { public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - if let playlistStateAndStatus = self.playlistStateAndStatus { - let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: super.navigationHeight), size: CGSize(width: layout.size.width, height: max(0.0, layout.size.height - super.navigationHeight - layout.insets(options: [.input]).bottom))) - if let mediaAccessoryPanel = self.mediaAccessoryPanel { - transition.updateFrame(node: mediaAccessoryPanel, frame: panelFrame) + if let (item, _, type) = self.playlistStateAndType { + let navigationHeight = super.navigationHeight + let panelHeight = MediaNavigationAccessoryHeaderNode.minimizedHeight + let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: navigationHeight.isZero ? -panelHeight : (navigationHeight + UIScreenPixel)), size: CGSize(width: layout.size.width, height: panelHeight)) + if let (mediaAccessoryPanel, mediaType) = self.mediaAccessoryPanel, mediaType == type { + transition.updateFrame(layer: mediaAccessoryPanel.layer, frame: panelFrame) mediaAccessoryPanel.updateLayout(size: panelFrame.size, transition: transition) - mediaAccessoryPanel.containerNode.headerNode.stateAndStatus = playlistStateAndStatus - mediaAccessoryPanel.containerNode.itemListNode.stateAndStatus = playlistStateAndStatus + mediaAccessoryPanel.containerNode.headerNode.playbackItem = item + mediaAccessoryPanel.containerNode.headerNode.playbackStatus = self.account.telegramApplicationContext.mediaManager.globalMediaPlayerState |> map { state in + return state?.0.status ?? MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, timestamp: 0.0, seekId: 0, status: .paused) + } } else { - let mediaAccessoryPanel = MediaNavigationAccessoryPanel(account: self.account) - mediaAccessoryPanel.close = { [weak self] in - if let strongSelf = self { - if let applicationContext = strongSelf.account.applicationContext as? TelegramApplicationContext { - applicationContext.mediaManager.setPlaylistPlayer(nil) + if let (mediaAccessoryPanel, _) = self.mediaAccessoryPanel { + self.mediaAccessoryPanel = nil + self.dismissingPanel = mediaAccessoryPanel + mediaAccessoryPanel.animateOut(transition: transition, completion: { [weak self, weak mediaAccessoryPanel] in + mediaAccessoryPanel?.removeFromSupernode() + if let strongSelf = self, strongSelf.dismissingPanel === mediaAccessoryPanel { + strongSelf.dismissingPanel = nil } + }) + } + + let mediaAccessoryPanel = MediaNavigationAccessoryPanel(account: self.account) + mediaAccessoryPanel.containerNode.headerNode.displayScrubber = type != .voice + mediaAccessoryPanel.close = { [weak self] in + if let strongSelf = self, let (_, _, type) = strongSelf.playlistStateAndType { + strongSelf.account.telegramApplicationContext.mediaManager.setPlaylist(nil, type: type) } } mediaAccessoryPanel.togglePlayPause = { [weak self] in - if let strongSelf = self { - if let applicationContext = strongSelf.account.applicationContext as? TelegramApplicationContext { - applicationContext.mediaManager.playlistPlayerControl(.playback(.togglePlayPause)) - } + if let strongSelf = self, let (_, _, type) = strongSelf.playlistStateAndType { + strongSelf.account.telegramApplicationContext.mediaManager.playlistControl(.playback(.togglePlayPause), type: type) } } - mediaAccessoryPanel.previous = { [weak self] in - if let strongSelf = self { - if let applicationContext = strongSelf.account.applicationContext as? TelegramApplicationContext { - applicationContext.mediaManager.playlistPlayerControl(.navigation(.previous)) - } - } - } - mediaAccessoryPanel.next = { [weak self] in - if let strongSelf = self { - if let applicationContext = strongSelf.account.applicationContext as? TelegramApplicationContext { - applicationContext.mediaManager.playlistPlayerControl(.navigation(.next)) - } - } - } - mediaAccessoryPanel.seek = { [weak self] timestamp in - if let strongSelf = self { - if let applicationContext = strongSelf.account.applicationContext as? TelegramApplicationContext { - applicationContext.mediaManager.playlistPlayerControl(.playback(.seek(timestamp))) + mediaAccessoryPanel.tapAction = { [weak self] in + if let strongSelf = self, let navigationController = strongSelf.navigationController as? NavigationController, let (state, order, type) = strongSelf.playlistStateAndType { + if let id = state.id as? PeerMessagesMediaPlaylistItemId { + if type == .music { + let controller = OverlayPlayerController(account: strongSelf.account, peerId: id.messageId.peerId, type: type, initialMessageId: id.messageId, initialOrder: order, parentNavigationController: strongSelf.navigationController as? NavigationController) + strongSelf.displayNode.view.window?.endEditing(true) + strongSelf.present(controller, in: .window(.root)) + } else { + navigateToChatController(navigationController: navigationController, account: strongSelf.account, chatLocation: .peer(id.messageId.peerId), messageId: id.messageId) + } } } } mediaAccessoryPanel.frame = panelFrame - if let navigationBar = self.navigationBar { - self.displayNode.insertSubnode(mediaAccessoryPanel, belowSubnode: navigationBar) + if let dismissingPanel = self.dismissingPanel { + self.displayNode.insertSubnode(mediaAccessoryPanel, aboveSubnode: dismissingPanel) + } else if let navigationBar = self.navigationBar { + self.displayNode.insertSubnode(mediaAccessoryPanel, aboveSubnode: navigationBar) } else { self.displayNode.addSubnode(mediaAccessoryPanel) } - self.mediaAccessoryPanel = mediaAccessoryPanel + self.mediaAccessoryPanel = (mediaAccessoryPanel, type) mediaAccessoryPanel.updateLayout(size: panelFrame.size, transition: .immediate) - mediaAccessoryPanel.containerNode.headerNode.stateAndStatus = playlistStateAndStatus - mediaAccessoryPanel.containerNode.itemListNode.stateAndStatus = playlistStateAndStatus + mediaAccessoryPanel.containerNode.headerNode.playbackItem = item + mediaAccessoryPanel.containerNode.headerNode.playbackStatus = self.account.telegramApplicationContext.mediaManager.globalMediaPlayerState |> map { state in + return state?.0.status ?? MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, timestamp: 0.0, seekId: 0, status: .paused) + } mediaAccessoryPanel.animateIn(transition: transition) } - } else if let mediaAccessoryPanel = self.mediaAccessoryPanel { + } else if let (mediaAccessoryPanel, _) = self.mediaAccessoryPanel { self.mediaAccessoryPanel = nil - mediaAccessoryPanel.animateOut(transition: transition, completion: { [weak mediaAccessoryPanel] in + self.dismissingPanel = mediaAccessoryPanel + mediaAccessoryPanel.animateOut(transition: transition, completion: { [weak self, weak mediaAccessoryPanel] in mediaAccessoryPanel?.removeFromSupernode() + if let strongSelf = self, strongSelf.dismissingPanel === mediaAccessoryPanel { + strongSelf.dismissingPanel = nil + } }) } } diff --git a/TelegramUI/TelegramInitializeLegacyComponents.swift b/TelegramUI/TelegramInitializeLegacyComponents.swift index a3d4f9938f..0e217a4b25 100644 --- a/TelegramUI/TelegramInitializeLegacyComponents.swift +++ b/TelegramUI/TelegramInitializeLegacyComponents.swift @@ -1,11 +1,12 @@ import Foundation -import LegacyComponents import UIKit import TelegramCore import SwiftSignalKit import MtProtoKitDynamic import Display +import LegacyComponents + var legacyComponentsApplication: UIApplication! private var legacyComponentsAccount: Account? @@ -13,6 +14,10 @@ private var legacyLocalization = TGLocalization(version: 0, code: "en", dict: [: func updateLegacyLocalization(strings: PresentationStrings) { legacyLocalization = TGLocalization(version: 0, code: strings.languageCode, dict: strings.dict, isActive: true) + + let languages: [String] = [strings.languageCode] + UserDefaults.standard.set(languages, forKey: "AppleLanguages") + UserDefaults.standard.synchronize() } public func updateLegacyComponentsAccount(_ account: Account?) { @@ -186,6 +191,24 @@ private final class LegacyComponentsGlobalsProviderImpl: NSObject, LegacyCompone } public func request(_ type: TGAudioSessionType, interrupted: (() -> Void)!) -> SDisposable! { + if let legacyAccount = legacyAccount { + let convertedType: ManagedAudioSessionType + switch type { + case TGAudioSessionTypePlayAndRecord, TGAudioSessionTypePlayAndRecordHeadphones: + convertedType = .playAndRecord + default: + convertedType = .play + } + let disposable = legacyAccount.telegramApplicationContext.mediaManager.audioSession.push(audioSessionType: convertedType, once: true, activate: { _ in + }, deactivate: { + interrupted?() + return .complete() + }) + + return SBlockDisposable(block: { + disposable.dispose() + }) + } return nil } @@ -217,7 +240,11 @@ private final class LegacyComponentsGlobalsProviderImpl: NSObject, LegacyCompone return nil case let .color(color): return generateImage(CGSize(width: 1.0, height: 1.0), rotatedContext: { size, context in - context.setFillColor(UIColor(rgb: UInt32(bitPattern: color)).cgColor) + if color == 0 { + context.setFillColor(UIColor(rgb: 0x222222).cgColor) + } else { + context.setFillColor(UIColor(rgb: UInt32(bitPattern: color)).cgColor) + } context.fill(CGRect(origin: CGPoint(), size: size)) }) case let .image(representations): @@ -283,7 +310,7 @@ private final class LegacyComponentsGlobalsProviderImpl: NSObject, LegacyCompone } public func makeHTTPRequestOperation(with request: URLRequest!) -> (Operation & LegacyHTTPRequestOperation)! { - return nil + return LegacyHTTPOperationImpl(request: request) } public func pausePictureInPicturePlayback() { @@ -297,6 +324,47 @@ private final class LegacyComponentsGlobalsProviderImpl: NSObject, LegacyCompone public func maybeReleaseVolumeOverlay() { } + + func navigationBarPallete() -> TGNavigationBarPallete! { + if let account = legacyComponentsAccount { + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + let theme = presentationData.theme.rootController.navigationBar + return TGNavigationBarPallete(backgroundColor: theme.backgroundColor, separatorColor: theme.separatorColor, titleColor: theme.primaryTextColor, tintColor: theme.accentTextColor) + } else { + return nil + } + } + + func menuSheetPallete() -> TGMenuSheetPallete! { + if let account = legacyComponentsAccount { + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + let theme = presentationData.theme.actionSheet + return TGMenuSheetPallete(dark: presentationData.theme.overallDarkAppearance, backgroundColor: theme.opaqueItemBackgroundColor, selectionColor: theme.opaqueItemHighlightedBackgroundColor, separatorColor: theme.opaqueItemSeparatorColor, accentColor: theme.controlAccentColor, destructiveColor: theme.destructiveActionTextColor, textColor: theme.primaryTextColor, secondaryTextColor: theme.secondaryTextColor, spinnerColor: theme.secondaryTextColor, badgeTextColor: theme.controlAccentColor, badgeImage: nil, cornersImage: generateStretchableFilledCircleImage(diameter: 11.0, color: nil, strokeColor: nil, strokeWidth: nil, backgroundColor: theme.opaqueItemBackgroundColor)) + } else { + return nil + } + } + + func mediaAssetsPallete() -> TGMediaAssetsPallete! { + if let account = legacyComponentsAccount { + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + let theme = presentationData.theme.list + let navigationBar = presentationData.theme.rootController.navigationBar + return TGMediaAssetsPallete(dark: presentationData.theme.overallDarkAppearance, backgroundColor: theme.plainBackgroundColor, selectionColor: theme.itemHighlightedBackgroundColor, separatorColor: theme.itemPlainSeparatorColor, textColor: theme.itemPrimaryTextColor, secondaryTextColor: theme.controlSecondaryColor, accentColor: theme.itemAccentColor, barBackgroundColor: navigationBar.backgroundColor, barSeparatorColor: navigationBar.separatorColor, navigationTitleColor: navigationBar.primaryTextColor, badge: generateStretchableFilledCircleImage(diameter: 22.0, color: navigationBar.accentTextColor), badgeTextColor: navigationBar.backgroundColor, sendIconImage: PresentationResourcesChat.chatInputPanelSendButtonImage(presentationData.theme), maybeAccentColor: navigationBar.accentTextColor) + } else { + return nil + } + } + + func checkButtonPallete() -> TGCheckButtonPallete! { + if let account = legacyComponentsAccount { + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + let theme = presentationData.theme + return TGCheckButtonPallete(defaultBackgroundColor: theme.chat.bubble.selectionControlFillColor, accentBackgroundColor: theme.chat.bubble.selectionControlFillColor, defaultBorderColor: theme.chat.bubble.selectionControlBorderColor, mediaBorderColor: theme.chat.bubble.selectionControlBorderColor, chatBorderColor: theme.chat.bubble.selectionControlBorderColor, check: theme.chat.bubble.selectionControlForegroundColor, blueColor: theme.chat.bubble.selectionControlFillColor, barBackgroundColor: theme.chat.bubble.selectionControlFillColor) + } else { + return nil + } + } } public func setupLegacyComponents(account: Account) { diff --git a/TelegramUI/TelegramRootController.swift b/TelegramUI/TelegramRootController.swift index 6eeee03606..eb431a86cb 100644 --- a/TelegramUI/TelegramRootController.swift +++ b/TelegramUI/TelegramRootController.swift @@ -8,7 +8,11 @@ public final class TelegramRootController: NavigationController { private let account: Account public var rootTabController: TabBarController? + + public var contactsController: ContactsController? + public var callListController: CallListController? public var chatListController: ChatListController? + public var accountSettingsController: ViewController? private var presentationDataDisposable: Disposable? private var presentationData: PresentationData @@ -41,13 +45,46 @@ public final class TelegramRootController: NavigationController { self.presentationDataDisposable?.dispose() } - public func addRootControllers() { + public func addRootControllers(showCallsTab: Bool) { let tabBarController = TabBarController(navigationBarTheme: NavigationBarTheme(rootControllerTheme: self.presentationData.theme), theme: TabBarControllerTheme(rootControllerTheme: self.presentationData.theme)) - let chatListController = ChatListController(account: self.account) - let callListController = CallListController(account: self.account) - tabBarController.controllers = [ContactsController(account: self.account), callListController, chatListController, settingsController(account: self.account, accountManager: self.account.telegramApplicationContext.accountManager)] + let chatListController = ChatListController(account: self.account, groupId: nil, controlsHistoryPreload: true) + let callListController = CallListController(account: self.account, mode: .tab) + + var controllers: [ViewController] = [] + + let contactsController = ContactsController(account: self.account) + controllers.append(contactsController) + + if showCallsTab { + controllers.append(callListController) + } + controllers.append(chatListController) + + let accountSettingsController = settingsController(account: self.account, accountManager: self.account.telegramApplicationContext.accountManager) + controllers.append(accountSettingsController) + + tabBarController.setControllers(controllers, selectedIndex: controllers.count - 2) + + self.contactsController = contactsController + self.callListController = callListController self.chatListController = chatListController + self.accountSettingsController = accountSettingsController self.rootTabController = tabBarController self.pushViewController(tabBarController, animated: false) } + + public func updateRootControllers(showCallsTab: Bool) { + guard let rootTabController = self.rootTabController else { + return + } + var controllers: [ViewController] = [] + controllers.append(self.contactsController!) + if showCallsTab { + controllers.append(self.callListController!) + } + controllers.append(self.chatListController!) + controllers.append(self.accountSettingsController!) + + rootTabController.setControllers(controllers, selectedIndex: nil) + } } diff --git a/TelegramUI/TelegramUIPrivate/module.modulemap b/TelegramUI/TelegramUIPrivate/module.modulemap index 36fedb6b67..beaa71cfda 100644 --- a/TelegramUI/TelegramUIPrivate/module.modulemap +++ b/TelegramUI/TelegramUIPrivate/module.modulemap @@ -22,4 +22,6 @@ module TelegramUIPrivateModule { header "../OngoingCallThreadLocalContext.h" header "../SecretChatKeyVisualization.h" header "../NumberPluralizationForm.h" + header "../DeviceProximityManager.h" + header "../RaiseToListenActivator.h" } diff --git a/TelegramUI/TelegramVideoNode.swift b/TelegramUI/TelegramVideoNode.swift index 21b9d7f6b9..c968ed240b 100644 --- a/TelegramUI/TelegramVideoNode.swift +++ b/TelegramUI/TelegramVideoNode.swift @@ -63,7 +63,7 @@ private final class SharedTelegramVideoContext: SharedVideoContext { func setSoundEnabled(_ value: Bool) { assert(Queue.mainQueue().isCurrent()) if value { - self.player.playOnceWithSound() + self.player.playOnceWithSound(playAndRecord: false) } else { self.player.continuePlayingWithoutSound() } @@ -238,7 +238,7 @@ final class TelegramVideoNode: OverlayMediaItemNode { self.addSubnode(controlsNode) } - self.imageNode.setSignal(account: account, signal: chatMessageVideo(account: account, video: source.file)) + self.imageNode.setSignal(chatMessageVideo(postbox: account.postbox, video: source.file)) } deinit { diff --git a/TelegramUI/TextNode.swift b/TelegramUI/TextNode.swift index fbdafcb822..5dedfc6297 100644 --- a/TelegramUI/TextNode.swift +++ b/TelegramUI/TextNode.swift @@ -25,10 +25,34 @@ enum TextNodeCutoutPosition { struct TextNodeCutout: Equatable { let position: TextNodeCutoutPosition let size: CGSize + + static func ==(lhs: TextNodeCutout, rhs: TextNodeCutout) -> Bool { + return lhs.position == rhs.position && lhs.size == rhs.size + } } -func ==(lhs: TextNodeCutout, rhs: TextNodeCutout) -> Bool { - return lhs.position == rhs.position && lhs.size == rhs.size +final class TextNodeLayoutArguments { + let attributedString: NSAttributedString? + let backgroundColor: UIColor? + let maximumNumberOfLines: Int + let truncationType: CTLineTruncationType + let constrainedSize: CGSize + let alignment: NSTextAlignment + let lineSpacing: CGFloat + let cutout: TextNodeCutout? + let insets: UIEdgeInsets + + init(attributedString: NSAttributedString?, backgroundColor: UIColor? = nil, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, constrainedSize: CGSize, alignment: NSTextAlignment = .natural, lineSpacing: CGFloat = 0.12, cutout: TextNodeCutout? = nil, insets: UIEdgeInsets = UIEdgeInsets()) { + self.attributedString = attributedString + self.backgroundColor = backgroundColor + self.maximumNumberOfLines = maximumNumberOfLines + self.truncationType = truncationType + self.constrainedSize = constrainedSize + self.alignment = alignment + self.lineSpacing = lineSpacing + self.cutout = cutout + self.insets = insets + } } final class TextNodeLayout: NSObject { @@ -38,18 +62,20 @@ final class TextNodeLayout: NSObject { fileprivate let backgroundColor: UIColor? fileprivate let constrainedSize: CGSize fileprivate let alignment: NSTextAlignment + fileprivate let lineSpacing: CGFloat fileprivate let cutout: TextNodeCutout? fileprivate let insets: UIEdgeInsets let size: CGSize fileprivate let firstLineOffset: CGFloat fileprivate let lines: [TextNodeLine] - fileprivate init(attributedString: NSAttributedString?, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, constrainedSize: CGSize, alignment: NSTextAlignment, cutout: TextNodeCutout?, insets: UIEdgeInsets, size: CGSize, firstLineOffset: CGFloat, lines: [TextNodeLine], backgroundColor: UIColor?) { + fileprivate init(attributedString: NSAttributedString?, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, constrainedSize: CGSize, alignment: NSTextAlignment, lineSpacing: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets, size: CGSize, firstLineOffset: CGFloat, lines: [TextNodeLine], backgroundColor: UIColor?) { self.attributedString = attributedString self.maximumNumberOfLines = maximumNumberOfLines self.truncationType = truncationType self.constrainedSize = constrainedSize self.alignment = alignment + self.lineSpacing = lineSpacing self.cutout = cutout self.insets = insets self.size = size @@ -214,7 +240,7 @@ final class TextNode: ASDisplayNode { } } - private class func calculateLayout(attributedString: NSAttributedString?, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, backgroundColor: UIColor?, constrainedSize: CGSize, alignment: NSTextAlignment, cutout: TextNodeCutout?, insets: UIEdgeInsets) -> TextNodeLayout { + private class func calculateLayout(attributedString: NSAttributedString?, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, backgroundColor: UIColor?, constrainedSize: CGSize, alignment: NSTextAlignment, lineSpacingFactor: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets) -> TextNodeLayout { if let attributedString = attributedString { let stringLength = attributedString.length @@ -232,14 +258,14 @@ final class TextNode: ASDisplayNode { let fontAscent = CTFontGetAscent(font) let fontDescent = CTFontGetDescent(font) let fontLineHeight = floor(fontAscent + fontDescent) - let fontLineSpacing = floor(fontLineHeight * 0.12) + let fontLineSpacing = floor(fontLineHeight * lineSpacingFactor) var lines: [TextNodeLine] = [] var maybeTypesetter: CTTypesetter? maybeTypesetter = CTTypesetterCreateWithAttributedString(attributedString as CFAttributedString) if maybeTypesetter == nil { - return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, alignment: alignment, cutout: cutout, insets: insets, size: CGSize(), firstLineOffset: 0.0, lines: [], backgroundColor: backgroundColor) + return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, alignment: alignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(), firstLineOffset: 0.0, lines: [], backgroundColor: backgroundColor) } let typesetter = maybeTypesetter! @@ -267,7 +293,8 @@ final class TextNode: ASDisplayNode { var first = true while true { var lineConstrainedWidth = constrainedSize.width - var lineOriginY = floorToScreenPixels(layoutSize.height + fontLineHeight - fontLineSpacing * 2.0) + //var lineOriginY = floorToScreenPixels(layoutSize.height + fontLineHeight - fontLineSpacing * 2.0) + var lineOriginY = floorToScreenPixels(layoutSize.height + fontAscent) if !first { lineOriginY += fontLineSpacing } @@ -321,7 +348,7 @@ final class TextNode: ASDisplayNode { coreTextLine = CTLineCreateTruncatedLine(originalLine, Double(constrainedSize.width), truncationType, truncationToken) ?? truncationToken } - let lineWidth = ceil(CGFloat(CTLineGetTypographicBounds(coreTextLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(coreTextLine))) + let lineWidth = min(constrainedSize.width, ceil(CGFloat(CTLineGetTypographicBounds(coreTextLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(coreTextLine)))) let lineFrame = CGRect(x: lineCutoutOffset, y: lineOriginY, width: lineWidth, height: fontLineHeight) layoutSize.height += fontLineHeight + fontLineSpacing layoutSize.width = max(layoutSize.width, lineWidth + lineAdditionalWidth) @@ -356,9 +383,9 @@ final class TextNode: ASDisplayNode { } } - return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, alignment: alignment, cutout: cutout, insets: insets, size: CGSize(width: ceil(layoutSize.width) + insets.left + insets.right, height: ceil(layoutSize.height) + insets.top + insets.bottom), firstLineOffset: firstLineOffset, lines: lines, backgroundColor: backgroundColor) + return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, alignment: alignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(width: ceil(layoutSize.width) + insets.left + insets.right, height: ceil(layoutSize.height) + insets.top + insets.bottom), firstLineOffset: firstLineOffset, lines: lines, backgroundColor: backgroundColor) } else { - return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, alignment: alignment, cutout: cutout, insets: insets, size: CGSize(), firstLineOffset: 0.0, lines: [], backgroundColor: backgroundColor) + return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, alignment: alignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(), firstLineOffset: 0.0, lines: [], backgroundColor: backgroundColor) } } @@ -418,30 +445,30 @@ final class TextNode: ASDisplayNode { context.setBlendMode(.normal) } - class func asyncLayout(_ maybeNode: TextNode?) -> (_ attributedString: NSAttributedString?, _ backgroundColor: UIColor?, _ maximumNumberOfLines: Int, _ truncationType: CTLineTruncationType, _ constrainedSize: CGSize, _ alignment: NSTextAlignment, _ cutout: TextNodeCutout?, _ insets: UIEdgeInsets) -> (TextNodeLayout, () -> TextNode) { + class func asyncLayout(_ maybeNode: TextNode?) -> (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode) { let existingLayout: TextNodeLayout? = maybeNode?.cachedLayout - return { attributedString, backgroundColor, maximumNumberOfLines, truncationType, constrainedSize, alignment, cutout, insets in + return { arguments in let layout: TextNodeLayout var updated = false - if let existingLayout = existingLayout, existingLayout.constrainedSize == constrainedSize && existingLayout.maximumNumberOfLines == maximumNumberOfLines && existingLayout.truncationType == truncationType && existingLayout.cutout == cutout && existingLayout.alignment == alignment { + if let existingLayout = existingLayout, existingLayout.constrainedSize == arguments.constrainedSize && existingLayout.maximumNumberOfLines == arguments.maximumNumberOfLines && existingLayout.truncationType == arguments.truncationType && existingLayout.cutout == arguments.cutout && existingLayout.alignment == arguments.alignment && existingLayout.lineSpacing.isEqual(to: arguments.lineSpacing) { let stringMatch: Bool var colorMatch: Bool = true - if let backgroundColor = backgroundColor, let previousBackgroundColor = existingLayout.backgroundColor { + if let backgroundColor = arguments.backgroundColor, let previousBackgroundColor = existingLayout.backgroundColor { if !backgroundColor.isEqual(previousBackgroundColor) { colorMatch = false } - } else if (backgroundColor != nil) != (existingLayout.backgroundColor != nil) { + } else if (arguments.backgroundColor != nil) != (existingLayout.backgroundColor != nil) { colorMatch = false } if !colorMatch { stringMatch = false - } else if let existingString = existingLayout.attributedString, let string = attributedString { + } else if let existingString = existingLayout.attributedString, let string = arguments.attributedString { stringMatch = existingString.isEqual(to: string) - } else if existingLayout.attributedString == nil && attributedString == nil { + } else if existingLayout.attributedString == nil && arguments.attributedString == nil { stringMatch = true } else { stringMatch = false @@ -450,11 +477,11 @@ final class TextNode: ASDisplayNode { if stringMatch { layout = existingLayout } else { - layout = TextNode.calculateLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, backgroundColor: backgroundColor, constrainedSize: constrainedSize, alignment: alignment, cutout: cutout, insets: insets) + layout = TextNode.calculateLayout(attributedString: arguments.attributedString, maximumNumberOfLines: arguments.maximumNumberOfLines, truncationType: arguments.truncationType, backgroundColor: arguments.backgroundColor, constrainedSize: arguments.constrainedSize, alignment: arguments.alignment, lineSpacingFactor: arguments.lineSpacing, cutout: arguments.cutout, insets: arguments.insets) updated = true } } else { - layout = TextNode.calculateLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, backgroundColor: backgroundColor, constrainedSize: constrainedSize, alignment: alignment, cutout: cutout, insets: insets) + layout = TextNode.calculateLayout(attributedString: arguments.attributedString, maximumNumberOfLines: arguments.maximumNumberOfLines, truncationType: arguments.truncationType, backgroundColor: arguments.backgroundColor, constrainedSize: arguments.constrainedSize, alignment: arguments.alignment, lineSpacingFactor: arguments.lineSpacing, cutout: arguments.cutout, insets: arguments.insets) updated = true } diff --git a/TelegramUI/ThemeGalleryController.swift b/TelegramUI/ThemeGalleryController.swift index 2dd0589001..7a7ad58cf6 100644 --- a/TelegramUI/ThemeGalleryController.swift +++ b/TelegramUI/ThemeGalleryController.swift @@ -249,10 +249,10 @@ class ThemeGalleryController: ViewController { } let _ = (updatePresentationThemeSettingsInteractively(postbox: strongSelf.account.postbox, { current in if case .color(0x000000) = wallpaper { - return PresentationThemeSettings(chatWallpaper: wallpaper, theme: .builtin(.dark)) + return PresentationThemeSettings(chatWallpaper: wallpaper, theme: current.theme, fontSize: .regular) } - return PresentationThemeSettings(chatWallpaper: wallpaper, theme: .builtin(.light)) + return PresentationThemeSettings(chatWallpaper: wallpaper, theme: current.theme, fontSize: current.fontSize) }) |> deliverOnMainQueue).start(completed: { self?.dismiss(forceAway: true) }) diff --git a/TelegramUI/ThemeGalleryItem.swift b/TelegramUI/ThemeGalleryItem.swift index 2d747e95c8..f792ec0367 100644 --- a/TelegramUI/ThemeGalleryItem.swift +++ b/TelegramUI/ThemeGalleryItem.swift @@ -80,9 +80,8 @@ final class ThemeGalleryItemNode: ZoomableContentGalleryItemNode { switch wallpaper { case .builtin: let displaySize = CGSize(width: 640.0, height: 1136.0) - self.imageNode.alphaTransitionOnFirstUpdate = false self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets()))() - self.imageNode.setSignal(account: account, signal: settingsBuiltinWallpaperImage(account: self.account), dispatchOnDisplayLink: false) + self.imageNode.setSignal(settingsBuiltinWallpaperImage(account: self.account), dispatchOnDisplayLink: false) self.zoomableContent = (displaySize, self.imageNode) case let .color(color): self.imageNode.isHidden = true @@ -90,9 +89,8 @@ final class ThemeGalleryItemNode: ZoomableContentGalleryItemNode { case let .image(representations): if let largestSize = largestImageRepresentation(representations) { let displaySize = largestSize.dimensions.fitted(CGSize(width: 1280.0, height: 1280.0)).dividedByScreenScale().integralFloor - self.imageNode.alphaTransitionOnFirstUpdate = false self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets()))() - self.imageNode.setSignal(account: account, signal: chatAvatarGalleryPhoto(account: account, representations: representations), dispatchOnDisplayLink: false) + self.imageNode.setSignal(chatAvatarGalleryPhoto(account: account, representations: representations), dispatchOnDisplayLink: false) self.zoomableContent = (largestSize.dimensions, self.imageNode) self.fetchDisposable.set(account.postbox.mediaBox.fetchedResource(largestSize.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .generic)).start()) } else { diff --git a/TelegramUI/ThemeGridController.swift b/TelegramUI/ThemeGridController.swift index 6bc692bbc4..d888dca690 100644 --- a/TelegramUI/ThemeGridController.swift +++ b/TelegramUI/ThemeGridController.swift @@ -5,7 +5,7 @@ import Postbox import TelegramCore import SwiftSignalKit -final class ThemeGridController: TelegramController { +final class ThemeGridController: ViewController { private var controllerNode: ThemeGridControllerNode { return self.displayNode as! ThemeGridControllerNode } @@ -20,11 +20,11 @@ final class ThemeGridController: TelegramController { private var presentationData: PresentationData private var presentationDataDisposable: Disposable? - override init(account: Account) { + init(account: Account) { self.account = account self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } - super.init(account: account) + super.init(navigationBarTheme: NavigationBarTheme(rootControllerTheme: self.presentationData.theme)) self.title = self.presentationData.strings.Wallpaper_Title self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style diff --git a/TelegramUI/ThemeGridControllerNode.swift b/TelegramUI/ThemeGridControllerNode.swift index 4235fc8871..b830414463 100644 --- a/TelegramUI/ThemeGridControllerNode.swift +++ b/TelegramUI/ThemeGridControllerNode.swift @@ -84,7 +84,7 @@ final class ThemeGridControllerNode: ASDisplayNode { return UITracingLayerView() }) - self.backgroundColor = presentationData.theme.list.itemBackgroundColor + self.backgroundColor = presentationData.theme.list.itemBlocksBackgroundColor self.addSubnode(self.gridNode) @@ -128,7 +128,7 @@ final class ThemeGridControllerNode: ASDisplayNode { func updatePresentationData(_ presentationData: PresentationData) { self.presentationData = presentationData - self.backgroundColor = presentationData.theme.list.itemBackgroundColor + self.backgroundColor = presentationData.theme.list.itemBlocksBackgroundColor } private func enqueueTransition(_ transition: ThemeGridEntryTransition) { diff --git a/TelegramUI/ThemeSettingsChatPreviewItem.swift b/TelegramUI/ThemeSettingsChatPreviewItem.swift new file mode 100644 index 0000000000..739afac651 --- /dev/null +++ b/TelegramUI/ThemeSettingsChatPreviewItem.swift @@ -0,0 +1,269 @@ +import Foundation +import Display +import AsyncDisplayKit +import SwiftSignalKit +import TelegramCore +import Postbox + +class ThemeSettingsChatPreviewItem: ListViewItem, ItemListItem { + let account: Account + let theme: PresentationTheme + let strings: PresentationStrings + let sectionId: ItemListSectionId + let fontSize: PresentationFontSize + let wallpaper: TelegramWallpaper + let timeFormat: PresentationTimeFormat + + init(account: Account, theme: PresentationTheme, strings: PresentationStrings, sectionId: ItemListSectionId, fontSize: PresentationFontSize, wallpaper: TelegramWallpaper, timeFormat: PresentationTimeFormat) { + self.account = account + self.theme = theme + self.strings = strings + self.sectionId = sectionId + self.fontSize = fontSize + self.wallpaper = wallpaper + self.timeFormat = timeFormat + } + + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + async { + let node = ThemeSettingsChatPreviewItemNode() + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + completion(node, { + return (nil, { apply() }) + }) + } + } + + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + if let node = node as? ThemeSettingsChatPreviewItemNode { + Queue.mainQueue().async { + let makeLayout = node.asyncLayout() + + async { + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + Queue.mainQueue().async { + completion(layout, { + apply() + }) + } + } + } + } + } +} + +class ThemeSettingsChatPreviewItemNode: ListViewItemNode { + private let backgroundNode: ASImageNode + private let topStripeNode: ASDisplayNode + private let bottomStripeNode: ASDisplayNode + + private let containerNode: ASDisplayNode + + private var messageNode1: ListViewItemNode? + private var messageNode2: ListViewItemNode? + + private var item: ThemeSettingsChatPreviewItem? + + private let controllerInteraction: ChatControllerInteraction + + init() { + self.backgroundNode = ASImageNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.displaysAsynchronously = false + self.backgroundNode.displayWithoutProcessing = true + self.backgroundNode.contentMode = .scaleAspectFill + + self.topStripeNode = ASDisplayNode() + self.topStripeNode.isLayerBacked = true + + self.bottomStripeNode = ASDisplayNode() + self.bottomStripeNode.isLayerBacked = true + + self.containerNode = ASDisplayNode() + self.containerNode.subnodeTransform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) + + self.controllerInteraction = ChatControllerInteraction(openMessage: { _ in + return false }, openSecretMessagePreview: { _ in }, closeSecretMessagePreview: { }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _ in }, navigateToMessage: { _, _ in }, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendMessage: { _ in }, sendSticker: { _ in }, sendGif: { _ in }, requestMessageActionCallback: { _, _, _ in }, openUrl: { _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, openMessageShareMenu: { _ in + }, presentController: { _, _ in }, callPeer: { _ in }, longTap: { _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in + }, canSetupReply: { + return false + }, automaticMediaDownloadSettings: .none) + + super.init(layerBacked: false, dynamicBounce: false) + + self.addSubnode(self.containerNode) + } + + func asyncLayout() -> (_ item: ThemeSettingsChatPreviewItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + let currentItem = self.item + + let controllerInteraction = self.controllerInteraction + let currentNode1 = self.messageNode1 + let currentNode2 = self.messageNode2 + + return { item, params, neighbors in + var updatedBackgroundImage: UIImage? + if currentItem?.wallpaper != item.wallpaper { + updatedBackgroundImage = UIImage() + switch item.wallpaper { + case .builtin: + if let filePath = frameworkBundle.path(forResource: "ChatWallpaperBuiltin0", ofType: "jpg") { + updatedBackgroundImage = UIImage(contentsOfFile: filePath)?.precomposed() + } + case let .color(color): + updatedBackgroundImage = generateImage(CGSize(width: 1.0, height: 1.0), rotatedContext: { size, context in + context.setFillColor(UIColor(rgb: UInt32(bitPattern: color)).cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + }) + case let .image(representations): + if let largest = largestImageRepresentation(representations) { + if let path = item.account.postbox.mediaBox.completedResourcePath(largest.resource) { + updatedBackgroundImage = UIImage(contentsOfFile: path)?.precomposed() + } + } + } + } + + let insets: UIEdgeInsets + let separatorHeight = UIScreenPixel + + let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: 1) + + var peers = SimpleDictionary() + var messages = SimpleDictionary() + + peers[peerId] = TelegramUser(id: peerId, accessHash: nil, firstName: "Lucio", lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, flags: []) + let replyMessageId = MessageId(peerId: peerId, namespace: 0, id: 3) + messages[replyMessageId] = Message(stableId: 3, stableVersion: 0, id: replyMessageId, globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], forwardInfo: nil, author: peers[peerId], text: "Reinhart, we need to find you some...", attributes: [], media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) + + let chatPresentationData = ChatPresentationData(theme: item.theme, fontSize: item.fontSize, strings: item.strings, wallpaper: item.wallpaper, timeFormat: item.timeFormat) + + let item2: ChatMessageItem = ChatMessageItem(presentationData: chatPresentationData, account: item.account, chatLocation: .peer(peerId), controllerInteraction: controllerInteraction, content: .message(message: Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], forwardInfo: nil, author: nil, text: "Ahh you kids today with techno music! Enjoy the classics, like Hasselhoff!", attributes: [ReplyMessageAttribute(messageId: replyMessageId)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []), read: true, selection: .none), disableDate: true) + let item1: ChatMessageItem = ChatMessageItem(presentationData: chatPresentationData, account: item.account, chatLocation: .peer(peerId), controllerInteraction: controllerInteraction, content: .message(message: Message(stableId: 2, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 2), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: 66001, flags: [], tags: [], globalTags: [], forwardInfo: nil, author: TelegramUser(id: item.account.peerId, accessHash: nil, firstName: "", lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, flags: []), text: "I can't take you seriously right now. Sorry..", attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []), read: true, selection: .none), disableDate: true) + + var node1: ListViewItemNode? + if let current = currentNode1 { + node1 = current + item1.updateNode(async: { $0() }, node: current, params: params, previousItem: nil, nextItem: nil, animation: .None, completion: { (layout, apply) in + let nodeFrame = CGRect(origin: current.frame.origin, size: CGSize(width: layout.size.width, height: layout.size.height)) + + current.contentSize = layout.contentSize + current.insets = layout.insets + current.frame = nodeFrame + + apply() + }) + } else { + item1.nodeConfiguredForParams(async: { $0() }, params: params, previousItem: nil, nextItem: nil, completion: { node, apply in + node1 = node + apply().1() + }) + } + + var node2: ListViewItemNode? + if let current = currentNode2 { + node2 = current + item2.updateNode(async: { $0() }, node: current, params: params, previousItem: nil, nextItem: nil, animation: .None, completion: { (layout, apply) in + let nodeFrame = CGRect(origin: current.frame.origin, size: CGSize(width: layout.size.width, height: layout.size.height)) + + current.contentSize = layout.contentSize + current.insets = layout.insets + current.frame = nodeFrame + + apply() + }) + } else { + item2.nodeConfiguredForParams(async: { $0() }, params: params, previousItem: nil, nextItem: nil, completion: { node, apply in + node2 = node + apply().1() + }) + } + + var contentSize = CGSize(width: params.width, height: 4.0 + 4.0) + if let node1 = node1 { + contentSize.height += node1.frame.size.height + } + if let node2 = node2 { + contentSize.height += node2.frame.size.height + } + insets = itemListNeighborsGroupedInsets(neighbors) + + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + let layoutSize = layout.size + + return (layout, { [weak self] in + if let strongSelf = self { + strongSelf.item = item + + strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: contentSize) + + var topOffset: CGFloat = 4.0 + if let node1 = node1 { + strongSelf.messageNode1 = node1 + if node1.supernode == nil { + strongSelf.containerNode.addSubnode(node1) + } + node1.frame = CGRect(origin: CGPoint(x: 0.0, y: topOffset), size: node1.frame.size) + topOffset += node1.frame.size.height + } + + if let node2 = node2 { + strongSelf.messageNode2 = node2 + if node2.supernode == nil { + strongSelf.containerNode.addSubnode(node2) + } + node2.frame = CGRect(origin: CGPoint(x: 0.0, y: topOffset), size: node2.frame.size) + topOffset += node2.frame.size.height + } + + if let updatedBackgroundImage = updatedBackgroundImage { + strongSelf.backgroundNode.image = updatedBackgroundImage + } + strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + + if strongSelf.backgroundNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) + } + if strongSelf.topStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) + } + switch neighbors.top { + case .sameSection(false): + strongSelf.topStripeNode.isHidden = true + default: + strongSelf.topStripeNode.isHidden = false + } + let bottomStripeInset: CGFloat + let bottomStripeOffset: CGFloat + switch neighbors.bottom { + case .sameSection(false): + bottomStripeInset = 0.0 + bottomStripeOffset = -separatorHeight + default: + bottomStripeInset = 0.0 + bottomStripeOffset = 0.0 + } + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) + } + }) + } + } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) + } +} diff --git a/TelegramUI/ThemeSettingsController.swift b/TelegramUI/ThemeSettingsController.swift new file mode 100644 index 0000000000..9b57c58117 --- /dev/null +++ b/TelegramUI/ThemeSettingsController.swift @@ -0,0 +1,283 @@ +import Foundation +import Display +import SwiftSignalKit +import Postbox +import TelegramCore + +private final class ThemeSettingsControllerArguments { + let account: Account + let selectTheme: (Int32) -> Void + let selectFontSize: (PresentationFontSize) -> Void + let openWallpaperSettings: () -> Void + + init(account: Account, selectTheme: @escaping (Int32) -> Void, selectFontSize: @escaping (PresentationFontSize) -> Void, openWallpaperSettings: @escaping () -> Void) { + self.account = account + self.selectTheme = selectTheme + self.selectFontSize = selectFontSize + self.openWallpaperSettings = openWallpaperSettings + } +} + +private enum ThemeSettingsControllerSection: Int32 { + case chatPreview + case themeList + case fontSize +} + +private enum ThemeSettingsControllerEntry: ItemListNodeEntry { + case fontSizeHeader(PresentationTheme, String) + case fontSize(PresentationTheme, PresentationFontSize) + case chatPreviewHeader(PresentationTheme, String) + case chatPreview(PresentationTheme, TelegramWallpaper, PresentationFontSize, PresentationStrings, PresentationTimeFormat) + case wallpaper(PresentationTheme, String) + case themeListHeader(PresentationTheme, String) + case themeItem(PresentationTheme, String, Bool, Int32) + + var section: ItemListSectionId { + switch self { + case .chatPreviewHeader, .chatPreview, .wallpaper: + return ThemeSettingsControllerSection.chatPreview.rawValue + case .themeListHeader, .themeItem: + return ThemeSettingsControllerSection.themeList.rawValue + case .fontSizeHeader, .fontSize: + return ThemeSettingsControllerSection.fontSize.rawValue + } + } + + var stableId: Int32 { + switch self { + case .fontSizeHeader: + return 0 + case .fontSize: + return 1 + case .chatPreviewHeader: + return 2 + case .chatPreview: + return 3 + case .wallpaper: + return 4 + case .themeListHeader: + return 5 + case let .themeItem(_, _, _, index): + return 6 + index + } + } + + static func ==(lhs: ThemeSettingsControllerEntry, rhs: ThemeSettingsControllerEntry) -> Bool { + switch lhs { + case let .chatPreviewHeader(lhsTheme, lhsText): + if case let .chatPreviewHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .chatPreview(lhsTheme, lhsWallpaper, lhsFontSize, lhsStrings, lhsTimeFormat): + if case let .chatPreview(rhsTheme, rhsWallpaper, rhsFontSize, rhsStrings, rhsTimeFormat) = rhs, lhsTheme === rhsTheme, lhsWallpaper == rhsWallpaper, lhsFontSize == rhsFontSize, lhsStrings === rhsStrings, lhsTimeFormat == rhsTimeFormat { + return true + } else { + return false + } + case let .wallpaper(lhsTheme, lhsText): + if case let .wallpaper(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .themeListHeader(lhsTheme, lhsText): + if case let .themeListHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .themeItem(lhsTheme, lhsText, lhsValue, lhsIndex): + if case let .themeItem(rhsTheme, rhsText, rhsValue, rhsIndex) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue, lhsIndex == rhsIndex { + return true + } else { + return false + } + case let .fontSizeHeader(lhsTheme, lhsText): + if case let .fontSizeHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .fontSize(lhsTheme, lhsFontSize): + if case let .fontSize(rhsTheme, rhsFontSize) = rhs, lhsTheme === rhsTheme, lhsFontSize == rhsFontSize { + return true + } else { + return false + } + } + } + + static func <(lhs: ThemeSettingsControllerEntry, rhs: ThemeSettingsControllerEntry) -> Bool { + return lhs.stableId < rhs.stableId + } + + func item(_ arguments: ThemeSettingsControllerArguments) -> ListViewItem { + switch self { + case let .fontSizeHeader(theme, text): + return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + case let .fontSize(theme, fontSize): + return ThemeSettingsFontSizeItem(theme: theme, fontSize: fontSize, sectionId: self.section, updated: { value in + arguments.selectFontSize(value) + }) + case let .chatPreviewHeader(theme, text): + return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + case let .chatPreview(theme, wallpaper, fontSize, strings, timeFormat): + return ThemeSettingsChatPreviewItem(account: arguments.account, theme: theme, strings: strings, sectionId: self.section, fontSize: fontSize, wallpaper: wallpaper, timeFormat: timeFormat) + case let .wallpaper(theme, text): + return ItemListDisclosureItem(theme: theme, title: text, label: "", sectionId: self.section, style: .blocks, action: { + arguments.openWallpaperSettings() + }) + case let .themeListHeader(theme, text): + return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + case let .themeItem(theme, title, value, index): + return ItemListCheckboxItem(theme: theme, title: title, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { + arguments.selectTheme(index) + }) + } + } +} + +private func themeSettingsControllerEntries(theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, timeFormat: PresentationTimeFormat) -> [ThemeSettingsControllerEntry] { + var entries: [ThemeSettingsControllerEntry] = [] + + entries.append(.fontSizeHeader(theme, "TEXT SIZE")) + entries.append(.fontSize(theme, fontSize)) + entries.append(.chatPreviewHeader(theme, "CHAT PREVIEW")) + entries.append(.chatPreview(theme, wallpaper, fontSize, strings, timeFormat)) + entries.append(.wallpaper(theme, "Chat Background")) + entries.append(.themeListHeader(theme, "COLOR THEME")) + entries.append(.themeItem(theme, "Day Classic", theme.name == .builtin(.dayClassic), 0)) + entries.append(.themeItem(theme, "Day", theme.name == .builtin(.day), 1)) + entries.append(.themeItem(theme, "Night", theme.name == .builtin(.nightGrayscale), 2)) + entries.append(.themeItem(theme, "Night Blue", theme.name == .builtin(.nightAccent), 3)) + + return entries +} + +public func themeSettingsController(account: Account) -> ViewController { + var pushControllerImpl: ((ViewController) -> Void)? + var presentControllerImpl: ((ViewController) -> Void)? + + let arguments = ThemeSettingsControllerArguments(account: account, selectTheme: { index in + let _ = updatePresentationThemeSettingsInteractively(postbox: account.postbox, { current in + let wallpaper: TelegramWallpaper + let theme: PresentationThemeReference + if index == 0 { + wallpaper = .builtin + theme = .builtin(.dayClassic) + } else if index == 1 { + wallpaper = .color(0xffffff) + theme = .builtin(.day) + } else if index == 2 { + wallpaper = .color(0x000000) + theme = .builtin(.nightGrayscale) + } else { + wallpaper = .color(0x18222D) + theme = .builtin(.nightAccent) + } + return PresentationThemeSettings(chatWallpaper: wallpaper, theme: theme, fontSize: current.fontSize) + }).start() + }, selectFontSize: { size in + let _ = updatePresentationThemeSettingsInteractively(postbox: account.postbox, { current in + return PresentationThemeSettings(chatWallpaper: current.chatWallpaper, theme: current.theme, fontSize: size) + }).start() + }, openWallpaperSettings: { + pushControllerImpl?(ThemeGridController(account: account)) + }) + + let themeSettingsKey = ApplicationSpecificPreferencesKeys.presentationThemeSettings + let localizationSettingsKey = PreferencesKeys.localizationSettings + let preferences = account.postbox.preferencesView(keys: [themeSettingsKey, localizationSettingsKey]) + + let previousTheme = Atomic(value: nil) + + let signal = preferences + |> deliverOnMainQueue + |> map { preferences -> (ItemListControllerState, (ItemListNodeState, ThemeSettingsControllerEntry.ItemGenerationArguments)) in + let theme: PresentationTheme + let fontSize: PresentationFontSize + let wallpaper: TelegramWallpaper + let strings: PresentationStrings + let timeFormat: PresentationTimeFormat + + let settings = (preferences.values[themeSettingsKey] as? PresentationThemeSettings) ?? PresentationThemeSettings.defaultSettings + switch settings.theme { + case let .builtin(reference): + switch reference { + case .dayClassic: + theme = defaultPresentationTheme + case .nightGrayscale: + theme = defaultDarkPresentationTheme + case .nightAccent: + theme = defaultDarkAccentPresentationTheme + case .day: + theme = defaultDayPresentationTheme + } + } + wallpaper = settings.chatWallpaper + fontSize = settings.fontSize + + if let entry = preferences.values[localizationSettingsKey] as? LocalizationSettings { + strings = PresentationStrings(languageCode: entry.languageCode, dict: dictFromLocalization(entry.localization)) + } else { + strings = defaultPresentationStrings + } + + timeFormat = .regular + + let controllerState = ItemListControllerState(theme: theme, title: .text("Appearance"), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: strings.Common_Back)) + let listState = ItemListNodeState(entries: themeSettingsControllerEntries(theme: theme, strings: strings, wallpaper: wallpaper, fontSize: fontSize, timeFormat: timeFormat), style: .blocks, animateChanges: false) + + if previousTheme.swap(theme) !== theme { + presentControllerImpl?(ThemeSettingsCrossfadeController()) + } + + return (controllerState, (listState, arguments)) + } + + let controller = ItemListController(account: account, state: signal) + pushControllerImpl = { [weak controller] c in + (controller?.navigationController as? NavigationController)?.pushViewController(c) + } + presentControllerImpl = { [weak controller] c in + controller?.present(c, in: .window(.root)) + } + return controller +} + +private final class ThemeSettingsCrossfadeController: ViewController { + private let snapshotView: UIView? + + init() { + self.snapshotView = UIScreen.main.snapshotView(afterScreenUpdates: false) + + super.init(navigationBarTheme: nil) + + self.statusBar.statusBarStyle = .Hide + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadDisplayNode() { + self.displayNode = ViewControllerTracingNode() + + self.displayNode.backgroundColor = nil + self.displayNode.isOpaque = false + if let snapshotView = self.snapshotView { + self.displayNode.view.addSubview(snapshotView) + } + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + self.displayNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak self] _ in + self?.presentingViewController?.dismiss(animated: false, completion: nil) + }) + } +} diff --git a/TelegramUI/ThemeSettingsFontSizeItem.swift b/TelegramUI/ThemeSettingsFontSizeItem.swift new file mode 100644 index 0000000000..d31ed30e6a --- /dev/null +++ b/TelegramUI/ThemeSettingsFontSizeItem.swift @@ -0,0 +1,260 @@ +import Foundation +import Display +import AsyncDisplayKit +import SwiftSignalKit +import TelegramCore + +import LegacyComponents + +class ThemeSettingsFontSizeItem: ListViewItem, ItemListItem { + let theme: PresentationTheme + let fontSize: PresentationFontSize + let sectionId: ItemListSectionId + let updated: (PresentationFontSize) -> Void + + init(theme: PresentationTheme, fontSize: PresentationFontSize, sectionId: ItemListSectionId, updated: @escaping (PresentationFontSize) -> Void) { + self.theme = theme + self.fontSize = fontSize + self.sectionId = sectionId + self.updated = updated + } + + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + async { + let node = ThemeSettingsFontSizeItemNode() + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + completion(node, { + return (nil, { apply() }) + }) + } + } + + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + if let node = node as? ThemeSettingsFontSizeItemNode { + Queue.mainQueue().async { + let makeLayout = node.asyncLayout() + + async { + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + Queue.mainQueue().async { + completion(layout, { + apply() + }) + } + } + } + } + } +} + +private func generateKnobImage() -> UIImage? { + return generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setShadow(offset: CGSize(width: 0.0, height: -1.0), blur: 3.5, color: UIColor(white: 0.0, alpha: 0.25).cgColor) + context.setFillColor(UIColor.white.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 6.0, y: 6.0), size: CGSize(width: 28.0, height: 28.0))) + }) +} + +class ThemeSettingsFontSizeItemNode: ListViewItemNode { + private let backgroundNode: ASDisplayNode + private let topStripeNode: ASDisplayNode + private let bottomStripeNode: ASDisplayNode + + private var sliderView: TGPhotoEditorSliderView? + private let leftIconNode: ASImageNode + private let rightIconNode: ASImageNode + + private var item: ThemeSettingsFontSizeItem? + private var layoutParams: ListViewItemLayoutParams? + + init() { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + + self.topStripeNode = ASDisplayNode() + self.topStripeNode.isLayerBacked = true + + self.bottomStripeNode = ASDisplayNode() + self.bottomStripeNode.isLayerBacked = true + + self.leftIconNode = ASImageNode() + self.leftIconNode.displaysAsynchronously = false + self.leftIconNode.displayWithoutProcessing = true + + self.rightIconNode = ASImageNode() + self.rightIconNode.displaysAsynchronously = false + self.rightIconNode.displayWithoutProcessing = true + + super.init(layerBacked: false, dynamicBounce: false) + + self.addSubnode(self.leftIconNode) + self.addSubnode(self.rightIconNode) + } + + override func didLoad() { + super.didLoad() + + let sliderView = TGPhotoEditorSliderView() + sliderView.enablePanHandling = true + sliderView.trackCornerRadius = 1.0 + sliderView.lineSize = 2.0 + sliderView.dotSize = 5.0 + sliderView.minimumValue = 0.0 + sliderView.maximumValue = 4.0 + sliderView.startValue = 0.0 + sliderView.positionsCount = 5 + sliderView.disablesInteractiveTransitionGestureRecognizer = true + if let item = self.item, let params = self.layoutParams { + let value: CGFloat + switch item.fontSize { + case .extraSmall: + value = 0.0 + case .small: + value = 1.0 + case .regular: + value = 2.0 + case .large: + value = 3.0 + case .extraLarge: + value = 4.0 + } + sliderView.value = value + sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor + sliderView.backColor = item.theme.list.itemSecondaryTextColor + sliderView.trackColor = item.theme.list.itemAccentColor + sliderView.knobImage = generateKnobImage() + + sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + 38.0, y: 8.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 38.0 * 2.0, height: 44.0)) + } + self.view.addSubview(sliderView) + sliderView.addTarget(self, action: #selector(self.sliderValueChanged), for: .valueChanged) + self.sliderView = sliderView + } + + func asyncLayout() -> (_ item: ThemeSettingsFontSizeItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + let currentItem = self.item + + return { item, params, neighbors in + var updatedLeftIcon: UIImage? + var updatedRightIcon: UIImage? + + var themeUpdated = false + if currentItem?.theme !== item.theme { + themeUpdated = true + + updatedLeftIcon = generateTintedImage(image: UIImage(bundleImageName: "Instant View/SettingsFontMinIcon"), color: item.theme.list.itemPrimaryTextColor) + updatedRightIcon = generateTintedImage(image: UIImage(bundleImageName: "Instant View/SettingsFontMaxIcon"), color: item.theme.list.itemPrimaryTextColor) + } + + let contentSize: CGSize + let insets: UIEdgeInsets + let separatorHeight = UIScreenPixel + + contentSize = CGSize(width: params.width, height: 60.0) + insets = itemListNeighborsGroupedInsets(neighbors) + + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + let layoutSize = layout.size + + return (layout, { [weak self] in + if let strongSelf = self { + strongSelf.item = item + strongSelf.layoutParams = params + + strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor + strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + + if strongSelf.backgroundNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) + } + if strongSelf.topStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) + } + switch neighbors.top { + case .sameSection(false): + strongSelf.topStripeNode.isHidden = true + default: + strongSelf.topStripeNode.isHidden = false + } + let bottomStripeInset: CGFloat + let bottomStripeOffset: CGFloat + switch neighbors.bottom { + case .sameSection(false): + bottomStripeInset = params.leftInset + 16.0 + bottomStripeOffset = -separatorHeight + default: + bottomStripeInset = 0.0 + bottomStripeOffset = 0.0 + } + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) + + if let updatedLeftIcon = updatedLeftIcon { + strongSelf.leftIconNode.image = updatedLeftIcon + } + if let image = strongSelf.leftIconNode.image { + strongSelf.leftIconNode.frame = CGRect(origin: CGPoint(x: params.leftInset + 18.0, y: 25.0), size: CGSize(width: image.size.width, height: image.size.height)) + } + if let updatedRightIcon = updatedRightIcon { + strongSelf.rightIconNode.image = updatedRightIcon + } + if let image = strongSelf.rightIconNode.image { + strongSelf.rightIconNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 14.0 - image.size.width, y: 21.0), size: CGSize(width: image.size.width, height: image.size.height)) + } + + if let sliderView = strongSelf.sliderView { + if themeUpdated { + sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor + sliderView.backColor = item.theme.list.itemSecondaryTextColor + sliderView.trackColor = item.theme.list.itemAccentColor + sliderView.knobImage = generateKnobImage() + } + + sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + 38.0, y: 8.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 38.0 * 2.0, height: 44.0)) + } + } + }) + } + } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) + } + + @objc func sliderValueChanged() { + guard let sliderView = self.sliderView else { + return + } + let fontSize: PresentationFontSize + switch Int(sliderView.value) { + case 0: + fontSize = .extraSmall + case 1: + fontSize = .small + case 2: + fontSize = .regular + case 3: + fontSize = .large + case 4: + fontSize = .extraLarge + default: + fontSize = .regular + } + self.item?.updated(fontSize) + } +} + diff --git a/TelegramUI/TimestampStrings.swift b/TelegramUI/TimestampStrings.swift new file mode 100644 index 0000000000..9a30829567 --- /dev/null +++ b/TelegramUI/TimestampStrings.swift @@ -0,0 +1,3 @@ +import Foundation + + diff --git a/TelegramUI/TransformImageArguments.swift b/TelegramUI/TransformImageArguments.swift index 4d5aad7a90..c95c1278e9 100644 --- a/TelegramUI/TransformImageArguments.swift +++ b/TelegramUI/TransformImageArguments.swift @@ -1,12 +1,26 @@ import Foundation import UIKit +public enum TransformImageResizeMode { + case fill(UIColor) + case blurBackground +} + public struct TransformImageArguments: Equatable { public let corners: ImageCorners public let imageSize: CGSize public let boundingSize: CGSize public let intrinsicInsets: UIEdgeInsets + public let resizeMode: TransformImageResizeMode + + public init(corners: ImageCorners, imageSize: CGSize, boundingSize: CGSize, intrinsicInsets: UIEdgeInsets, resizeMode: TransformImageResizeMode = .fill(.black)) { + self.corners = corners + self.imageSize = imageSize + self.boundingSize = boundingSize + self.intrinsicInsets = intrinsicInsets + self.resizeMode = resizeMode + } public var drawingSize: CGSize { let cornersExtendedEdges = self.corners.extendedEdges diff --git a/TelegramUI/TransformImageNode.swift b/TelegramUI/TransformImageNode.swift index 81bdd699c1..e63f00ea74 100644 --- a/TelegramUI/TransformImageNode.swift +++ b/TelegramUI/TransformImageNode.swift @@ -4,9 +4,20 @@ import SwiftSignalKit import Display import TelegramCore +public struct TransformImageNodeContentAnimations: OptionSet { + public var rawValue: Int32 + + public init(rawValue: Int32) { + self.rawValue = rawValue + } + + public static let firstUpdate = TransformImageNodeContentAnimations(rawValue: 1 << 0) + public static let subsequentUpdates = TransformImageNodeContentAnimations(rawValue: 1 << 1) +} + public class TransformImageNode: ASDisplayNode { public var imageUpdated: (() -> Void)? - public var alphaTransitionOnFirstUpdate = false + public var contentAnimations: TransformImageNodeContentAnimations = [] private var disposable = MetaDisposable() private var argumentsPromise = ValuePromise(ignoreRepeated: true) @@ -26,10 +37,10 @@ public class TransformImageNode: ASDisplayNode { } } - func setSignal(account: Account, signal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>, dispatchOnDisplayLink: Bool = true) { + func setSignal(_ signal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>, dispatchOnDisplayLink: Bool = true) { let argumentsPromise = self.argumentsPromise - let result = combineLatest(signal, argumentsPromise.get()) |> deliverOn(Queue.concurrentDefaultQueue() /*account.graphicsThreadPool*/) |> mapToThrottled { transform, arguments -> Signal in + let result = combineLatest(signal, argumentsPromise.get()) |> deliverOn(Queue.concurrentDefaultQueue()) |> mapToThrottled { transform, arguments -> Signal in return deferred { if let context = transform(arguments) { return Signal.single(context.generateImage()) @@ -40,28 +51,24 @@ public class TransformImageNode: ASDisplayNode { } self.disposable.set((result |> deliverOnMainQueue).start(next: { [weak self] next in - if dispatchOnDisplayLink { - displayLinkDispatcher.dispatch { - if let strongSelf = self { - if strongSelf.alphaTransitionOnFirstUpdate && strongSelf.contents == nil { + let apply: () -> Void = { + if let strongSelf = self { + if strongSelf.contents == nil { + if strongSelf.contentAnimations.contains(.firstUpdate) { strongSelf.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) } - strongSelf.contents = next?.cgImage - if let overlayColor = strongSelf.overlayColor { - strongSelf.applyOverlayColor(animated: false) - } - if let imageUpdated = strongSelf.imageUpdated { - imageUpdated() - } - } - } - } else { - if let strongSelf = self { - if strongSelf.alphaTransitionOnFirstUpdate && strongSelf.contents == nil { - strongSelf.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + } else if strongSelf.contentAnimations.contains(.subsequentUpdates) { + let tempLayer = CALayer() + tempLayer.frame = strongSelf.bounds + tempLayer.contents = strongSelf.contents + strongSelf.layer.addSublayer(tempLayer) + tempLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak tempLayer] _ in + tempLayer?.removeFromSuperlayer() + }) } + strongSelf.contents = next?.cgImage - if let overlayColor = strongSelf.overlayColor { + if let _ = strongSelf.overlayColor { strongSelf.applyOverlayColor(animated: false) } if let imageUpdated = strongSelf.imageUpdated { @@ -69,6 +76,13 @@ public class TransformImageNode: ASDisplayNode { } } } + if dispatchOnDisplayLink { + displayLinkDispatcher.dispatch { + apply() + } + } else { + apply() + } })) } diff --git a/TelegramUI/TwoStepVerificationUnlockController.swift b/TelegramUI/TwoStepVerificationUnlockController.swift index 517558f002..2e8baa401d 100644 --- a/TelegramUI/TwoStepVerificationUnlockController.swift +++ b/TelegramUI/TwoStepVerificationUnlockController.swift @@ -507,7 +507,7 @@ func twoStepVerificationUnlockSettingsController(account: Account, mode: TwoStep } } } else { - emptyStateItem = ItemListLoadingIndicatorEmptyStateItem() + emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme) } case .manage: title = presentationData.strings.PrivacySettings_TwoStepAuth diff --git a/TelegramUI/UniversalVideoCalleryItem.swift b/TelegramUI/UniversalVideoCalleryItem.swift index c0aa16301d..f728d6ce95 100644 --- a/TelegramUI/UniversalVideoCalleryItem.swift +++ b/TelegramUI/UniversalVideoCalleryItem.swift @@ -4,6 +4,11 @@ import AsyncDisplayKit import SwiftSignalKit import TelegramCore import Display +import Postbox + +enum UniversalVideoGalleryItemContentInfo { + case message(Message) +} class UniversalVideoGalleryItem: GalleryItem { let account: Account @@ -12,15 +17,17 @@ class UniversalVideoGalleryItem: GalleryItem { let content: UniversalVideoContent let originData: GalleryItemOriginData? let indexData: GalleryItemIndexData? + let contentInfo: UniversalVideoGalleryItemContentInfo? let caption: String - init(account: Account, theme: PresentationTheme, strings: PresentationStrings, content: UniversalVideoContent, originData: GalleryItemOriginData?, indexData: GalleryItemIndexData?, caption: String) { + init(account: Account, theme: PresentationTheme, strings: PresentationStrings, content: UniversalVideoContent, originData: GalleryItemOriginData?, indexData: GalleryItemIndexData?, contentInfo: UniversalVideoGalleryItemContentInfo?, caption: String) { self.account = account self.theme = theme self.strings = strings self.content = content self.originData = originData self.indexData = indexData + self.contentInfo = contentInfo self.caption = caption } @@ -28,33 +35,12 @@ class UniversalVideoGalleryItem: GalleryItem { let node = UniversalVideoGalleryItemNode(account: self.account, theme: self.theme, strings: self.strings) node.setupItem(self) - /*for media in self.message.media { - if let file = media as? TelegramMediaFile, (file.isVideo || file.mimeType.hasPrefix("video/")) { - node.setFile(account: account, stableId: self.message.stableId, file: file, loopVideo: file.isAnimated || self.message.containsSecretMedia) - break - } else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { - if let file = content.file, (file.isVideo || file.mimeType.hasPrefix("video/")) { - node.setFile(account: account, stableId: self.message.stableId, file: file, loopVideo: file.isAnimated || self.message.containsSecretMedia) - break - } - } - }*/ - - if let indexData = self.indexData { - node._title.set(.single("\(indexData.position + 1) of \(indexData.totalCount)")) - } - //node.setMessage(self.message) - return node } func updateNode(node: GalleryItemNode) { if let node = node as? UniversalVideoGalleryItemNode { - if let indexData = self.indexData { - node._title.set(.single("\(indexData.position + 1) of \(indexData.totalCount)")) - } node.setupItem(self) - //node.setMessage(self.message) } } } @@ -115,6 +101,8 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { private var isCentral = false private var validLayout: (ContainerViewLayout, CGFloat)? + private var didPause = false + private var isPaused = true private var item: UniversalVideoGalleryItem? @@ -130,9 +118,11 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { self.statusButtonNode = HighlightableButtonNode() self.statusNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.5)) + self._title.set(.single("")) + self._titleView.set(.single(nil)) + super.init() - self._titleView.set(.single(self.scrubberView)) self.scrubberView.seek = { [weak self] timestamp in self?.videoNode?.seek(timestamp) } @@ -141,10 +131,12 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { self.statusButtonNode.addTarget(self, action: #selector(statusButtonPressed), forControlEvents: .touchUpInside) self.addSubnode(self.statusButtonNode) - self.statusNode.transitionToState(.play(.white), completion: {}) self.footerContentNode.playbackControl = { [weak self] in if let strongSelf = self { + if !strongSelf.isPaused { + strongSelf.didPause = true + } strongSelf.videoNode?.togglePlayPause() } } @@ -177,23 +169,6 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } } - /*fileprivate func setMessage(_ message: Message) { - self.footerContentNode.setMessage(message) - - self.message = message - - var rightBarButtonItem: UIBarButtonItem? - for media in message.media { - if let file = media as? TelegramMediaFile { - if file.isVideo { - rightBarButtonItem = UIBarButtonItem(image: pictureInPictureButtonImage, style: .plain, target: self, action: #selector(self.pictureInPictureButtonPressed)) - break - } - } - } - self._rightBarButtonItem.set(.single(rightBarButtonItem)) - }*/ - func setupItem(_ item: UniversalVideoGalleryItem) { if self.item?.content.id != item.content.id { if let videoNode = self.videoNode { @@ -201,7 +176,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { videoNode.removeFromSupernode() } - let videoNode = UniversalVideoNode(account: item.account, manager: item.account.telegramApplicationContext.mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: item.content, priority: .gallery) + let videoNode = UniversalVideoNode(postbox: item.account.postbox, audioSession: item.account.telegramApplicationContext.mediaManager.audioSession, manager: item.account.telegramApplicationContext.mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: item.content, priority: .gallery) let videoSize = CGSize(width: item.content.dimensions.width * 2.0, height: item.content.dimensions.height * 2.0) videoNode.updateLayout(size: videoSize, transition: .immediate) videoNode.ownsContentNodeUpdated = { [weak self] value in @@ -219,38 +194,89 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { if let value = value, !value.duration.isZero { return value } else { - return MediaPlayerStatus(generationTimestamp: 0.0, duration: max(Double(item.content.duration), 0.01), timestamp: 0.0, status: .paused) + return MediaPlayerStatus(generationTimestamp: 0.0, duration: max(Double(item.content.duration), 0.01), timestamp: 0.0, seekId: 0, status: .paused) } }) self.statusDisposable.set((videoNode.status |> deliverOnMainQueue).start(next: { [weak self] value in if let strongSelf = self { + var initialBuffering = false var isPaused = true if let value = value { switch value.status { case .playing: isPaused = false - case let .buffering(whilePlaying): + case let .buffering(_, whilePlaying): + initialBuffering = true isPaused = !whilePlaying + if let content = item.content as? NativeVideoContent, !content.streamVideo { + initialBuffering = false + if !content.enableSound { + isPaused = false + } + } default: - break + if let content = item.content as? NativeVideoContent, !content.streamVideo { + if !content.enableSound { + isPaused = false + } + } } } - strongSelf.statusButtonNode.isHidden = !isPaused - strongSelf.footerContentNode.content = isPaused ? .info : .playbackPause + if initialBuffering { + strongSelf.statusNode.transitionToState(.progress(color: .white, value: nil, cancelEnabled: false), animated: false, completion: {}) + } else { + + strongSelf.statusNode.transitionToState(.play(.white), animated: false, completion: {}) + } + + strongSelf.isPaused = isPaused + + strongSelf.statusButtonNode.isHidden = !initialBuffering && (strongSelf.didPause || !isPaused || value == nil) + if isPaused { + if strongSelf.didPause { + strongSelf.footerContentNode.content = .playbackPlay + } else { + strongSelf.footerContentNode.content = .info + } + } else { + strongSelf.footerContentNode.content = .playbackPause + } } })) self.zoomableContent = (videoSize, videoNode) - let rightBarButtonItem = UIBarButtonItem(image: pictureInPictureButtonImage, style: .plain, target: self, action: #selector(self.pictureInPictureButtonPressed)) - self._rightBarButtonItem.set(.single(rightBarButtonItem)) + var isAnimated = false + var isInstagram = false + if let content = item.content as? NativeVideoContent { + isAnimated = content.file.isAnimated + } else if let _ = item.content as? SystemVideoContent { + isInstagram = true + self._title.set(.single(item.strings.Message_Video)) + } - self._ready.set(.single(Void())) + if !isAnimated && !isInstagram { + self._titleView.set(.single(self.scrubberView)) + } + + if !isAnimated { + let rightBarButtonItem = UIBarButtonItem(image: pictureInPictureButtonImage, style: .plain, target: self, action: #selector(self.pictureInPictureButtonPressed)) + self._rightBarButtonItem.set(.single(rightBarButtonItem)) + } + + self._ready.set(videoNode.ready) } + self.item = item + if let contentInfo = item.contentInfo { + switch contentInfo { + case let .message(message): + self.footerContentNode.setMessage(message) + } + } self.footerContentNode.setup(origin: item.originData, caption: item.caption) } @@ -304,7 +330,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { return } - if let node = node as? TelegramVideoNode { + if let node = node as? OverlayMediaItemNode { var transformedFrame = node.view.convert(node.view.bounds, to: videoNode.view) let transformedSuperFrame = node.view.convert(node.view.bounds, to: videoNode.view.superview) @@ -359,7 +385,10 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { surfaceCopyView.layer.animate(from: NSValue(caTransform3D: CATransform3DIdentity), to: NSValue(caTransform3D: CATransform3DMakeScale(scale.width, scale.height, 1.0)), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false) } - videoNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) + videoNode.allowsGroupOpacity = true + videoNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, completion: { [weak videoNode] _ in + videoNode?.allowsGroupOpacity = false + }) videoNode.layer.animatePosition(from: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), to: videoNode.layer.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) transformedFrame.origin = CGPoint() @@ -375,6 +404,10 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { pictureInPictureNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) pictureInPictureNode.layer.animatePosition(from: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), to: pictureInPictureNode.layer.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) } + + self.statusButtonNode.layer.animatePosition(from: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), to: self.statusButtonNode.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) + self.statusButtonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) + self.statusButtonNode.layer.animateScale(from: 0.5, to: 1.0, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) } } @@ -416,8 +449,8 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } } - copyView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.12, removeOnCompletion: false) - surfaceCopyView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, removeOnCompletion: false) + copyView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false) + surfaceCopyView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, removeOnCompletion: false) copyView.layer.animatePosition(from: CGPoint(x: transformedCopyViewInitialFrame.midX, y: transformedCopyViewInitialFrame.midY), to: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) let scale = CGSize(width: transformedCopyViewInitialFrame.size.width / transformedSelfFrame.size.width, height: transformedCopyViewInitialFrame.size.height / transformedSelfFrame.size.height) @@ -437,7 +470,10 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { intermediateCompletion() }) - videoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + videoNode.allowsGroupOpacity = true + videoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak videoNode] _ in + videoNode?.allowsGroupOpacity = false + }) self.statusButtonNode.layer.animatePosition(from: self.statusButtonNode.layer.position, to: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in //positionCompleted = true @@ -541,7 +577,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } override func title() -> Signal { - return .single("") + return self._title.get() } override func titleView() -> Signal { @@ -591,24 +627,37 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { let baseNavigationController = self.baseNavigationController() let mediaManager = self.account.telegramApplicationContext.mediaManager var expandImpl: (() -> Void)? - let overlayNode = OverlayUniversalVideoNode(account: self.account, manager: self.account.telegramApplicationContext.mediaManager.universalVideoManager, content: item.content, expand: { + let overlayNode = OverlayUniversalVideoNode(account: self.account, audioSession: self.account.telegramApplicationContext.mediaManager.audioSession, manager: self.account.telegramApplicationContext.mediaManager.universalVideoManager, content: item.content, expand: { expandImpl?() }, close: { [weak mediaManager] in mediaManager?.setOverlayVideoNode(nil) }) expandImpl = { [weak overlayNode] in - /*let gallery = GalleryController(account: account, messageId: message.id, replaceRootController: { controller, ready in - if let baseNavigationController = baseNavigationController { - baseNavigationController.replaceTopController(controller, animated: false, ready: ready) - } - }, baseNavigationController: baseNavigationController) + guard let contentInfo = item.contentInfo else { + return + } - (baseNavigationController?.topViewController as? ViewController)?.present(gallery, in: .window(.root), with: GalleryControllerPresentationArguments(transitionArguments: { _, _ in - if let overlayNode = overlayNode, let overlaySupernode = overlayNode.supernode { - return GalleryTransitionArguments(transitionNode: overlayNode, transitionContainerNode: overlaySupernode, transitionBackgroundNode: ASDisplayNode()) - } - return nil - }))*/ + switch contentInfo { + case let .message(message): + let gallery = GalleryController(account: account, messageId: message.id, replaceRootController: { controller, ready in + if let baseNavigationController = baseNavigationController { + baseNavigationController.replaceTopController(controller, animated: false, ready: ready) + } + }, baseNavigationController: baseNavigationController) + gallery.temporaryDoNotWaitForReady = true + + baseNavigationController?.view.endEditing(true) + + (baseNavigationController?.topViewController as? ViewController)?.present(gallery, in: .window(.root), with: GalleryControllerPresentationArguments(transitionArguments: { _, _ in + if let overlayNode = overlayNode, let overlaySupernode = overlayNode.supernode { + return GalleryTransitionArguments(transitionNode: overlayNode, addToTransitionSurface: { [weak overlaySupernode, weak overlayNode] view in + overlaySupernode?.view.addSubview(view) + overlayNode?.canAttachContent = false + }) + } + return nil + })) + } } account.telegramApplicationContext.mediaManager.setOverlayVideoNode(overlayNode) if overlayNode.supernode != nil { diff --git a/TelegramUI/UniversalVideoContentManager.swift b/TelegramUI/UniversalVideoContentManager.swift index d24c11fb55..433d26e86a 100644 --- a/TelegramUI/UniversalVideoContentManager.swift +++ b/TelegramUI/UniversalVideoContentManager.swift @@ -5,10 +5,10 @@ import SwiftSignalKit private final class UniversalVideoContentSubscriber { let id: Int32 let priority: UniversalVideoPriority - let update: ((UniversalVideoContentNode & ASDisplayNode)?) -> Void + let update: (((UniversalVideoContentNode & ASDisplayNode), Bool)?) -> Void var active: Bool = false - init(id: Int32, priority: UniversalVideoPriority, update: @escaping ((UniversalVideoContentNode & ASDisplayNode)?) -> Void) { + init(id: Int32, priority: UniversalVideoPriority, update: @escaping (((UniversalVideoContentNode & ASDisplayNode), Bool)?) -> Void) { self.id = id self.priority = priority self.update = update @@ -23,7 +23,9 @@ private final class UniversalVideoContentHolder { var statusDisposable: Disposable? var statusValue: MediaPlayerStatus? - init(content: UniversalVideoContentNode & ASDisplayNode, statusUpdated: @escaping (MediaPlayerStatus?) -> Void) { + var playbackCompletedIndex: Int? + + init(content: UniversalVideoContentNode & ASDisplayNode, statusUpdated: @escaping (MediaPlayerStatus?) -> Void, playbackCompleted: @escaping () -> Void) { self.content = content self.statusDisposable = (content.status |> deliverOn(Queue.mainQueue())).start(next: { [weak self] value in @@ -32,17 +34,24 @@ private final class UniversalVideoContentHolder { statusUpdated(value) } }) + + self.playbackCompletedIndex = content.addPlaybackCompleted { + playbackCompleted() + } } deinit { self.statusDisposable?.dispose() + if let playbackCompletedIndex = self.playbackCompletedIndex { + self.content.removePlaybackCompleted(playbackCompletedIndex) + } } var isEmpty: Bool { return self.subscribers.isEmpty } - func addSubscriber(priority: UniversalVideoPriority, update: @escaping ((UniversalVideoContentNode & ASDisplayNode)?) -> Void) -> Int32 { + func addSubscriber(priority: UniversalVideoPriority, update: @escaping (((UniversalVideoContentNode & ASDisplayNode), Bool)?) -> Void) -> Int32 { let id = self.nextId self.nextId += 1 @@ -63,25 +72,40 @@ private final class UniversalVideoContentHolder { let subscriber = self.subscribers[i] self.subscribers.remove(at: i) if subscriber.active { - subscriber.update(nil) - self.update() + self.update(removeSubscribers: [subscriber]) } break } } } - func update() { + func update(forceUpdateId: Int32? = nil, initiatedCreation: Int32? = nil, removeSubscribers: [UniversalVideoContentSubscriber] = []) { + var removeSubscribers = removeSubscribers for i in (0 ..< self.subscribers.count) { if i == self.subscribers.count - 1 { if !self.subscribers[i].active { self.subscribers[i].active = true - self.subscribers[i].update(self.content) + self.subscribers[i].update((self.content, initiatedCreation: initiatedCreation == self.subscribers[i].id)) } } else { if self.subscribers[i].active { self.subscribers[i].active = false - self.subscribers[i].update(nil) + removeSubscribers.append(self.subscribers[i]) + } + } + } + + for subscriber in removeSubscribers { + subscriber.update(nil) + } + + if let forceUpdateId = forceUpdateId { + for subscriber in self.subscribers { + if subscriber.id == forceUpdateId { + if !subscriber.active { + subscriber.update(nil) + } + break } } } @@ -101,13 +125,16 @@ final class UniversalVideoContentManager { private var holders: [AnyHashable: UniversalVideoContentHolder] = [:] private var holderCallbacks: [AnyHashable: UniversalVideoContentHolderCallbacks] = [:] - func attachUniversalVideoContent(id: AnyHashable, priority: UniversalVideoPriority, create: () -> UniversalVideoContentNode & ASDisplayNode, update: @escaping ((UniversalVideoContentNode & ASDisplayNode)?) -> Void) -> Int32 { + func attachUniversalVideoContent(id: AnyHashable, priority: UniversalVideoPriority, create: () -> UniversalVideoContentNode & ASDisplayNode, update: @escaping (((UniversalVideoContentNode & ASDisplayNode), Bool)?) -> Void) -> Int32 { assert(Queue.mainQueue().isCurrent()) + var initiatedCreation = false + let holder: UniversalVideoContentHolder if let current = self.holders[id] { holder = current } else { + initiatedCreation = true holder = UniversalVideoContentHolder(content: create(), statusUpdated: { [weak self] value in if let strongSelf = self { if let current = strongSelf.holderCallbacks[id] { @@ -116,12 +143,20 @@ final class UniversalVideoContentManager { } } } + }, playbackCompleted: { [weak self] in + if let strongSelf = self { + if let current = strongSelf.holderCallbacks[id] { + for subscriber in current.playbackCompleted.copyItems() { + subscriber() + } + } + } }) self.holders[id] = holder } let id = holder.addSubscriber(priority: priority, update: update) - holder.update() + holder.update(forceUpdateId: id, initiatedCreation: initiatedCreation ? id : nil) return id } diff --git a/TelegramUI/UniversalVideoNode.swift b/TelegramUI/UniversalVideoNode.swift index 7781079a82..af1b377d63 100644 --- a/TelegramUI/UniversalVideoNode.swift +++ b/TelegramUI/UniversalVideoNode.swift @@ -6,6 +6,7 @@ import TelegramCore import Display protocol UniversalVideoContentNode: class { + var ready: Signal { get } var status: Signal { get } func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) @@ -15,15 +16,20 @@ protocol UniversalVideoContentNode: class { func togglePlayPause() func setSoundEnabled(_ value: Bool) func seek(_ timestamp: Double) + func playOnceWithSound(playAndRecord: Bool) + func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) + func continuePlayingWithoutSound() func addPlaybackCompleted(_ f: @escaping () -> Void) -> Int func removePlaybackCompleted(_ index: Int) + func fetchControl(_ control: UniversalVideoNodeFetchControl) } protocol UniversalVideoContent { var id: AnyHashable { get } var dimensions: CGSize { get } var duration: Int32 { get } - func makeContentNode(account: Account) -> UniversalVideoContentNode & ASDisplayNode + + func makeContentNode(postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode } protocol UniversalVideoDecoration: class { @@ -34,14 +40,16 @@ protocol UniversalVideoDecoration: class { func setStatus(_ status: Signal) func updateContentNode(_ contentNode: (UniversalVideoContentNode & ASDisplayNode)?) + func updateContentNodeSnapshot(_ snapshot: UIView?) func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) func tap() } enum UniversalVideoPriority: Int32, Comparable { - case embedded = 0 - case gallery = 1 - case overlay = 2 + case secondaryOverlay = 0 + case embedded = 1 + case gallery = 2 + case overlay = 3 static func <(lhs: UniversalVideoPriority, rhs: UniversalVideoPriority) -> Bool { return lhs.rawValue < rhs.rawValue @@ -52,12 +60,20 @@ enum UniversalVideoPriority: Int32, Comparable { } } +enum UniversalVideoNodeFetchControl { + case fetch + case cancel +} + final class UniversalVideoNode: ASDisplayNode { - private let account: Account + private let postbox: Postbox + private let audioSession: ManagedAudioSession private let manager: UniversalVideoContentManager private let content: UniversalVideoContent private let priority: UniversalVideoPriority private let decoration: UniversalVideoDecoration + private let autoplay: Bool + private let snapshotContentWhenGone: Bool private var contentNode: (UniversalVideoContentNode & ASDisplayNode)? private var contentNodeId: Int32? @@ -75,6 +91,11 @@ final class UniversalVideoNode: ASDisplayNode { return self._status.get() } + private let _ready = Promise() + var ready: Signal { + return self._ready.get() + } + var canAttachContent: Bool = false { didSet { if self.canAttachContent != oldValue { @@ -82,12 +103,13 @@ final class UniversalVideoNode: ASDisplayNode { assert(self.contentRequestIndex == nil) let content = self.content - let account = self.account + let postbox = self.postbox + let audioSession = self.audioSession self.contentRequestIndex = self.manager.attachUniversalVideoContent(id: self.content.id, priority: self.priority, create: { - return content.makeContentNode(account: account) - }, update: { [weak self] contentNode in + return content.makeContentNode(postbox: postbox, audioSession: audioSession) + }, update: { [weak self] contentNodeAndFlags in if let strongSelf = self { - strongSelf.updateContentNode(contentNode) + strongSelf.updateContentNode(contentNodeAndFlags) } }) } else { @@ -101,12 +123,19 @@ final class UniversalVideoNode: ASDisplayNode { } } - init(account: Account, manager: UniversalVideoContentManager, decoration: UniversalVideoDecoration, content: UniversalVideoContent, priority: UniversalVideoPriority) { - self.account = account + var hasAttachedContext: Bool { + return self.contentNode != nil + } + + init(postbox: Postbox, audioSession: ManagedAudioSession, manager: UniversalVideoContentManager, decoration: UniversalVideoDecoration, content: UniversalVideoContent, priority: UniversalVideoPriority, autoplay: Bool = false, snapshotContentWhenGone: Bool = false) { + self.postbox = postbox + self.audioSession = audioSession self.manager = manager self.content = content self.priority = priority self.decoration = decoration + self.autoplay = autoplay + self.snapshotContentWhenGone = snapshotContentWhenGone super.init() @@ -148,39 +177,36 @@ final class UniversalVideoNode: ASDisplayNode { } } - private func updateContentNode(_ contentNode: (UniversalVideoContentNode & ASDisplayNode)?) { + private func updateContentNode(_ contentNode: ((UniversalVideoContentNode & ASDisplayNode), Bool)?) { let previous = self.contentNode - self.contentNode = contentNode - if previous !== contentNode { - if let previous = previous { - /*if let contextPlaybackEndedIndex = self.contextPlaybackEndedIndex { - previous.removePlaybackCompleted(contextPlaybackEndedIndex) + self.contentNode = contentNode?.0 + if previous !== contentNode?.0 { + if let previous = previous, contentNode?.0 == nil && self.snapshotContentWhenGone { + if let snapshotView = previous.view.snapshotView(afterScreenUpdates: false) { + self.decoration.updateContentNodeSnapshot(snapshotView) } - self.contextPlaybackEndedIndex = nil*/ - /*if let snapshotView = previous.playerNode.view.snapshotView(afterScreenUpdates: false) { - self.snapshotView = snapshotView - snapshotView.frame = self.imageNode.frame - self.view.addSubview(snapshotView) - }*/ } - if let contentNode = contentNode { - /*self.contextPlaybackEndedIndex = context.addPlaybackCompleted { [weak self] in - self?.playbackEnded?() - }*/ - + if let (contentNode, initiatedCreation) = contentNode { + self._ready.set(contentNode.ready) + if initiatedCreation && self.autoplay { + self.play() + } } - self.decoration.updateContentNode(contentNode) - /*if self.hasAttachedContext != (context !== nil) { - self.hasAttachedContext = (context !== nil) - self.hasAttachedContextUpdated?(self.hasAttachedContext) - }*/ + if contentNode?.0 != nil && self.snapshotContentWhenGone { + self.decoration.updateContentNodeSnapshot(nil) + } + self.decoration.updateContentNode(contentNode?.0) - let ownsContentNode = contentNode !== nil + let ownsContentNode = contentNode?.0 !== nil if self.ownsContentNode != ownsContentNode { self.ownsContentNode = ownsContentNode self.ownsContentNodeUpdated?(ownsContentNode) } } + + if contentNode == nil { + self._ready.set(.single(Void())) + } } func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { @@ -227,6 +253,38 @@ final class UniversalVideoNode: ASDisplayNode { }) } + func playOnceWithSound(playAndRecord: Bool) { + self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in + if let contentNode = contentNode { + contentNode.playOnceWithSound(playAndRecord: playAndRecord) + } + }) + } + + func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) { + self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in + if let contentNode = contentNode { + contentNode.setForceAudioToSpeaker(forceAudioToSpeaker) + } + }) + } + + func continuePlayingWithoutSound() { + self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in + if let contentNode = contentNode { + contentNode.continuePlayingWithoutSound() + } + }) + } + + func fetchControl(_ control: UniversalVideoNodeFetchControl) { + self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in + if let contentNode = contentNode { + contentNode.fetchControl(control) + } + }) + } + @objc func tapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { self.decoration.tap() diff --git a/TelegramUI/UrlHandling.swift b/TelegramUI/UrlHandling.swift index ba3c572859..9727984246 100644 --- a/TelegramUI/UrlHandling.swift +++ b/TelegramUI/UrlHandling.swift @@ -12,6 +12,7 @@ private enum ParsedInternalPeerUrlParameter { private enum ParsedInternalUrl { case peerName(String, ParsedInternalPeerUrlParameter?) case stickerPack(String) + case join(String) } private enum ParsedUrl { @@ -26,6 +27,8 @@ enum ResolvedUrl { case groupBotStart(peerId: PeerId, payload: String) case channelMessage(peerId: PeerId, messageId: MessageId) case stickerPack(name: String) + case instantView(TelegramMediaWebpage, String?) + case join(String) } private func parseInternalUrl(query: String) -> ParsedInternalUrl? { @@ -54,6 +57,8 @@ private func parseInternalUrl(query: String) -> ParsedInternalUrl? { } else if pathComponents.count == 2 { if pathComponents[0] == "addstickers" { return .stickerPack(pathComponents[1]) + } else if pathComponents[0] == "joinchat" || pathComponents[0] == "joinchannel" { + return .join(pathComponents[1]) } else if let value = Int(pathComponents[1]) { return .peerName(peerName, .channelMessage(Int32(value))) } else { @@ -92,13 +97,15 @@ private func resolveInternalUrl(account: Account, url: ParsedInternalUrl) -> Sig } case let .stickerPack(name): return .single(.stickerPack(name: name)) + case let .join(link): + return .single(.join(link)) } } func resolveUrl(account: Account, url: String) -> Signal { let schemes = ["http://", "https://", ""] - let basePaths = ["telegram.me", "t.me"] - for basePath in basePaths { + let baseTelegramMePaths = ["telegram.me", "t.me"] + for basePath in baseTelegramMePaths { for scheme in schemes { let basePrefix = scheme + basePath + "/" if url.lowercased().hasPrefix(basePrefix) { @@ -117,6 +124,29 @@ func resolveUrl(account: Account, url: String) -> Signal { } } } + let baseTelegraPhPaths = ["telegra.ph"] + for basePath in baseTelegraPhPaths { + for scheme in schemes { + let basePrefix = scheme + basePath + "/" + if url.lowercased().hasPrefix(basePrefix) { + return webpagePreview(account: account, url: url) + |> map { webpage -> ResolvedUrl in + if let webpage = webpage, case let .Loaded(content) = webpage.content, content.instantPage != nil { + var anchorValue: String? + if let anchorRange = url.range(of: "#") { + let anchor = url[anchorRange.upperBound...] + if !anchor.isEmpty { + anchorValue = String(anchor) + } + } + return .instantView(webpage, anchorValue) + } else { + return .externalUrl(url) + } + } + } + } + } return .single(.externalUrl(url)) } diff --git a/TelegramUI/UserInfoController.swift b/TelegramUI/UserInfoController.swift index db1c545db4..11ee840919 100644 --- a/TelegramUI/UserInfoController.swift +++ b/TelegramUI/UserInfoController.swift @@ -10,6 +10,9 @@ private final class UserInfoControllerArguments { let updateEditingName: (ItemListAvatarAndNameInfoItemName) -> Void let tapAvatarAction: () -> Void let openChat: () -> Void + let addContact: () -> Void + let shareContact: () -> Void + let startSecretChat: () -> Void let changeNotificationMuteSettings: () -> Void let changeNotificationSoundSettings: () -> Void let openSharedMedia: () -> Void @@ -17,15 +20,20 @@ private final class UserInfoControllerArguments { let updatePeerBlocked: (Bool) -> Void let deleteContact: () -> Void let displayUsernameContextMenu: (String) -> Void + let displayCopyContextMenu: (UserInfoEntryTag, String) -> Void let call: () -> Void let openCallMenu: (String) -> Void + let displayAboutContextMenu: (String) -> Void - init(account: Account, avatarAndNameInfoContext: ItemListAvatarAndNameInfoItemContext, updateEditingName: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, tapAvatarAction: @escaping () -> Void, openChat: @escaping () -> Void, changeNotificationMuteSettings: @escaping () -> Void, changeNotificationSoundSettings: @escaping () -> Void, openSharedMedia: @escaping () -> Void, openGroupsInCommon: @escaping () -> Void, updatePeerBlocked: @escaping (Bool) -> Void, deleteContact: @escaping () -> Void, displayUsernameContextMenu: @escaping (String) -> Void, call: @escaping () -> Void, openCallMenu: @escaping (String) -> Void) { + init(account: Account, avatarAndNameInfoContext: ItemListAvatarAndNameInfoItemContext, updateEditingName: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, tapAvatarAction: @escaping () -> Void, openChat: @escaping () -> Void, addContact: @escaping () -> Void, shareContact: @escaping () -> Void, startSecretChat: @escaping () -> Void, changeNotificationMuteSettings: @escaping () -> Void, changeNotificationSoundSettings: @escaping () -> Void, openSharedMedia: @escaping () -> Void, openGroupsInCommon: @escaping () -> Void, updatePeerBlocked: @escaping (Bool) -> Void, deleteContact: @escaping () -> Void, displayUsernameContextMenu: @escaping (String) -> Void, displayCopyContextMenu: @escaping (UserInfoEntryTag, String) -> Void, call: @escaping () -> Void, openCallMenu: @escaping (String) -> Void, displayAboutContextMenu: @escaping (String) -> Void) { self.account = account self.avatarAndNameInfoContext = avatarAndNameInfoContext self.updateEditingName = updateEditingName self.tapAvatarAction = tapAvatarAction self.openChat = openChat + self.addContact = addContact + self.shareContact = shareContact + self.startSecretChat = startSecretChat self.changeNotificationMuteSettings = changeNotificationMuteSettings self.changeNotificationSoundSettings = changeNotificationSoundSettings self.openSharedMedia = openSharedMedia @@ -33,8 +41,10 @@ private final class UserInfoControllerArguments { self.updatePeerBlocked = updatePeerBlocked self.deleteContact = deleteContact self.displayUsernameContextMenu = displayUsernameContextMenu + self.displayCopyContextMenu = displayCopyContextMenu self.call = call self.openCallMenu = openCallMenu + self.displayAboutContextMenu = displayAboutContextMenu } } @@ -46,15 +56,18 @@ private enum UserInfoSection: ItemListSectionId { } private enum UserInfoEntryTag { + case about + case phoneNumber case username } private enum UserInfoEntry: ItemListNodeEntry { case info(PresentationTheme, PresentationStrings, peer: Peer?, presence: PeerPresence?, cachedData: CachedPeerData?, state: ItemListAvatarAndNameInfoItemState, displayCall: Bool) case about(PresentationTheme, String, String) - case phoneNumber(PresentationTheme, Int, PhoneNumberWithLabel) + case phoneNumber(PresentationTheme, Int, String, String) case userName(PresentationTheme, String, String) case sendMessage(PresentationTheme, String) + case addContact(PresentationTheme, String) case shareContact(PresentationTheme, String) case startSecretChat(PresentationTheme, String) case sharedMedia(PresentationTheme, String) @@ -68,7 +81,7 @@ private enum UserInfoEntry: ItemListNodeEntry { switch self { case .info, .about, .phoneNumber, .userName: return UserInfoSection.info.rawValue - case .sendMessage, .shareContact, .startSecretChat: + case .sendMessage, .addContact, .shareContact, .startSecretChat: return UserInfoSection.actions.rawValue case .sharedMedia, .notifications, .notificationSound, .secretEncryptionKey, .groupsInCommon: return UserInfoSection.sharedMediaAndNotifications.rawValue @@ -129,8 +142,8 @@ private enum UserInfoEntry: ItemListNodeEntry { } else { return false } - case let .phoneNumber(lhsTheme, lhsIndex, lhsValue): - if case let .phoneNumber(rhsTheme, rhsIndex, rhsValue) = rhs, lhsTheme === rhsTheme, lhsIndex == rhsIndex, lhsValue == rhsValue { + case let .phoneNumber(lhsTheme, lhsIndex, lhsLabel, lhsValue): + if case let .phoneNumber(rhsTheme, rhsIndex, rhsLabel, rhsValue) = rhs, lhsTheme === rhsTheme, lhsIndex == rhsIndex, lhsLabel == rhsLabel, lhsValue == rhsValue { return true } else { return false @@ -147,6 +160,12 @@ private enum UserInfoEntry: ItemListNodeEntry { } else { return false } + case let .addContact(lhsTheme, lhsText): + if case let .addContact(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } case let .shareContact(lhsTheme, lhsText): if case let .shareContact(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true @@ -204,28 +223,30 @@ private enum UserInfoEntry: ItemListNodeEntry { return 0 case .about: return 1 - case let .phoneNumber(_, index, _): + case let .phoneNumber(_, index, _, _): return 2 + index case .userName: return 1000 case .sendMessage: return 1001 - case .shareContact: + case .addContact: return 1002 - case .startSecretChat: + case .shareContact: return 1003 - case .sharedMedia: + case .startSecretChat: return 1004 - case .notifications: + case .sharedMedia: return 1005 - case .notificationSound: + case .notifications: return 1006 - case .groupsInCommon: + case .notificationSound: return 1007 - case .secretEncryptionKey: + case .groupsInCommon: return 1008 - case .block: + case .secretEncryptionKey: return 1009 + case .block: + return 1010 } } @@ -236,7 +257,7 @@ private enum UserInfoEntry: ItemListNodeEntry { func item(_ arguments: UserInfoControllerArguments) -> ListViewItem { switch self { case let .info(theme, strings, peer, presence, cachedData, state, displayCall): - return ItemListAvatarAndNameInfoItem(account: arguments.account, theme: theme, strings: strings, peer: peer, presence: presence, cachedData: cachedData, state: state, sectionId: self.section, style: .plain, editingNameUpdated: { editingName in + return ItemListAvatarAndNameInfoItem(account: arguments.account, theme: theme, strings: strings, mode: .generic, peer: peer, presence: presence, cachedData: cachedData, state: state, sectionId: self.section, style: .plain, editingNameUpdated: { editingName in arguments.updateEditingName(editingName) }, avatarTapped: { arguments.tapAvatarAction() @@ -244,26 +265,36 @@ private enum UserInfoEntry: ItemListNodeEntry { arguments.call() } : nil) case let .about(theme, text, value): - return ItemListTextWithLabelItem(theme: theme, label: text, text: value, multiline: true, sectionId: self.section, action: nil) - case let .phoneNumber(theme, _, value): - return ItemListTextWithLabelItem(theme: theme, label: value.label, text: formatPhoneNumber(value.number), multiline: false, sectionId: self.section, action: { - arguments.openCallMenu(value.number) - }) + return ItemListTextWithLabelItem(theme: theme, label: text, text: value, enabledEntitiyTypes: [], multiline: true, sectionId: self.section, action: { + arguments.displayAboutContextMenu(value) + }, tag: UserInfoEntryTag.about) + case let .phoneNumber(theme, _, label, value): + return ItemListTextWithLabelItem(theme: theme, label: label, text: value, enabledEntitiyTypes: [], multiline: false, sectionId: self.section, action: { + arguments.openCallMenu(value) + }, longTapAction: { + arguments.displayCopyContextMenu(.phoneNumber, value) + }, tag: UserInfoEntryTag.phoneNumber) case let .userName(theme, text, value): - return ItemListTextWithLabelItem(theme: theme, label: text, text: "@\(value)", multiline: false, sectionId: self.section, action: { + return ItemListTextWithLabelItem(theme: theme, label: text, text: "@\(value)", enabledEntitiyTypes: [], multiline: false, sectionId: self.section, action: { arguments.displayUsernameContextMenu("@" + value) + }, longTapAction: { + arguments.displayCopyContextMenu(.username, value) }, tag: UserInfoEntryTag.username) case let .sendMessage(theme, text): return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { arguments.openChat() }) + case let .addContact(theme, text): + return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { + arguments.addContact() + }) case let .shareContact(theme, text): return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { - + arguments.shareContact() }) case let .startSecretChat(theme, text): return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { - + arguments.startSecretChat() }) case let .sharedMedia(theme, text): return ItemListDisclosureItem(theme: theme, title: text, label: "", sectionId: self.section, style: .plain, action: { @@ -386,7 +417,7 @@ private func userInfoEntries(account: Account, presentationData: PresentationDat } if let phoneNumber = user.phone, !phoneNumber.isEmpty { - entries.append(UserInfoEntry.phoneNumber(presentationData.theme, 0, PhoneNumberWithLabel(label: "home", number: phoneNumber))) + entries.append(UserInfoEntry.phoneNumber(presentationData.theme, 0, "home", formatPhoneNumber(phoneNumber))) } if !isEditing { @@ -398,6 +429,8 @@ private func userInfoEntries(account: Account, presentationData: PresentationDat entries.append(UserInfoEntry.sendMessage(presentationData.theme, presentationData.strings.UserInfo_SendMessage)) if view.peerIsContact { entries.append(UserInfoEntry.shareContact(presentationData.theme, presentationData.strings.UserInfo_ShareContact)) + } else if let phone = user.phone, !phone.isEmpty { + entries.append(UserInfoEntry.addContact(presentationData.theme, presentationData.strings.UserInfo_AddContact)) } entries.append(UserInfoEntry.startSecretChat(presentationData.theme, presentationData.strings.UserInfo_StartSecretChat)) } @@ -452,7 +485,8 @@ public func userInfoController(account: Account, peerId: PeerId) -> ViewControll var pushControllerImpl: ((ViewController) -> Void)? var presentControllerImpl: ((ViewController, Any?) -> Void)? var openChatImpl: (() -> Void)? - var displayUsernameContextMenuImpl: ((String) -> Void)? + var shareContactImpl: (() -> Void)? + var startSecretChatImpl: (() -> Void)? let actionsDisposable = DisposableSet() @@ -468,10 +502,16 @@ public func userInfoController(account: Account, peerId: PeerId) -> ViewControll let hiddenAvatarRepresentationDisposable = MetaDisposable() actionsDisposable.add(hiddenAvatarRepresentationDisposable) + let createSecretChatDisposable = MetaDisposable() + actionsDisposable.add(createSecretChatDisposable) + var avatarGalleryTransitionArguments: ((AvatarGalleryEntry) -> GalleryTransitionArguments?)? let avatarAndNameInfoContext = ItemListAvatarAndNameInfoItemContext() var updateHiddenAvatarImpl: (() -> Void)? + var displayAboutContextMenuImpl: ((String) -> Void)? + var displayCopyContextMenuImpl: ((UserInfoEntryTag, String) -> Void)? + let cachedAvatarEntries = Atomic?>(value: nil) let requestCallImpl: () -> Void = { @@ -521,6 +561,18 @@ public func userInfoController(account: Account, peerId: PeerId) -> ViewControll }) }, openChat: { openChatImpl?() + }, addContact: { + let _ = (account.postbox.modify { modifier -> TelegramUser? in + return modifier.getPeer(peerId) as? TelegramUser + }).start(next: { user in + if let user = user, let phone = user.phone, !phone.isEmpty { + let _ = addContactPeerInteractively(account: account, peerId: user.id, phone: phone).start() + } + }) + }, shareContact: { + shareContactImpl?() + }, startSecretChat: { + startSecretChatImpl?() }, changeNotificationMuteSettings: { let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } let controller = ActionSheetController(presentationTheme: presentationData.theme) @@ -582,15 +634,44 @@ public func userInfoController(account: Account, peerId: PeerId) -> ViewControll }, openGroupsInCommon: { pushControllerImpl?(groupsInCommonController(account: account, peerId: peerId)) }, updatePeerBlocked: { value in - updatePeerBlockedDisposable.set(requestUpdatePeerIsBlocked(account: account, peerId: peerId, isBlocked: value).start()) + let _ = (account.postbox.loadedPeerWithId(peerId) + |> take(1) + |> deliverOnMainQueue).start(next: { peer in + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + let text: String + if value { + text = presentationData.strings.UserInfo_BlockConfirmation(peer.displayTitle).0 + } else { + text = presentationData.strings.UserInfo_UnblockConfirmation(peer.displayTitle).0 + } + presentControllerImpl?(standardTextAlertController(title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_No, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Yes, action: { + updatePeerBlockedDisposable.set(requestUpdatePeerIsBlocked(account: account, peerId: peerId, isBlocked: value).start()) + })]), nil) + }) }, deleteContact: { - + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + let controller = ActionSheetController(presentationTheme: presentationData.theme) + let dismissAction: () -> Void = { [weak controller] in + controller?.dismissAnimated() + } + controller.setItemGroups([ + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.UserInfo_DeleteContact, color: .destructive, action: { + dismissAction() + updatePeerBlockedDisposable.set(deleteContactPeerInteractively(account: account, peerId: peerId).start()) + }) + ]), + ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) + ]) + presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) }, displayUsernameContextMenu: { text in - displayUsernameContextMenuImpl?(text) + let shareController = ShareController(account: account, subject: .url("\(text)")) + presentControllerImpl?(shareController, nil) + }, displayCopyContextMenu: { tag, phone in + displayCopyContextMenuImpl?(tag, phone) }, call: { requestCallImpl() }, openCallMenu: { number in - let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } let _ = (account.postbox.modify { modifier -> Peer? in return modifier.getPeer(peerId) } |> deliverOnMainQueue).start(next: { peer in @@ -618,6 +699,8 @@ public func userInfoController(account: Account, peerId: PeerId) -> ViewControll account.telegramApplicationContext.applicationBindings.openUrl("tel:\(formatPhoneNumber(number).replacingOccurrences(of: " ", with: ""))") } }) + }, displayAboutContextMenu: { text in + displayAboutContextMenuImpl?(text) }) let globalNotificationsKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.globalNotifications])) @@ -665,7 +748,7 @@ public func userInfoController(account: Account, peerId: PeerId) -> ViewControll updateState { state in if let editingState = state.editingState, let editingName = editingState.editingName { if let user = peer { - if ItemListAvatarAndNameInfoItemName(user.indexName) != editingName { + if ItemListAvatarAndNameInfoItemName(user) != editingName { updateName = editingName } } @@ -694,7 +777,7 @@ public func userInfoController(account: Account, peerId: PeerId) -> ViewControll rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Edit, style: .regular, enabled: true, action: { if let user = peer { updateState { state in - return state.withUpdatedEditingState(UserInfoEditingState(editingName: ItemListAvatarAndNameInfoItemName(user.indexName))) + return state.withUpdatedEditingState(UserInfoEditingState(editingName: ItemListAvatarAndNameInfoItemName(user))) } } }) @@ -718,37 +801,70 @@ public func userInfoController(account: Account, peerId: PeerId) -> ViewControll } openChatImpl = { [weak controller] in if let navigationController = (controller?.navigationController as? NavigationController) { - navigateToChatController(navigationController: navigationController, account: account, peerId: peerId) + navigateToChatController(navigationController: navigationController, account: account, chatLocation: .peer(peerId)) } } - displayUsernameContextMenuImpl = { [weak controller] text in - if let strongController = controller { - var resultItemNode: ListViewItemNode? - let _ = strongController.frameForItemNode({ itemNode in - if let itemNode = itemNode as? ItemListTextWithLabelItemNode { - if let tag = itemNode.tag as? UserInfoEntryTag { - if tag == .username { - resultItemNode = itemNode - return true + shareContactImpl = { [weak controller] in + let _ = (account.postbox.modify { modifier -> Peer? in + return modifier.getPeer(peerId) + } |> deliverOnMainQueue).start(next: { peer in + if let peer = peer as? TelegramUser, let phone = peer.phone { + let selectionController = PeerSelectionController(account: account) + selectionController.peerSelected = { [weak selectionController] peerId in + let _ = (enqueueMessages(account: account, peerId: peerId, messages: [.message(text: "", attributes: [], media: TelegramMediaContact(firstName: peer.firstName ?? "", lastName: peer.lastName ?? "", phoneNumber: phone, peerId: peer.id), replyToMessageId: nil, localGroupingKey: nil)]) |> deliverOnMainQueue).start(completed: { + if let controller = controller { + let ready = ValuePromise() + let _ = (ready.get() |> take(1) |> deliverOnMainQueue).start(next: { _ in + selectionController?.dismiss() + }) + (controller.navigationController as? NavigationController)?.replaceTopController(ChatController(account: account, chatLocation: .peer(peerId)), animated: false, ready: ready) } + }) + } + controller?.present(selectionController, in: .window(.root)) + } + }) + } + startSecretChatImpl = { [weak controller] in + let _ = (account.postbox.modify { modifier -> PeerId? in + let filteredPeerIds = Array(modifier.getAssociatedPeerIds(peerId)).filter { $0.namespace == Namespaces.Peer.SecretChat } + var activeIndices: [ChatListIndex] = [] + for associatedId in filteredPeerIds { + if let state = (modifier.getPeer(associatedId) as? TelegramSecretChat)?.embeddedState { + switch state { + case .active, .handshake: + if let (_, index) = modifier.getPeerChatListIndex(associatedId) { + activeIndices.append(index) + } + default: + break } } - return false - }) - if let resultItemNode = resultItemNode { - let contextMenuController = ContextMenuController(actions: [ContextMenuAction(content: .text("Copy"), action: { - UIPasteboard.general.string = text - })]) - strongController.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak resultItemNode] in - if let resultItemNode = resultItemNode { - return (resultItemNode, resultItemNode.contentBounds.insetBy(dx: 0.0, dy: -2.0)) - } else { - return nil + } + activeIndices.sort() + if let index = activeIndices.last { + return index.messageIndex.id.peerId + } else { + return nil + } + } |> deliverOnMainQueue).start(next: { currentPeerId in + if let currentPeerId = currentPeerId { + if let navigationController = (controller?.navigationController as? NavigationController) { + navigateToChatController(navigationController: navigationController, account: account, chatLocation: .peer(currentPeerId)) + } + } else { + createSecretChatDisposable.set((createSecretChat(account: account, peerId: peerId) |> deliverOnMainQueue).start(next: { peerId in + if let navigationController = (controller?.navigationController as? NavigationController) { + navigateToChatController(navigationController: navigationController, account: account, chatLocation: .peer(peerId)) + } + }, error: { _ in + if let controller = controller { + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + controller.present(standardTextAlertController(title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) } })) - } - } + }) } avatarGalleryTransitionArguments = { [weak controller] entry in if let controller = controller { @@ -774,5 +890,67 @@ public func userInfoController(account: Account, peerId: PeerId) -> ViewControll } } } + displayAboutContextMenuImpl = { [weak controller] text in + if let strongController = controller { + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + var resultItemNode: ListViewItemNode? + let _ = strongController.frameForItemNode({ itemNode in + if let itemNode = itemNode as? ItemListTextWithLabelItemNode { + if let tag = itemNode.tag as? UserInfoEntryTag { + if tag == .about { + resultItemNode = itemNode + return true + } + } + } + return false + }) + if let resultItemNode = resultItemNode { + let contextMenuController = ContextMenuController(actions: [ContextMenuAction(content: .text(presentationData.strings.Conversation_ContextMenuCopy), action: { + UIPasteboard.general.string = text + })]) + strongController.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak resultItemNode] in + if let resultItemNode = resultItemNode { + return (resultItemNode, resultItemNode.contentBounds.insetBy(dx: 0.0, dy: -2.0)) + } else { + return nil + } + })) + + } + } + } + + displayCopyContextMenuImpl = { [weak controller] tag, value in + if let strongController = controller { + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + var resultItemNode: ListViewItemNode? + let _ = strongController.frameForItemNode({ itemNode in + if let itemNode = itemNode as? ItemListTextWithLabelItemNode { + if let itemTag = itemNode.tag as? UserInfoEntryTag { + if itemTag == tag { + resultItemNode = itemNode + return true + } + } + } + return false + }) + if let resultItemNode = resultItemNode { + let contextMenuController = ContextMenuController(actions: [ContextMenuAction(content: .text(presentationData.strings.Conversation_ContextMenuCopy), action: { + UIPasteboard.general.string = value + })]) + strongController.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak resultItemNode] in + if let resultItemNode = resultItemNode { + return (resultItemNode, resultItemNode.contentBounds.insetBy(dx: 0.0, dy: -2.0)) + } else { + return nil + } + })) + + } + } + } + return controller } diff --git a/TelegramUI/UsernameSetupController.swift b/TelegramUI/UsernameSetupController.swift index 053bd73cc5..3d2686f3f3 100644 --- a/TelegramUI/UsernameSetupController.swift +++ b/TelegramUI/UsernameSetupController.swift @@ -109,7 +109,7 @@ private enum UsernameSetupEntry: ItemListNodeEntry { text = NSAttributedString(string: "Checking name...", textColor: UIColor(rgb: 0x6d6d72)) displayActivity = true } - return ItemListActivityTextItem(displayActivity: displayActivity, text: text, sectionId: self.section) + return ItemListActivityTextItem(displayActivity: displayActivity, theme: theme, text: text, sectionId: self.section) } } } diff --git a/TelegramUI/VerticalListContextResultsChatInputContextPanelNode.swift b/TelegramUI/VerticalListContextResultsChatInputContextPanelNode.swift index d1c34f2763..ff85525c8f 100644 --- a/TelegramUI/VerticalListContextResultsChatInputContextPanelNode.swift +++ b/TelegramUI/VerticalListContextResultsChatInputContextPanelNode.swift @@ -36,28 +36,28 @@ private enum VerticalChatContextResultsEntryStableId: Hashable { } private enum VerticalListContextResultsChatInputContextPanelEntry: Comparable, Identifiable { - case action(String) - case result(Int, ChatContextResult) + case action(PresentationTheme, String) + case result(Int, PresentationTheme, ChatContextResult) var stableId: VerticalChatContextResultsEntryStableId { switch self { case .action: return .action - case let .result(_, result): + case let .result(_, _, result): return .result(result) } } static func ==(lhs: VerticalListContextResultsChatInputContextPanelEntry, rhs: VerticalListContextResultsChatInputContextPanelEntry) -> Bool { switch lhs { - case let .action(title): - if case .action(title) = rhs { + case let .action(lhsTheme, lhsTitle): + if case let .action(rhsTheme, rhsTitle) = rhs, lhsTheme === rhsTheme && lhsTitle == rhsTitle { return true } else { return false } - case let .result(index, result): - if case .result(index, result) = rhs { + case let .result(lhsIndex, lhsTheme, lhsResult): + if case let .result(rhsIndex, rhsTheme, rhsResult) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsResult == rhsResult { return true } else { return false @@ -69,11 +69,11 @@ private enum VerticalListContextResultsChatInputContextPanelEntry: Comparable, I switch lhs { case .action: return true - case let .result(index, _): + case let .result(index, _, _): switch rhs { case .action: return false - case let .result(rhsIndex, _): + case let .result(rhsIndex, _, _): return index < rhsIndex } } @@ -81,10 +81,10 @@ private enum VerticalListContextResultsChatInputContextPanelEntry: Comparable, I func item(account: Account, actionSelected: @escaping () -> Void, resultSelected: @escaping (ChatContextResult) -> Void) -> ListViewItem { switch self { - case let .action(title): - return VerticalListContextResultsChatInputPanelButtonItem(title: title, pressed: actionSelected) - case let .result(_, result): - return VerticalListContextResultsChatInputPanelItem(account: account, result: result, resultSelected: resultSelected) + case let .action(theme, title): + return VerticalListContextResultsChatInputPanelButtonItem(theme: theme, title: title, pressed: actionSelected) + case let .result(_, theme, result): + return VerticalListContextResultsChatInputPanelItem(account: account, theme: theme, result: result, resultSelected: resultSelected) } } } @@ -111,17 +111,21 @@ final class VerticalListContextResultsChatInputContextPanelNode: ChatInputContex private var currentEntries: [VerticalListContextResultsChatInputContextPanelEntry]? private var enqueuedTransitions: [(VerticalListContextResultsChatInputContextPanelTransition, Bool)] = [] - private var hasValidLayout = false + private var validLayout: (CGSize, CGFloat, CGFloat)? - override init(account: Account) { + private var theme: PresentationTheme + + override init(account: Account, theme: PresentationTheme, strings: PresentationStrings) { + self.theme = theme + self.listView = ListView() self.listView.isOpaque = false self.listView.stackFromBottom = true - self.listView.keepBottomItemOverscrollBackground = .white + self.listView.keepBottomItemOverscrollBackground = theme.list.plainBackgroundColor self.listView.limitHitTestToNodes = true self.listView.isHidden = true - super.init(account: account) + super.init(account: account, theme: theme, strings: strings) self.isOpaque = false self.clipsToBounds = true @@ -135,12 +139,12 @@ final class VerticalListContextResultsChatInputContextPanelNode: ChatInputContex var index = 0 var resultIds = Set() if let switchPeer = results.switchPeer { - let entry: VerticalListContextResultsChatInputContextPanelEntry = .action(switchPeer.text) + let entry: VerticalListContextResultsChatInputContextPanelEntry = .action(self.theme, switchPeer.text) entries.append(entry) resultIds.insert(entry.stableId) } for result in results.results { - let entry: VerticalListContextResultsChatInputContextPanelEntry = .result(index, result) + let entry: VerticalListContextResultsChatInputContextPanelEntry = .result(index, self.theme, result) if resultIds.contains(entry.stableId) { continue } else { @@ -167,7 +171,7 @@ final class VerticalListContextResultsChatInputContextPanelNode: ChatInputContex private func enqueueTransition(_ transition: VerticalListContextResultsChatInputContextPanelTransition, firstTime: Bool) { enqueuedTransitions.append((transition, firstTime)) - if self.hasValidLayout { + if self.validLayout != nil { while !self.enqueuedTransitions.isEmpty { self.dequeueTransition() } @@ -175,7 +179,7 @@ final class VerticalListContextResultsChatInputContextPanelNode: ChatInputContex } private func dequeueTransition() { - if let (transition, firstTime) = self.enqueuedTransitions.first { + if let validLayout = self.validLayout, let (transition, firstTime) = self.enqueuedTransitions.first { self.enqueuedTransitions.remove(at: 0) var options = ListViewDeleteAndInsertOptions() @@ -188,7 +192,9 @@ final class VerticalListContextResultsChatInputContextPanelNode: ChatInputContex } var insets = UIEdgeInsets() - insets.top = topInsetForLayout(size: self.listView.bounds.size, hasSwitchPeer: self.currentResults?.switchPeer != nil) + insets.top = topInsetForLayout(size: validLayout.0, hasSwitchPeer: self.currentResults?.switchPeer != nil) + insets.left = validLayout.1 + insets.right = validLayout.2 let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: self.listView.bounds.size, insets: insets, duration: 0.0, curve: .Default) @@ -221,9 +227,14 @@ final class VerticalListContextResultsChatInputContextPanelNode: ChatInputContex return max(size.height - minimumItemHeights, 0.0) } - override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) { + override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) { + let hadValidLayout = self.validLayout != nil + self.validLayout = (size, leftInset, rightInset) + var insets = UIEdgeInsets() insets.top = self.topInsetForLayout(size: size, hasSwitchPeer: self.currentResults?.switchPeer != nil) + insets.left = leftInset + insets.right = rightInset transition.updateFrame(node: self.listView, frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)) @@ -253,8 +264,7 @@ final class VerticalListContextResultsChatInputContextPanelNode: ChatInputContex self.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) - if !hasValidLayout { - hasValidLayout = true + if !hadValidLayout { while !self.enqueuedTransitions.isEmpty { self.dequeueTransition() } diff --git a/TelegramUI/VerticalListContextResultsChatInputPanelButtonItem.swift b/TelegramUI/VerticalListContextResultsChatInputPanelButtonItem.swift index aff6c15c11..8882bb56cd 100644 --- a/TelegramUI/VerticalListContextResultsChatInputPanelButtonItem.swift +++ b/TelegramUI/VerticalListContextResultsChatInputPanelButtonItem.swift @@ -6,21 +6,23 @@ import SwiftSignalKit import Postbox final class VerticalListContextResultsChatInputPanelButtonItem: ListViewItem { + fileprivate let theme: PresentationTheme fileprivate let title: String fileprivate let pressed: () -> Void - public init(title: String, pressed: @escaping () -> Void) { + public init(theme: PresentationTheme, title: String, pressed: @escaping () -> Void) { + self.theme = theme self.title = title self.pressed = pressed } - public func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { let configure = { () -> Void in let node = VerticalListContextResultsChatInputPanelButtonItemNode() let nodeLayout = node.asyncLayout() let (top, bottom) = (previousItem != nil, nextItem != nil) - let (layout, apply) = nodeLayout(self, width, top, bottom) + let (layout, apply) = nodeLayout(self, params, top, bottom) node.contentSize = layout.contentSize node.insets = layout.insets @@ -38,7 +40,7 @@ final class VerticalListContextResultsChatInputPanelButtonItem: ListViewItem { } } - public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { if let node = node as? VerticalListContextResultsChatInputPanelButtonItemNode { Queue.mainQueue().async { let nodeLayout = node.asyncLayout() @@ -46,7 +48,7 @@ final class VerticalListContextResultsChatInputPanelButtonItem: ListViewItem { async { let (top, bottom) = (previousItem != nil, nextItem != nil) - let (layout, apply) = nodeLayout(self, width, top, bottom) + let (layout, apply) = nodeLayout(self, params, top, bottom) Queue.mainQueue().async { completion(layout, { apply(animation) @@ -76,19 +78,15 @@ final class VerticalListContextResultsChatInputPanelButtonItemNode: ListViewItem self.buttonNode = HighlightTrackingButtonNode() self.topSeparatorNode = ASDisplayNode() - self.topSeparatorNode.backgroundColor = UIColor(rgb: 0xC9CDD1) self.topSeparatorNode.isLayerBacked = true self.separatorNode = ASDisplayNode() - self.separatorNode.backgroundColor = UIColor(rgb: 0xD6D6DA) self.separatorNode.isLayerBacked = true self.titleNode = TextNode() super.init(layerBacked: false, dynamicBounce: false) - self.backgroundColor = .white - self.addSubnode(self.topSeparatorNode) self.addSubnode(self.separatorNode) @@ -109,40 +107,44 @@ final class VerticalListContextResultsChatInputPanelButtonItemNode: ListViewItem self.buttonNode.addTarget(self, action: #selector(buttonPressed), forControlEvents: .touchUpInside) } - override public func layoutForWidth(_ width: CGFloat, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { + override public func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { if let item = item as? VerticalListContextResultsChatInputPanelButtonItem { let doLayout = self.asyncLayout() let merged = (top: previousItem != nil, bottom: nextItem != nil) - let (layout, apply) = doLayout(item, width, merged.top, merged.bottom) + let (layout, apply) = doLayout(item, params, merged.top, merged.bottom) self.contentSize = layout.contentSize self.insets = layout.insets apply(.None) } } - func asyncLayout() -> (_ item: VerticalListContextResultsChatInputPanelButtonItem, _ width: CGFloat, _ mergedTop: Bool, _ mergedBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { + func asyncLayout() -> (_ item: VerticalListContextResultsChatInputPanelButtonItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) - return { [weak self] item, width, mergedTop, mergedBottom in - let titleString = NSAttributedString(string: item.title, font: titleFont, textColor: UIColor(rgb: 0x007ee5)) + return { [weak self] item, params, mergedTop, mergedBottom in + let titleString = NSAttributedString(string: item.title, font: titleFont, textColor: item.theme.list.itemAccentColor) - let (titleLayout, titleApply) = makeTitleLayout(titleString, nil, 1, .end, CGSize(width: width - 16.0, height: 100.0), .natural, nil, UIEdgeInsets()) + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 16.0, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: width, height: VerticalListContextResultsChatInputPanelButtonItemNode.itemHeight), insets: UIEdgeInsets()) + let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: VerticalListContextResultsChatInputPanelButtonItemNode.itemHeight), insets: UIEdgeInsets()) return (nodeLayout, { _ in if let strongSelf = self { strongSelf.item = item - titleApply() + strongSelf.separatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor + strongSelf.topSeparatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor + strongSelf.backgroundColor = item.theme.list.plainBackgroundColor - strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: floor((width - titleLayout.size.width) / 2.0), y: floor((nodeLayout.contentSize.height - titleLayout.size.height) / 2.0) + 2.0), size: titleLayout.size) + let _ = titleApply() + + strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: floor((params.width - titleLayout.size.width) / 2.0), y: floor((nodeLayout.contentSize.height - titleLayout.size.height) / 2.0) + 2.0), size: titleLayout.size) strongSelf.topSeparatorNode.isHidden = mergedTop strongSelf.separatorNode.isHidden = !mergedBottom - strongSelf.topSeparatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: UIScreenPixel)) - strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: nodeLayout.contentSize.height - UIScreenPixel), size: CGSize(width: width, height: UIScreenPixel)) + strongSelf.topSeparatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.width, height: UIScreenPixel)) + strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: nodeLayout.contentSize.height - UIScreenPixel), size: CGSize(width: params.width, height: UIScreenPixel)) strongSelf.buttonNode.frame = CGRect(origin: CGPoint(), size: nodeLayout.contentSize) } diff --git a/TelegramUI/VerticalListContextResultsChatInputPanelItem.swift b/TelegramUI/VerticalListContextResultsChatInputPanelItem.swift index 03d588289c..e870da6627 100644 --- a/TelegramUI/VerticalListContextResultsChatInputPanelItem.swift +++ b/TelegramUI/VerticalListContextResultsChatInputPanelItem.swift @@ -7,24 +7,26 @@ import Postbox final class VerticalListContextResultsChatInputPanelItem: ListViewItem { fileprivate let account: Account + fileprivate let theme: PresentationTheme fileprivate let result: ChatContextResult private let resultSelected: (ChatContextResult) -> Void let selectable: Bool = true - public init(account: Account, result: ChatContextResult, resultSelected: @escaping (ChatContextResult) -> Void) { + public init(account: Account, theme: PresentationTheme, result: ChatContextResult, resultSelected: @escaping (ChatContextResult) -> Void) { self.account = account + self.theme = theme self.result = result self.resultSelected = resultSelected } - public func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { let configure = { () -> Void in let node = VerticalListContextResultsChatInputPanelItemNode() let nodeLayout = node.asyncLayout() let (top, bottom) = (previousItem != nil, nextItem != nil) - let (layout, apply) = nodeLayout(self, width, top, bottom) + let (layout, apply) = nodeLayout(self, params, top, bottom) node.contentSize = layout.contentSize node.insets = layout.insets @@ -42,7 +44,7 @@ final class VerticalListContextResultsChatInputPanelItem: ListViewItem { } } - public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { if let node = node as? VerticalListContextResultsChatInputPanelItemNode { Queue.mainQueue().async { let nodeLayout = node.asyncLayout() @@ -50,7 +52,7 @@ final class VerticalListContextResultsChatInputPanelItem: ListViewItem { async { let (top, bottom) = (previousItem != nil, nextItem != nil) - let (layout, apply) = nodeLayout(self, width, top, bottom) + let (layout, apply) = nodeLayout(self, params, top, bottom) Queue.mainQueue().async { completion(layout, { apply(animation) @@ -92,15 +94,12 @@ final class VerticalListContextResultsChatInputPanelItemNode: ListViewItemNode { self.textNode = TextNode() self.topSeparatorNode = ASDisplayNode() - self.topSeparatorNode.backgroundColor = UIColor(rgb: 0xC9CDD1) self.topSeparatorNode.isLayerBacked = true self.separatorNode = ASDisplayNode() - self.separatorNode.backgroundColor = UIColor(rgb: 0xD6D6DA) self.separatorNode.isLayerBacked = true self.highlightedBackgroundNode = ASDisplayNode() - self.highlightedBackgroundNode.backgroundColor = UIColor(rgb: 0xd9d9d9) self.highlightedBackgroundNode.isLayerBacked = true self.iconTextBackgroundNode = ASImageNode() @@ -112,13 +111,12 @@ final class VerticalListContextResultsChatInputPanelItemNode: ListViewItemNode { self.iconTextNode.isLayerBacked = true self.iconImageNode = TransformImageNode() + self.iconImageNode.contentAnimations = [.subsequentUpdates] self.iconImageNode.isLayerBacked = true self.iconImageNode.displaysAsynchronously = false super.init(layerBacked: false, dynamicBounce: false) - self.backgroundColor = .white - self.addSubnode(self.topSeparatorNode) self.addSubnode(self.separatorNode) @@ -127,27 +125,27 @@ final class VerticalListContextResultsChatInputPanelItemNode: ListViewItemNode { self.addSubnode(self.textNode) } - override public func layoutForWidth(_ width: CGFloat, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { + override public func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { if let item = item as? VerticalListContextResultsChatInputPanelItem { let doLayout = self.asyncLayout() let merged = (top: previousItem != nil, bottom: nextItem != nil) - let (layout, apply) = doLayout(item, width, merged.top, merged.bottom) + let (layout, apply) = doLayout(item, params, merged.top, merged.bottom) self.contentSize = layout.contentSize self.insets = layout.insets apply(.None) } } - func asyncLayout() -> (_ item: VerticalListContextResultsChatInputPanelItem, _ width: CGFloat, _ mergedTop: Bool, _ mergedBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { + func asyncLayout() -> (_ item: VerticalListContextResultsChatInputPanelItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeTextLayout = TextNode.asyncLayout(self.textNode) let iconTextMakeLayout = TextNode.asyncLayout(self.iconTextNode) let iconImageLayout = self.iconImageNode.asyncLayout() let currentIconImageResource = self.currentIconImageResource - return { [weak self] item, width, mergedTop, mergedBottom in - let leftInset: CGFloat = 80.0 - let rightInset: CGFloat = 10.0 + return { [weak self] item, params, mergedTop, mergedBottom in + let leftInset: CGFloat = 80.0 + params.leftInset + let rightInset: CGFloat = 10.0 + params.rightInset let applyIconTextBackgroundImage = iconTextBackgroundImage @@ -159,11 +157,11 @@ final class VerticalListContextResultsChatInputPanelItemNode: ListViewItemNode { var updateIconImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? if let title = item.result.title { - titleString = NSAttributedString(string: title, font: titleFont, textColor: .black) + titleString = NSAttributedString(string: title, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor) } if let text = item.result.description { - textString = NSAttributedString(string: text, font: textFont, textColor: UIColor(rgb: 0x8e8e93)) + textString = NSAttributedString(string: text, font: textFont, textColor: item.theme.list.itemSecondaryTextColor) } var imageResource: TelegramMediaResource? @@ -218,18 +216,18 @@ final class VerticalListContextResultsChatInputPanelItemNode: ListViewItemNode { if updatedIconImageResource { if let imageResource = imageResource { let tmpRepresentation = TelegramMediaImageRepresentation(dimensions: CGSize(width: 55.0, height: 55.0), resource: imageResource) - let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [tmpRepresentation]) + let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [tmpRepresentation], reference: nil) updateIconImageSignal = chatWebpageSnippetPhoto(account: item.account, photo: tmpImage) } else { updateIconImageSignal = .complete() } } - let (titleLayout, titleApply) = makeTitleLayout(titleString, nil, 1, .end, CGSize(width: width - leftInset - rightInset, height: 100.0), .natural, nil, UIEdgeInsets()) + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let (textLayout, textApply) = makeTextLayout(textString, nil, 2, .end, CGSize(width: width - leftInset - rightInset, height: 100.0), .natural, nil, UIEdgeInsets()) + let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: textString, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let (iconTextLayout, iconTextApply) = iconTextMakeLayout(iconText, nil, 1, .end, CGSize(width: 38.0, height: CGFloat.infinity), .natural, nil, UIEdgeInsets()) + let (iconTextLayout, iconTextApply) = iconTextMakeLayout(TextNodeLayoutArguments(attributedString: iconText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 38.0, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) var titleFrame: CGRect? if let _ = titleString { @@ -245,12 +243,17 @@ final class VerticalListContextResultsChatInputPanelItemNode: ListViewItemNode { textFrame = CGRect(origin: CGPoint(x: leftInset, y: topOffset), size: textLayout.size) } - let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: width, height: VerticalListContextResultsChatInputPanelItemNode.itemHeight), insets: UIEdgeInsets()) + let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: VerticalListContextResultsChatInputPanelItemNode.itemHeight), insets: UIEdgeInsets()) return (nodeLayout, { _ in if let strongSelf = self { - titleApply() - textApply() + strongSelf.separatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor + strongSelf.topSeparatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor + strongSelf.backgroundColor = item.theme.list.plainBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor + + let _ = titleApply() + let _ = textApply() if let titleFrame = titleFrame { strongSelf.titleNode.frame = titleFrame @@ -259,7 +262,7 @@ final class VerticalListContextResultsChatInputPanelItemNode: ListViewItemNode { strongSelf.textNode.frame = textFrame } - let iconFrame = CGRect(origin: CGPoint(x: 12.0, y: 11.0), size: CGSize(width: 55.0, height: 55.0)) + let iconFrame = CGRect(origin: CGPoint(x: params.leftInset + 12.0, y: 11.0), size: CGSize(width: 55.0, height: 55.0)) strongSelf.iconTextNode.frame = CGRect(origin: CGPoint(x: iconFrame.minX + floor((55.0 - iconTextLayout.size.width) / 2.0), y: iconFrame.minY + floor((55.0 - iconTextLayout.size.height) / 2.0) + 2.0), size: iconTextLayout.size) let _ = iconTextApply() @@ -268,7 +271,7 @@ final class VerticalListContextResultsChatInputPanelItemNode: ListViewItemNode { if let iconImageApply = iconImageApply { if let updateImageSignal = updateIconImageSignal { - strongSelf.iconImageNode.setSignal(account: item.account, signal: updateImageSignal) + strongSelf.iconImageNode.setSignal(updateImageSignal) } if strongSelf.iconImageNode.supernode == nil { @@ -301,17 +304,17 @@ final class VerticalListContextResultsChatInputPanelItemNode: ListViewItemNode { strongSelf.topSeparatorNode.isHidden = mergedTop strongSelf.separatorNode.isHidden = !mergedBottom - strongSelf.topSeparatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: UIScreenPixel)) - strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: nodeLayout.contentSize.height - UIScreenPixel), size: CGSize(width: width - leftInset, height: UIScreenPixel)) + strongSelf.topSeparatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.width, height: UIScreenPixel)) + strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: nodeLayout.contentSize.height - UIScreenPixel), size: CGSize(width: params.width - leftInset, height: UIScreenPixel)) - strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: nodeLayout.size.height + UIScreenPixel)) + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.width, height: nodeLayout.size.height + UIScreenPixel)) } }) } } - override func setHighlighted(_ highlighted: Bool, animated: Bool) { - super.setHighlighted(highlighted, animated: animated) + override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { + super.setHighlighted(highlighted, at: point, animated: animated) if highlighted { self.highlightedBackgroundNode.alpha = 1.0 diff --git a/TelegramUI/VideoPlayerProxy.swift b/TelegramUI/VideoPlayerProxy.swift index d02745a7ec..40ba6290e9 100644 --- a/TelegramUI/VideoPlayerProxy.swift +++ b/TelegramUI/VideoPlayerProxy.swift @@ -24,7 +24,7 @@ private final class VideoPlayerProxyContext { } } - var state: (timebase: CMTimebase, requestFrames: Bool, rotationAngle: Double)? { + var state: (timebase: CMTimebase, requestFrames: Bool, rotationAngle: Double, aspect: Double)? { didSet { self.node?.state = self.state } @@ -49,7 +49,7 @@ final class VideoPlayerProxy { } } - var state: (timebase: CMTimebase, requestFrames: Bool, rotationAngle: Double)? { + var state: (timebase: CMTimebase, requestFrames: Bool, rotationAngle: Double, aspect: Double)? { didSet { let updatedState = self.state self.withContext { context in diff --git a/TelegramUI/VoiceCallDataSavingController.swift b/TelegramUI/VoiceCallDataSavingController.swift index 6d62defa61..71a1e3f75e 100644 --- a/TelegramUI/VoiceCallDataSavingController.swift +++ b/TelegramUI/VoiceCallDataSavingController.swift @@ -92,24 +92,24 @@ private enum VoiceCallDataSavingEntry: ItemListNodeEntry { } } -private func stringForDataSavingOption(_ option: VoiceCallDataSaving) -> String { +private func stringForDataSavingOption(_ option: VoiceCallDataSaving, strings: PresentationStrings) -> String { switch option { case .never: - return "Never" + return strings.CallSettings_Never case .cellular: - return "On Mobile Network" + return strings.CallSettings_OnMobile case .always: - return "Always" + return strings.CallSettings_Always } } private func voiceCallDataSavingControllerEntries(presentationData: PresentationData, settings: VoiceCallSettings) -> [VoiceCallDataSavingEntry] { var entries: [VoiceCallDataSavingEntry] = [] - entries.append(.never(presentationData.theme, stringForDataSavingOption(.never), settings.dataSaving == .never)) - entries.append(.cellular(presentationData.theme, stringForDataSavingOption(.cellular), settings.dataSaving == .cellular)) - entries.append(.always(presentationData.theme, stringForDataSavingOption(.always), settings.dataSaving == .always)) - entries.append(.info(presentationData.theme, "Using less data may improve your experience on bad networks, but will slightly decrease audio quality.")) + entries.append(.never(presentationData.theme, stringForDataSavingOption(.never, strings: presentationData.strings), settings.dataSaving == .never)) + entries.append(.cellular(presentationData.theme, stringForDataSavingOption(.cellular, strings: presentationData.strings), settings.dataSaving == .cellular)) + entries.append(.always(presentationData.theme, stringForDataSavingOption(.always, strings: presentationData.strings), settings.dataSaving == .always)) + entries.append(.info(presentationData.theme, presentationData.strings.CallSettings_UseLessDataLongDescription)) return entries } @@ -137,7 +137,7 @@ func voiceCallDataSavingController(account: Account) -> ViewController { let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, voiceCallSettingsPromise.get()) |> deliverOnMainQueue |> map { presentationData, data -> (ItemListControllerState, (ItemListNodeState, VoiceCallDataSavingEntry.ItemGenerationArguments)) in - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text("Use Less Data"), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.CallSettings_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) let listState = ItemListNodeState(entries: voiceCallDataSavingControllerEntries(presentationData: presentationData, settings: data), style: .blocks, emptyStateItem: nil, animateChanges: false) return (controllerState, (listState, arguments)) diff --git a/TelegramUI/WebController.swift b/TelegramUI/WebController.swift new file mode 100644 index 0000000000..ceee7f376a --- /dev/null +++ b/TelegramUI/WebController.swift @@ -0,0 +1,34 @@ +import Foundation +import Display +import SafariServices + +final class WebController: ViewController { + private let url: URL + + private var controllerNode: WebControllerNode { + return self.displayNode as! WebControllerNode + } + + init(url: URL) { + self.url = url + + super.init(navigationBarTheme: nil) + + self.edgesForExtendedLayout = [] + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadDisplayNode() { + self.displayNode = WebControllerNode(url: self.url) + + self.displayNodeDidLoad() + } + + override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + self.controllerNode.containerLayoutUpdated(layout, transition: transition) + } +} diff --git a/TelegramUI/WebControllerNode.swift b/TelegramUI/WebControllerNode.swift new file mode 100644 index 0000000000..c916cc3526 --- /dev/null +++ b/TelegramUI/WebControllerNode.swift @@ -0,0 +1,34 @@ +import Foundation +import AsyncDisplayKit +import Display +import WebKit + +final class WebControllerNode: ViewControllerTracingNode { + private let webView: WKWebView + + init(url: URL) { + let configuration = WKWebViewConfiguration() + self.webView = WKWebView(frame: CGRect(), configuration: configuration) + if #available(iOSApplicationExtension 9.0, *) { + self.webView.allowsLinkPreview = false + } + self.webView.allowsBackForwardNavigationGestures = true + //webView.navigationDelegate = self + + super.init() + + self.view.addSubview(self.webView) + if #available(iOSApplicationExtension 11.0, *) { + self.webView.scrollView.contentInsetAdjustmentBehavior = .never + } + self.webView.scrollView.contentInset = UIEdgeInsets(top: 0.0, left: 10.0, bottom: 0.0, right: 10.0) + + self.webView.load(URLRequest(url: url)) + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + transition.animateView { + self.webView.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: max(1.0, layout.size.height))) + } + } +} diff --git a/TelegramUI/WebEmbedVideoContent.swift b/TelegramUI/WebEmbedVideoContent.swift index e33459a2e2..44211f7943 100644 --- a/TelegramUI/WebEmbedVideoContent.swift +++ b/TelegramUI/WebEmbedVideoContent.swift @@ -7,6 +7,25 @@ import TelegramCore import LegacyComponents +func webEmbedVideoContentSupportsWebpage(_ webpageContent: TelegramMediaWebpageLoadedContent) -> Bool { + let converted = TGWebPageMediaAttachment() + + converted.url = webpageContent.url + converted.displayUrl = webpageContent.displayUrl + converted.pageType = webpageContent.type + converted.siteName = webpageContent.websiteName + converted.title = webpageContent.title + converted.pageDescription = webpageContent.text + converted.embedUrl = webpageContent.embedUrl + converted.embedType = webpageContent.embedType + converted.embedSize = webpageContent.embedSize ?? CGSize() + let approximateDuration = Int32(webpageContent.duration ?? 0) + converted.duration = approximateDuration as NSNumber + converted.author = webpageContent.author + + return TGEmbedPlayerView.hasNativeSupportFor(x: converted) +} + final class WebEmbedVideoContent: UniversalVideoContent { let id: AnyHashable let webpageContent: TelegramMediaWebpageLoadedContent @@ -23,27 +42,31 @@ final class WebEmbedVideoContent: UniversalVideoContent { self.duration = Int32(webpageContent.duration ?? (0 as Int)) } - func makeContentNode(account: Account) -> UniversalVideoContentNode & ASDisplayNode { - return WebEmbedVideoContentNode(account: account, audioSessionManager: account.telegramApplicationContext.mediaManager.audioSession, webpageContent: self.webpageContent) + func makeContentNode(postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode { + return WebEmbedVideoContentNode(postbox: postbox, audioSessionManager: audioSession, webpageContent: self.webpageContent) } } private final class WebEmbedVideoContentNode: ASDisplayNode, UniversalVideoContentNode { private let webpageContent: TelegramMediaWebpageLoadedContent private let intrinsicDimensions: CGSize + private let approximateDuration: Int32 private let playerView: TGEmbedPlayerView + private let playerViewContainer: UIView private let audioSessionDisposable = MetaDisposable() private var hasAudioSession = false private let playbackCompletedListeners = Bag<() -> Void>() private var initializedStatus = false - private let _status = Promise() + private let _status = ValuePromise() var status: Signal { return self._status.get() } + private var seekId: Int = 0 + private let _ready = Promise() var ready: Signal { return self._ready.get() @@ -58,8 +81,9 @@ private final class WebEmbedVideoContentNode: ASDisplayNode, UniversalVideoConte private var thumbnailDisposable: Disposable? private var loadProgressDisposable: Disposable? + private var statusDisposable: Disposable? - init(account: Account, audioSessionManager: ManagedAudioSession, webpageContent: TelegramMediaWebpageLoadedContent) { + init(postbox: Postbox, audioSessionManager: ManagedAudioSession, webpageContent: TelegramMediaWebpageLoadedContent) { self.webpageContent = webpageContent let converted = TGWebPageMediaAttachment() @@ -73,7 +97,8 @@ private final class WebEmbedVideoContentNode: ASDisplayNode, UniversalVideoConte converted.embedUrl = webpageContent.embedUrl converted.embedType = webpageContent.embedType converted.embedSize = webpageContent.embedSize ?? CGSize() - converted.duration = webpageContent.duration.flatMap { NSNumber.init(value: $0) } ?? 0 + self.approximateDuration = Int32(webpageContent.duration ?? 0) + converted.duration = self.approximateDuration as NSNumber converted.author = webpageContent.author if let embedSize = webpageContent.embedSize { @@ -96,16 +121,20 @@ private final class WebEmbedVideoContentNode: ASDisplayNode, UniversalVideoConte }) } + self.playerViewContainer = UIView() + self.playerView = TGEmbedPlayerView.make(forWebPage: converted, thumbnailSignal: thumbmnailSignal)! self.playerView.frame = CGRect(origin: CGPoint(), size: self.intrinsicDimensions) + self.playerViewContainer.frame = CGRect(origin: CGPoint(), size: self.intrinsicDimensions) self.playerView.disallowPIP = true self.playerView.isUserInteractionEnabled = false - self.playerView.disallowAutoplay = true + //self.playerView.disallowAutoplay = true self.playerView.disableControls = true super.init() - self.view.addSubview(self.playerView) + self.playerViewContainer.addSubview(self.playerView) + self.view.addSubview(self.playerViewContainer) self.playerView.setup(withEmbedSize: self.intrinsicDimensions) let nativeLoadProgress = self.playerView.loadProgress() @@ -124,7 +153,7 @@ private final class WebEmbedVideoContentNode: ASDisplayNode, UniversalVideoConte }) if let image = webpageContent.image { - self.thumbnailDisposable = (rawMessagePhoto(account: account, photo: image) |> deliverOnMainQueue).start(next: { [weak self] image in + self.thumbnailDisposable = (rawMessagePhoto(postbox: postbox, photo: image) |> deliverOnMainQueue).start(next: { [weak self] image in if let strongSelf = self { strongSelf.thumbnail.set(.single(image)) strongSelf._ready.set(.single(Void())) @@ -134,27 +163,37 @@ private final class WebEmbedVideoContentNode: ASDisplayNode, UniversalVideoConte self._ready.set(.single(Void())) } + self._status.set(MediaPlayerStatus(generationTimestamp: 0.0, duration: Double(self.approximateDuration), timestamp: 0.0, seekId: self.seekId, status: .buffering(initial: true, whilePlaying: true))) + let stateSignal = self.playerView.stateSignal()! - self._status.set(Signal { subscriber in + self.statusDisposable = (Signal { subscriber in let innerDisposable = stateSignal.start(next: { next in if let next = next as? TGEmbedPlayerState { let status: MediaPlayerPlaybackStatus if next.playing { status = .playing - } else if next.downloadProgress.isEqual(to: 1.0) { - status = .buffering(whilePlaying: next.playing) + } else if next.buffering { + status = .buffering(initial: false, whilePlaying: next.playing) } else { status = .paused } - subscriber.putNext(MediaPlayerStatus(generationTimestamp: 0.0, duration: next.duration, timestamp: next.position, status: status)) + subscriber.putNext(MediaPlayerStatus(generationTimestamp: 0.0, duration: next.duration, timestamp: max(0.0, next.position), seekId: 0, status: status)) } }) return ActionDisposable { innerDisposable?.dispose() } + } |> deliverOnMainQueue).start(next: { [weak self] value in + if let strongSelf = self { + if !strongSelf.initializedStatus { + if case .paused = value.status { + return + } + } + strongSelf.initializedStatus = true + strongSelf._status.set(MediaPlayerStatus(generationTimestamp: value.generationTimestamp, duration: value.duration, timestamp: value.timestamp, seekId: strongSelf.seekId, status: value.status)) + } }) - - //self._status.set(self.player.status) } deinit { @@ -162,23 +201,28 @@ private final class WebEmbedVideoContentNode: ASDisplayNode, UniversalVideoConte self.loadProgressDisposable?.dispose() self.thumbnailDisposable?.dispose() + self.statusDisposable?.dispose() } func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { - self.playerView.center = CGPoint(x: size.width / 2.0, y: size.height / 2.0) - self.playerView.transform = CGAffineTransform(scaleX: size.width / self.intrinsicDimensions.width, y: size.height / self.intrinsicDimensions.height) - - //self.imageNode.frame = CGRect(origin: CGPoint(), size: size) - //self.playerNode.frame = CGRect(origin: CGPoint(), size: size) + transition.updatePosition(layer: self.playerViewContainer.layer, position: CGPoint(x: size.width / 2.0, y: size.height / 2.0)) + transition.updateTransformScale(layer: self.playerViewContainer.layer, scale: size.width / self.intrinsicDimensions.width) } func play() { assert(Queue.mainQueue().isCurrent()) - self.playerView.playVideo() + if !self.initializedStatus { + self._status.set(MediaPlayerStatus(generationTimestamp: 0.0, duration: Double(self.approximateDuration), timestamp: 0.0, seekId: self.seekId, status: .buffering(initial: true, whilePlaying: true))) + } else { + self.playerView.playVideo() + } } func pause() { assert(Queue.mainQueue().isCurrent()) + if !self.initializedStatus { + self._status.set(MediaPlayerStatus(generationTimestamp: 0.0, duration: Double(self.approximateDuration), timestamp: 0.0, seekId: self.seekId, status: .paused)) + } self.playerView.pauseVideo() } @@ -202,9 +246,19 @@ private final class WebEmbedVideoContentNode: ASDisplayNode, UniversalVideoConte func seek(_ timestamp: Double) { assert(Queue.mainQueue().isCurrent()) + self.seekId += 1 self.playerView.seek(toPosition: timestamp) } + func playOnceWithSound(playAndRecord: Bool) { + } + + func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) { + } + + func continuePlayingWithoutSound() { + } + func addPlaybackCompleted(_ f: @escaping () -> Void) -> Int { return self.playbackCompletedListeners.add(f) } @@ -212,4 +266,7 @@ private final class WebEmbedVideoContentNode: ASDisplayNode, UniversalVideoConte func removePlaybackCompleted(_ index: Int) { self.playbackCompletedListeners.remove(index) } + + func fetchControl(_ control: UniversalVideoNodeFetchControl) { + } } diff --git a/TelegramUI/WebpagePreviewAccessoryPanelNode.swift b/TelegramUI/WebpagePreviewAccessoryPanelNode.swift index 315eddf3ea..88dea782b3 100644 --- a/TelegramUI/WebpagePreviewAccessoryPanelNode.swift +++ b/TelegramUI/WebpagePreviewAccessoryPanelNode.swift @@ -9,16 +9,21 @@ final class WebpagePreviewAccessoryPanelNode: AccessoryPanelNode { private let webpageDisposable = MetaDisposable() private(set) var webpage: TelegramMediaWebpage + private(set) var url: String let closeButton: ASButtonNode let lineNode: ASImageNode - let titleNode: ASTextNode - let textNode: ASTextNode + let titleNode: TextNode + private var titleString: NSAttributedString? + + let textNode: TextNode + private var textString: NSAttributedString? var theme: PresentationTheme var strings: PresentationStrings - init(account: Account, webpage: TelegramMediaWebpage, theme: PresentationTheme, strings: PresentationStrings) { + init(account: Account, url: String, webpage: TelegramMediaWebpage, theme: PresentationTheme, strings: PresentationStrings) { + self.url = url self.webpage = webpage self.theme = theme self.strings = strings @@ -33,14 +38,10 @@ final class WebpagePreviewAccessoryPanelNode: AccessoryPanelNode { self.lineNode.displaysAsynchronously = false self.lineNode.image = PresentationResourcesChat.chatInputPanelVerticalSeparatorLineImage(theme) - self.titleNode = ASTextNode() - self.titleNode.truncationMode = .byTruncatingTail - self.titleNode.maximumNumberOfLines = 1 + self.titleNode = TextNode() self.titleNode.displaysAsynchronously = false - self.textNode = ASTextNode() - self.textNode.truncationMode = .byTruncatingTail - self.textNode.maximumNumberOfLines = 1 + self.textNode = TextNode() self.textNode.displaysAsynchronously = false super.init() @@ -70,12 +71,12 @@ final class WebpagePreviewAccessoryPanelNode: AccessoryPanelNode { self.lineNode.image = PresentationResourcesChat.chatInputPanelVerticalSeparatorLineImage(theme) } - if let text = self.titleNode.attributedText?.string { - self.titleNode.attributedText = NSAttributedString(string: text, font: Font.medium(15.0), textColor: self.theme.chat.inputPanel.panelControlAccentColor) + if let text = self.titleString?.string { + titleString = NSAttributedString(string: text, font: Font.medium(15.0), textColor: self.theme.chat.inputPanel.panelControlAccentColor) } - if let text = self.textNode.attributedText?.string { - self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(15.0), textColor: self.theme.chat.inputPanel.primaryTextColor) + if let text = self.textString?.string { + textString = NSAttributedString(string: text, font: Font.regular(15.0), textColor: self.theme.chat.inputPanel.primaryTextColor) } self.updateWebpage() @@ -84,8 +85,9 @@ final class WebpagePreviewAccessoryPanelNode: AccessoryPanelNode { } } - func replaceWebpage(_ webpage: TelegramMediaWebpage) { - if !self.webpage.isEqual(webpage) { + func replaceWebpage(url: String, webpage: TelegramMediaWebpage) { + if self.url != url || !self.webpage.isEqual(webpage) { + self.url = url self.webpage = webpage self.updateWebpage() } @@ -97,6 +99,7 @@ final class WebpagePreviewAccessoryPanelNode: AccessoryPanelNode { switch self.webpage.content { case .Pending: authorName = self.strings.Channel_NotificationLoading + text = self.url case let .Loaded(content): if let title = content.title { authorName = title @@ -108,8 +111,8 @@ final class WebpagePreviewAccessoryPanelNode: AccessoryPanelNode { text = content.text ?? "" } - self.titleNode.attributedText = NSAttributedString(string: authorName, font: Font.medium(15.0), textColor: self.theme.chat.inputPanel.panelControlAccentColor) - self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(15.0), textColor: self.theme.chat.inputPanel.primaryTextColor) + self.titleString = NSAttributedString(string: authorName, font: Font.medium(15.0), textColor: self.theme.chat.inputPanel.panelControlAccentColor) + self.textString = NSAttributedString(string: text, font: Font.regular(15.0), textColor: self.theme.chat.inputPanel.primaryTextColor) self.setNeedsLayout() } @@ -132,11 +135,19 @@ final class WebpagePreviewAccessoryPanelNode: AccessoryPanelNode { self.lineNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 8.0), size: CGSize(width: 2.0, height: bounds.size.height - 10.0)) - let titleSize = self.titleNode.measure(CGSize(width: bounds.size.width - leftInset - textLineInset - rightInset - textRightInset, height: bounds.size.height)) - self.titleNode.frame = CGRect(origin: CGPoint(x: leftInset + textLineInset, y: 7.0), size: titleSize) + let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let makeTextLayout = TextNode.asyncLayout(self.textNode) - let textSize = self.textNode.measure(CGSize(width: bounds.size.width - leftInset - textLineInset - rightInset - textRightInset, height: bounds.size.height)) - self.textNode.frame = CGRect(origin: CGPoint(x: leftInset + textLineInset, y: 25.0), size: textSize) + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: self.titleString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: bounds.size.width - leftInset - textLineInset - rightInset - textRightInset, height: bounds.size.height), alignment: .natural, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) + + let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: self.textString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: bounds.size.width - leftInset - textLineInset - rightInset - textRightInset, height: bounds.size.height), alignment: .natural, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) + + self.titleNode.frame = CGRect(origin: CGPoint(x: leftInset + textLineInset, y: 7.0), size: titleLayout.size) + + self.textNode.frame = CGRect(origin: CGPoint(x: leftInset + textLineInset, y: 25.0), size: textLayout.size) + + let _ = titleApply() + let _ = textApply() } @objc func closePressed() { diff --git a/TelegramUI/ZoomableContentGalleryItemNode.swift b/TelegramUI/ZoomableContentGalleryItemNode.swift index c0dbb39b6b..cc2b7b0c47 100644 --- a/TelegramUI/ZoomableContentGalleryItemNode.swift +++ b/TelegramUI/ZoomableContentGalleryItemNode.swift @@ -7,6 +7,9 @@ class ZoomableContentGalleryItemNode: GalleryItemNode, UIScrollViewDelegate { private var containerLayout: ContainerViewLayout? + private var ignoreZoom = false + private var ignoreZoomTransition: ContainedViewLayoutTransition? + var zoomableContent: (CGSize, ASDisplayNode)? { didSet { if oldValue?.1 !== self.zoomableContent?.1 { @@ -17,7 +20,7 @@ class ZoomableContentGalleryItemNode: GalleryItemNode, UIScrollViewDelegate { if let node = self.zoomableContent?.1 { self.scrollNode.addSubnode(node) } - self.resetScrollViewContents() + self.resetScrollViewContents(transition: .immediate) } } @@ -89,16 +92,39 @@ class ZoomableContentGalleryItemNode: GalleryItemNode, UIScrollViewDelegate { self.containerLayout = layout if shouldResetContents { - self.scrollNode.frame = CGRect(origin: CGPoint(), size: layout.size) - self.resetScrollViewContents() + var previousFrame: CGRect? + var previousScale: CGFloat? + if let (_, contentNode) = self.zoomableContent { + previousFrame = contentNode.view.frame + let t = contentNode.layer.transform + previousScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13)) + } + + transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + self.resetScrollViewContents(transition: .immediate) + + if let (_, contentNode) = self.zoomableContent, let previousFrame = previousFrame, let previousScale = previousScale { + transition.animatePosition(node: contentNode, from: CGPoint(x: previousFrame.midX, y: previousFrame.midY)) + switch transition { + case .immediate: + break + case let .animated(duration, curve): + let t = contentNode.layer.transform + let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13)) + + contentNode.layer.animateScale(from: previousScale, to: currentScale, duration: duration, timingFunction: curve.timingFunction) + } + } } } - private func resetScrollViewContents() { + private func resetScrollViewContents(transition: ContainedViewLayoutTransition) { guard let (contentSize, contentNode) = self.zoomableContent else { return } + self.ignoreZoom = true + self.ignoreZoomTransition = transition self.scrollNode.view.minimumZoomScale = 1.0 self.scrollNode.view.maximumZoomScale = 1.0 //self.scrollView.normalZoomScale = 1.0 @@ -108,12 +134,14 @@ class ZoomableContentGalleryItemNode: GalleryItemNode, UIScrollViewDelegate { contentNode.transform = CATransform3DIdentity contentNode.frame = CGRect(origin: CGPoint(), size: contentSize) - self.centerScrollViewContents() + self.centerScrollViewContents(transition: transition) + self.ignoreZoom = false self.scrollNode.view.zoomScale = self.scrollNode.view.minimumZoomScale + self.ignoreZoomTransition = nil } - private func centerScrollViewContents() { + private func centerScrollViewContents(transition: ContainedViewLayoutTransition) { guard let (contentSize, contentNode) = self.zoomableContent else { return } @@ -159,7 +187,9 @@ class ZoomableContentGalleryItemNode: GalleryItemNode, UIScrollViewDelegate { contentFrame.origin.y = 0.0 } - contentNode.view.frame = contentFrame + if !self.ignoreZoom { + transition.updateFrame(view: contentNode.view, frame: contentFrame) + } //self.scrollView.scrollEnabled = ABS(_scrollView.zoomScale - _scrollView.normalZoomScale) > FLT_EPSILON; } @@ -169,6 +199,8 @@ class ZoomableContentGalleryItemNode: GalleryItemNode, UIScrollViewDelegate { } func scrollViewDidZoom(_ scrollView: UIScrollView) { - self.centerScrollViewContents() + if !self.ignoreZoom { + self.centerScrollViewContents(transition: self.ignoreZoomTransition ?? .immediate) + } } } diff --git a/submodules/libtgvoip b/submodules/libtgvoip index 45f8df3c96..dd43702ee0 160000 --- a/submodules/libtgvoip +++ b/submodules/libtgvoip @@ -1 +1 @@ -Subproject commit 45f8df3c9635ee48d6595ddb126cd41bbaba28be +Subproject commit dd43702ee02336fe7f5d26228dd5caa90be131e2 diff --git a/third-party/RMIntro/core/animations.c b/third-party/RMIntro/core/animations.c index fcfb56e8c0..a4a7755e4d 100644 --- a/third-party/RMIntro/core/animations.c +++ b/third-party/RMIntro/core/animations.c @@ -1497,8 +1497,13 @@ void draw_safe(int type, float alpha, float screw_alpha) } +static float backgroundColor[3] = {0.0, 0.0, 0.0}; - +void set_intro_background_color(float r, float g, float b) { + backgroundColor[0] = r; + backgroundColor[1] = g; + backgroundColor[2] = b; +} void on_draw_frame() { @@ -1582,15 +1587,9 @@ void on_draw_frame() { glEnable(GL_BLEND); - glClearColor(1, 1, 1, 1); - glClear(GL_COLOR_BUFFER_BIT); - - float private_back_k = .8; - - //glClearColor(0.5, 0.5, 0.5, 1); - glClearColor(1, 1, 1, 1); + glClearColor(backgroundColor[0], backgroundColor[0], backgroundColor[0], 1); glClear(GL_COLOR_BUFFER_BIT); /* @@ -2160,7 +2159,7 @@ void on_draw_frame() { else if (current_page == 2) { - rglNormalDraw(); + rglNormalDrawThroughMask(); float dribbon=87; @@ -2233,8 +2232,8 @@ void on_draw_frame() { float scale = t(1, 2, 0, duration_const, EaseIn); powerful_mask.params.scale = xyzMake(scale, scale, 1); - draw_textured_shape(&powerful_mask, main_matrix, NORMAL_ONE); - + + draw_textured_shape(&powerful_mask, main_matrix, backgroundColor[1] < 0.5 ? DARK : LIGHT); ribbonLayer.rotation = free_scroll_offset + t_reversed(360, 360+(45+30), 0, duration_const, EaseOut); ribbonLayer.position.y = t_reversed(0, -8, 0, duration_const*.8, EaseOut); @@ -2350,7 +2349,7 @@ void on_draw_frame() { float scale = t(2, 1, 0, duration_const, EaseOut); powerful_mask.params.scale = xyzMake(scale, scale, 1); - draw_textured_shape(&powerful_mask, main_matrix, NORMAL_ONE); + draw_textured_shape(&powerful_mask, main_matrix, backgroundColor[1] < 0.5 ? DARK : LIGHT); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); @@ -2439,7 +2438,7 @@ void on_draw_frame() { float scale = t(2, 1, 0, duration_const, EaseOut); powerful_mask.params.scale = xyzMake(scale, scale, 1); - draw_textured_shape(&powerful_mask, main_matrix, NORMAL_ONE); + draw_textured_shape(&powerful_mask, main_matrix, backgroundColor[1] < 0.5 ? DARK : LIGHT); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); @@ -2584,7 +2583,7 @@ void on_draw_frame() { if (time < duration_const*.4) { cloud_cover.params.position.y=t_reversed(118/2+50, 118/2, duration_const*.8*private_back_k, duration_const*private_back_k, EaseOut); - draw_shape(&cloud_cover, main_matrix); + draw_colored_shape(&cloud_cover, main_matrix, backgroundColor[1] < 0.5 ? black_color : white_color); } draw_safe(0, t(0,1,duration_const*private_back_k*.0, duration_const*private_back_k, Linear), t(0, 1, 0, duration_const, Linear)); @@ -2624,7 +2623,7 @@ void on_draw_frame() { cloud_cover.params.position.y = t(118/2+50, 118/2, 0, duration_const, EaseOut); - draw_shape(&cloud_cover, main_matrix); + draw_colored_shape(&cloud_cover, main_matrix, backgroundColor[1] < 0.5 ? black_color : white_color); } diff --git a/third-party/RMIntro/core/animations.h b/third-party/RMIntro/core/animations.h index b392313d6f..36ae37b756 100755 --- a/third-party/RMIntro/core/animations.h +++ b/third-party/RMIntro/core/animations.h @@ -42,4 +42,6 @@ void set_scroll_offset(float a_offset); void inc_stars_rendered(); -void set_elements_top_margins(int a_icon_y, int a_text_y, int a_button_y); \ No newline at end of file +void set_elements_top_margins(int a_icon_y, int a_text_y, int a_button_y); + +void set_intro_background_color(float r, float g, float b); diff --git a/third-party/RMIntro/core/objects.c b/third-party/RMIntro/core/objects.c index 0b69cb2e45..c7aa3b3c35 100644 --- a/third-party/RMIntro/core/objects.c +++ b/third-party/RMIntro/core/objects.c @@ -31,6 +31,7 @@ static TextureProgram texture_program_red; static TextureProgram texture_program_blue; static TextureProgram texture_program_light_red; static TextureProgram texture_program_light_blue; +static TextureProgram texture_program_black; static TextureProgram *texture_program_temp; @@ -260,6 +261,40 @@ void setup_shaders() "}"; texture_program_one = get_texture_program(build_program(vshader, (GLint)strlen(vshader), fshader, (GLint)strlen(fshader))); + + texture_program_one = get_texture_program(build_program(vshader, (GLint)strlen(vshader), fshader, (GLint)strlen(fshader))); + + + + + + + + + char* vshader_texture_black = + "uniform mat4 u_MvpMatrix;" + "attribute vec4 a_Position;" + "attribute vec2 a_TextureCoordinates;" + "varying vec2 v_TextureCoordinates;" + "void main(){" + " v_TextureCoordinates = a_TextureCoordinates;" + " gl_Position = u_MvpMatrix * a_Position;" + "}"; + + char* fshader_texture_black = + "precision lowp float;" + "uniform sampler2D u_TextureUnit;" + "varying vec2 v_TextureCoordinates;" + "uniform float u_Alpha;" + "void main(){" + " gl_FragColor = texture2D(u_TextureUnit, v_TextureCoordinates);" + //" float p = u_Alpha*gl_FragColor.w*0.4;" + //" gl_FragColor = vec4(0,0.353,0.761,p);" + " float p = u_Alpha*gl_FragColor.w;" + " gl_FragColor = vec4(0,0,0,p);" + "}"; + + texture_program_black = get_texture_program(build_program(vshader_texture_black, (GLint)strlen(vshader_texture_black), fshader_texture_black, (GLint)strlen(fshader_texture_black))); } @@ -454,6 +489,10 @@ void vec4_log(__unused vec4 M) void draw_shape(const Shape* shape, mat4x4 view_projection_matrix) { + draw_colored_shape(shape, view_projection_matrix, shape->color); +} + +void draw_colored_shape(const Shape* shape, mat4x4 view_projection_matrix, vec4 color) { if (shape->params.alpha>0 && (fabs(shape->params.scale.x)>0 && fabs(shape->params.scale.y)>0 && fabs(shape->params.scale.z)>0)) { @@ -466,7 +505,7 @@ void draw_shape(const Shape* shape, mat4x4 view_projection_matrix) glUniformMatrix4fv(color_program.u_mvp_matrix_location, 1, GL_FALSE, (GLfloat*)model_view_projection_matrix); if (shape->params.rotation==5.) { - glUniform4fv(color_program.u_color_location, 1, shape->color); + glUniform4fv(color_program.u_color_location, 1, color); } else if (shape->params.rotation==10.) { @@ -476,7 +515,7 @@ void draw_shape(const Shape* shape, mat4x4 view_projection_matrix) } else { - glUniform4fv(color_program.u_color_location, 1, shape->color); + glUniform4fv(color_program.u_color_location, 1, color); } glUniform1f(color_program.u_alpha_loaction, shape->params.alpha); @@ -536,6 +575,14 @@ void draw_textured_shape(const TexturedShape* shape, mat4x4 view_projection_matr { texture_program_temp=&texture_program_one; } + else if (program_type==DARK) + { + texture_program_temp=&texture_program_black; + } + else if (program_type==LIGHT) + { + texture_program_temp=&texture_program_one; + } else { texture_program_temp=&texture_program; @@ -1299,4 +1346,4 @@ void change_rounded_rectangle_stroked(Shape* shape, CSize size, float radius, __ glBufferSubData(GL_ARRAY_BUFFER, 0, shape->params.const_params.datasize, shape->data); // glBindBuffer(GL_ARRAY_BUFFER, 0); } -} \ No newline at end of file +} diff --git a/third-party/RMIntro/core/objects.h b/third-party/RMIntro/core/objects.h index e3acd6d563..8b0466d2cc 100755 --- a/third-party/RMIntro/core/objects.h +++ b/third-party/RMIntro/core/objects.h @@ -15,7 +15,7 @@ extern float scale_factor; extern int width, height; extern int y_offset_absolute; -typedef enum {NORMAL, NORMAL_ONE, RED, BLUE, LIGHT_RED, LIGHT_BLUE} texture_program_type; +typedef enum {NORMAL, NORMAL_ONE, RED, BLUE, LIGHT_RED, LIGHT_BLUE, DARK, LIGHT} texture_program_type; typedef struct { float x; @@ -127,6 +127,7 @@ void setup_shaders(); void draw_shape(const Shape* shape, mat4x4 view_projection_matrix); +void draw_colored_shape(const Shape* shape, mat4x4 view_projection_matrix, vec4 color); void draw_textured_shape(const TexturedShape* shape, mat4x4 view_projection_matrix, texture_program_type program_type); diff --git a/third-party/RMIntro/platform/ios/RMIntroPageView.h b/third-party/RMIntro/platform/ios/RMIntroPageView.h index 8e9421f118..a49c41c6e4 100644 --- a/third-party/RMIntro/platform/ios/RMIntroPageView.h +++ b/third-party/RMIntro/platform/ios/RMIntroPageView.h @@ -14,6 +14,6 @@ NSMutableAttributedString *_description; } -- (id)initWithFrame:(CGRect)frame headline:(NSString*)headline description:(NSString*)description; +- (id)initWithFrame:(CGRect)frame headline:(NSString*)headline description:(NSString*)description color:(UIColor *)color; @end diff --git a/third-party/RMIntro/platform/ios/RMIntroPageView.m b/third-party/RMIntro/platform/ios/RMIntroPageView.m index 4d46a140c7..986fccb013 100644 --- a/third-party/RMIntro/platform/ios/RMIntroPageView.m +++ b/third-party/RMIntro/platform/ios/RMIntroPageView.m @@ -12,7 +12,7 @@ #define IPAD ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) -- (id)initWithFrame:(CGRect)frame headline:(NSString*)headline description:(NSString*)description +- (id)initWithFrame:(CGRect)frame headline:(NSString*)headline description:(NSString*)description color:(UIColor *)color { self = [super initWithFrame:frame]; if (self) { @@ -26,6 +26,7 @@ UILabel *headlineLabel = [[UILabel alloc]initWithFrame:CGRectMake(0, 0, frame.size.width, 64+8)]; headlineLabel.font = [UIFont fontWithName:@"HelveticaNeue-Light" size:IPAD ? 96/2 : 36]; headlineLabel.text = _headline; + headlineLabel.textColor = color; headlineLabel.textAlignment = NSTextAlignmentCenter; headlineLabel.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleWidth; @@ -72,6 +73,7 @@ [_description addAttribute:NSParagraphStyleAttributeName value:style range:NSMakeRange(0, _description.length)]; + [_description addAttribute:NSForegroundColorAttributeName value:color range:NSMakeRange(0, _description.length)]; UILabel *descriptionLabel = [[UILabel alloc]initWithFrame:CGRectMake(0, 25 + (IPAD ? 22 : 0), frame.size.width, 120+8+5)]; descriptionLabel.font = [UIFont fontWithName:@"HelveticaNeue" size:IPAD ? 44/2 : 17]; diff --git a/third-party/RMIntro/platform/ios/RMIntroViewController.h b/third-party/RMIntro/platform/ios/RMIntroViewController.h index 386b6f3795..1e1378d2d6 100644 --- a/third-party/RMIntro/platform/ios/RMIntroViewController.h +++ b/third-party/RMIntro/platform/ios/RMIntroViewController.h @@ -25,8 +25,6 @@ typedef enum { GLKView *_glkView; - UIImageView *_startArrow; - NSArray *_headlines; NSArray *_descriptions; @@ -35,7 +33,6 @@ typedef enum { NSInteger _currentPage; UIScrollView *_pageScrollView; - UIButton *_startButton; UIPageControl *_pageControl; NSTimer *_updateAndRenderTimer; @@ -43,6 +40,8 @@ typedef enum { BOOL _isOpenGLLoaded; } +- (instancetype)initWithBackroundColor:(UIColor *)backgroundColor primaryColor:(UIColor *)primaryColor accentColor:(UIColor *)accentColor regularDotColor:(UIColor *)regularDotColor highlightedDotColor:(UIColor *)highlightedDotColor; + @property (nonatomic, copy) void (^startMessaging)(); - (void)startTimer; diff --git a/third-party/RMIntro/platform/ios/RMIntroViewController.m b/third-party/RMIntro/platform/ios/RMIntroViewController.m index 592c24747b..47e10c4d03 100644 --- a/third-party/RMIntro/platform/ios/RMIntroViewController.m +++ b/third-party/RMIntro/platform/ios/RMIntroViewController.m @@ -15,18 +15,13 @@ #include "objects.h" #include "texture_helper.h" -static UIColor *TGColorWithHex(int hex) -{ - return [[UIColor alloc] initWithRed:(((hex >> 16) & 0xff) / 255.0f) green:(((hex >> 8) & 0xff) / 255.0f) blue:(((hex) & 0xff) / 255.0f) alpha:1.0f]; -} +#import + +#import #define TGLog NSLog #define TGLocalized(x) NSLocalizedString(x, @"") -static UIColor *TGAccentColor() { - return TGColorWithHex(0x007ee5); -} - static void TGDispatchOnMainThread(dispatch_block_t block) { if ([NSThread isMainThread]) { block(); @@ -77,17 +72,37 @@ static void TGDispatchOnMainThread(dispatch_block_t block) { UIImageView *_stillLogoView; bool _displayedStillLogo; + + UIColor *_backgroundColor; + UIColor *_primaryColor; + UIColor *_accentColor; + UIColor *_regularDotColor; + UIColor *_highlightedDotColor; + + UIButton *_startButton; + TGModernButton *_alternativeLanguageButton; + + SMetaDisposable *_localizationsDisposable; + NSDictionary *_alternativeLocalizationInfo; + + SVariable *_alternativeLocalization; } @end @implementation RMIntroViewController -- (instancetype)init +- (instancetype)initWithBackroundColor:(UIColor *)backgroundColor primaryColor:(UIColor *)primaryColor accentColor:(UIColor *)accentColor regularDotColor:(UIColor *)regularDotColor highlightedDotColor:(UIColor *)highlightedDotColor { self = [super init]; if (self != nil) { + _backgroundColor = backgroundColor; + _primaryColor = primaryColor; + _accentColor = accentColor; + _regularDotColor = regularDotColor; + _highlightedDotColor = highlightedDotColor; + self.automaticallyAdjustsScrollViewInsets = false; _headlines = @[ TGLocalized(@"Tour.Title1"), TGLocalized(@"Tour.Title2"), TGLocalized(@"Tour.Title6"), TGLocalized(@"Tour.Title3"), TGLocalized(@"Tour.Title4"), TGLocalized(@"Tour.Title5")]; @@ -106,6 +121,38 @@ static void TGDispatchOnMainThread(dispatch_block_t block) { [strongSelf loadGL]; [strongSelf startTimer]; }]; + + _alternativeLanguageButton = [[TGModernButton alloc] init]; + _alternativeLanguageButton.modernHighlight = true; + [_alternativeLanguageButton setTitleColor:accentColor]; + + _alternativeLanguageButton.titleLabel.font = [UIFont systemFontOfSize:18.0]; + _alternativeLanguageButton.hidden = true; + [_alternativeLanguageButton addTarget:self action:@selector(alternativeLanguageButtonPressed) forControlEvents:UIControlEventTouchUpInside]; + + _alternativeLocalization = [[SVariable alloc] init]; + + /*SSignal *localizationSignal = [TGLocalizationSignals suggestedLocalization]; + + _localizationsDisposable = [[localizationSignal deliverOn:[SQueue mainQueue]] startWithNext:^(TGSuggestedLocalization *next) { + __strong RMIntroViewController *strongSelf = weakSelf; + if (strongSelf != nil && next != nil) { + if (strongSelf->_alternativeLocalizationInfo == nil) { + _alternativeLocalizationInfo = next; + + [strongSelf->_alternativeLanguageButton setTitle:next.continueWithLanguageString forState:UIControlStateNormal]; + strongSelf->_alternativeLanguageButton.hidden = false; + [strongSelf->_alternativeLanguageButton sizeToFit]; + + if ([strongSelf isViewLoaded]) { + [strongSelf->_alternativeLanguageButton.layer animateAlphaFrom:0.0f to:1.0f duration:0.3f timingFunction:kCAMediaTimingFunctionEaseInEaseOut removeOnCompletion:true completion:nil]; + [UIView animateWithDuration:0.3 animations:^{ + [strongSelf viewWillLayoutSubviews]; + }]; + } + } + } + }];*/ } return self; } @@ -153,6 +200,7 @@ static void TGDispatchOnMainThread(dispatch_block_t block) { height += 138 / 2; _glkView = [[GLKView alloc] initWithFrame:CGRectMake(self.view.bounds.size.width / 2 - size / 2, height, size, size) context:context]; + _glkView.backgroundColor = _backgroundColor; _glkView.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin; _glkView.drawableDepthFormat = GLKViewDrawableDepthFormat24; _glkView.drawableMultisample = GLKViewDrawableMultisample4X; @@ -166,7 +214,7 @@ static void TGDispatchOnMainThread(dispatch_block_t block) { UIView *v3 = [[UIView alloc] initWithFrame:CGRectMake(-patchHalfWidth, -patchHalfWidth + _glkView.frame.size.height, _glkView.frame.size.width + patchHalfWidth * 2, patchHalfWidth * 2)]; UIView *v4 = [[UIView alloc] initWithFrame:CGRectMake(-patchHalfWidth + _glkView.frame.size.width, -patchHalfWidth, patchHalfWidth * 2, _glkView.frame.size.height + patchHalfWidth * 2)]; - v1.backgroundColor = v2.backgroundColor = v3.backgroundColor = v4.backgroundColor = [UIColor whiteColor]; + v1.backgroundColor = v2.backgroundColor = v3.backgroundColor = v4.backgroundColor = _backgroundColor; [_glkView addSubview:v1]; [_glkView addSubview:v2]; @@ -202,7 +250,7 @@ static void TGDispatchOnMainThread(dispatch_block_t block) { { [super viewDidLoad]; - self.view.backgroundColor = [UIColor whiteColor]; + self.view.backgroundColor = _backgroundColor; [self loadGL]; @@ -223,7 +271,7 @@ static void TGDispatchOnMainThread(dispatch_block_t block) { for (NSUInteger i = 0; i < _headlines.count; i++) { - RMIntroPageView *p = [[RMIntroPageView alloc]initWithFrame:CGRectMake(i * self.view.bounds.size.width, 0, self.view.bounds.size.width, 0) headline:[_headlines objectAtIndex:i] description:[_descriptions objectAtIndex:i]]; + RMIntroPageView *p = [[RMIntroPageView alloc]initWithFrame:CGRectMake(i * self.view.bounds.size.width, 0, self.view.bounds.size.width, 0) headline:[_headlines objectAtIndex:i] description:[_descriptions objectAtIndex:i] color:_primaryColor]; p.opaque = true; p.clearsContextBeforeDrawing = false; [_pageViews addObject:p]; @@ -232,22 +280,47 @@ static void TGDispatchOnMainThread(dispatch_block_t block) { [_pageScrollView setPage:0]; _startButton = [[UIButton alloc] init]; + _startButton.adjustsImageWhenDisabled = false; [_startButton setTitle:TGLocalized(@"Tour.StartButton") forState:UIControlStateNormal]; - [_startButton.titleLabel setFont:[UIFont fontWithName:@"HelveticaNeue" size:isIpad ? 55 / 2.0f : 21]]; - [_startButton setTitleColor:TGAccentColor() forState:UIControlStateNormal]; - - _startArrow = [[UIImageView alloc]initWithImage:[UIImage imageNamed:isIpad ? @"start_arrow_ipad.png" : @"start_arrow.png"]]; - _startButton.titleLabel.clipsToBounds = false; - _startArrow.frame = CGRectChangedOrigin(_startArrow.frame, CGPointMake([_startButton.titleLabel.text sizeWithFont:_startButton.titleLabel.font].width + (isIpad ? 7 : 6), isIpad ? 6.5f : 4.5f)); - [_startButton.titleLabel addSubview:_startArrow]; + [_startButton.titleLabel setFont:TGMediumSystemFontOfSize(20.0f)]; + [_startButton setTitleColor:_backgroundColor forState:UIControlStateNormal]; + static UIImage *buttonBackgroundImage = nil; + static UIImage *buttonHighlightedBackgroundImage = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + { + UIGraphicsBeginImageContextWithOptions(CGSizeMake(48.0, 48.0), false, 0.0f); + CGContextRef context = UIGraphicsGetCurrentContext(); + CGContextSetFillColorWithColor(context, [_accentColor CGColor]); + CGContextFillEllipseInRect(context, CGRectMake(0.0f, 0.0f, 48.0f, 48.0f)); + buttonBackgroundImage = [UIGraphicsGetImageFromCurrentImageContext() stretchableImageWithLeftCapWidth:24 topCapHeight:24]; + UIGraphicsEndImageContext(); + } + { + UIGraphicsBeginImageContextWithOptions(CGSizeMake(48.0, 48.0), false, 0.0f); + CGContextRef context = UIGraphicsGetCurrentContext(); + CGFloat hue = 0.0f; + CGFloat sat = 0.0f; + CGFloat bri = 0.0f; + [_accentColor getHue:&hue saturation:&sat brightness:&bri alpha:nil]; + UIColor *color = [[UIColor alloc] initWithHue:hue saturation:sat brightness:bri * 0.7 alpha:1.0]; + CGContextSetFillColorWithColor(context, [color CGColor]); + CGContextFillEllipseInRect(context, CGRectMake(0.0f, 0.0f, 48.0f, 48.0f)); + buttonHighlightedBackgroundImage = [UIGraphicsGetImageFromCurrentImageContext() stretchableImageWithLeftCapWidth:24 topCapHeight:24]; + UIGraphicsEndImageContext(); + } + }); + [_startButton setContentEdgeInsets:UIEdgeInsetsMake(0.0f, 20.0f, 0.0f, 20.0f)]; + [_startButton setBackgroundImage:buttonBackgroundImage forState:UIControlStateNormal]; + [_startButton setBackgroundImage:buttonHighlightedBackgroundImage forState:UIControlStateHighlighted]; [self.view addSubview:_startButton]; _pageControl = [[UIPageControl alloc] init]; _pageControl.autoresizingMask = UIViewAutoresizingFlexibleBottomMargin; _pageControl.userInteractionEnabled = false; - [_pageControl setPageIndicatorTintColor:[UIColor colorWithWhite:.85 alpha:1]]; - [_pageControl setCurrentPageIndicatorTintColor:[UIColor colorWithWhite:.2 alpha:1]]; [_pageControl setNumberOfPages:6]; + _pageControl.pageIndicatorTintColor = _regularDotColor; + _pageControl.currentPageIndicatorTintColor = _highlightedDotColor; [self.view addSubview:_pageControl]; } @@ -313,111 +386,106 @@ static void TGDispatchOnMainThread(dispatch_block_t block) { { UIInterfaceOrientation isVertical = (self.view.bounds.size.height / self.view.bounds.size.width > 1.0f); - CGFloat statusBarHeight = 0.0f; + CGFloat statusBarHeight = 0; CGFloat pageControlY = 0; CGFloat glViewY = 0; CGFloat startButtonY = 0; CGFloat pageY = 0; + CGFloat languageButtonSpread = 60.0f; + CGFloat languageButtonOffset = 26.0f; + DeviceScreen deviceScreen = [self deviceScreen]; - bool landscape = self.view.bounds.size.width > self.view.bounds.size.height; switch (deviceScreen) { case iPad: - pageControlY = 386 / 2; glViewY = isVertical ? 121 + 90 : 121; startButtonY = 120; pageY = isVertical ? 485 : 335; + pageControlY = pageY + 200.0f; break; - + case iPadPro: - pageControlY = 386 / 2; glViewY = isVertical ? 221 + 110 : 221; startButtonY = 120; pageY = isVertical ? 605 : 435; + pageControlY = pageY + 200.0f; break; case Inch35: - if (landscape) { - pageControlY = 80; - glViewY = -200; - startButtonY = 75; - pageY = 38; - } else { - pageControlY = 162 / 2; - glViewY = 62 - 20; - startButtonY = 75; - pageY = 215; + pageControlY = 162 / 2; + glViewY = 62 - 20; + startButtonY = 75; + pageY = 215; + pageControlY = pageY + 160.0f; + if (!_alternativeLanguageButton.isHidden) { + glViewY -= 40.0f; + pageY -= 40.0f; + pageControlY -= 40.0f; + startButtonY -= 30.0f; } + languageButtonSpread = 65.0f; + languageButtonOffset = 15.0f; break; case Inch4: - if (landscape) { - pageControlY = 80; - glViewY = -200; - startButtonY = 75; - pageY = 38; - } else { - pageControlY = 162 / 2; - glViewY = 62; - startButtonY = 75; - pageY = 245; - } + glViewY = 62; + startButtonY = 75; + pageY = 245; + pageControlY = pageY + 160.0f; + languageButtonSpread = 50.0f; + languageButtonOffset = 20.0f; break; - + case Inch47: - if (landscape) { - pageControlY = 162 / 2 + 10; - glViewY = -200; - startButtonY = 75 + 5; - pageY = 70; - } else { - pageControlY = 162 / 2 + 10; - glViewY = 62 + 25; - startButtonY = 75 + 5; - pageY = 245 + 50; - } + pageControlY = 162 / 2 + 10; + glViewY = 62 + 25; + startButtonY = 75 + 5; + pageY = 245 + 50; + pageControlY = pageY + 160.0f; break; - + case Inch55: - if (landscape) { - pageControlY = 162 / 2 + 20; - glViewY = -200; - startButtonY = 75 + 20; - pageY = 76; - } else { - pageControlY = 162 / 2 + 20; - glViewY = 62 + 45; - startButtonY = 75 + 20; - pageY = 245 + 85; - } + glViewY = 62 + 45; + startButtonY = 75 + 20; + pageY = 245 + 85; + pageControlY = pageY + 160.0f; break; default: break; } - _pageControl.frame = CGRectMake(0, self.view.bounds.size.height - pageControlY - statusBarHeight, self.view.bounds.size.width, 7); + if (!_alternativeLanguageButton.isHidden) { + startButtonY += languageButtonSpread; + } + + _pageControl.frame = CGRectMake(0, pageControlY, self.view.bounds.size.width, 7); _glkView.frame = CGRectChangedOriginY(_glkView.frame, glViewY - statusBarHeight); - _startButton.frame = CGRectMake(-9, self.view.bounds.size.height - startButtonY - statusBarHeight, self.view.bounds.size.width, startButtonY - 4); + [_startButton sizeToFit]; + _startButton.frame = CGRectMake(floor((self.view.bounds.size.width - _startButton.frame.size.width) / 2.0f), self.view.bounds.size.height - startButtonY - statusBarHeight, _startButton.frame.size.width, 48.0f); [_startButton addTarget:self action:@selector(startButtonPress) forControlEvents:UIControlEventTouchUpInside]; + _alternativeLanguageButton.frame = CGRectMake(floor((self.view.bounds.size.width - _alternativeLanguageButton.frame.size.width) / 2.0f), CGRectGetMaxY(_startButton.frame) + languageButtonOffset, _alternativeLanguageButton.frame.size.width, _alternativeLanguageButton.frame.size.height); + _pageScrollView.frame=CGRectMake(0, 20, self.view.bounds.size.width, self.view.bounds.size.height - 20); _pageScrollView.contentSize=CGSizeMake(_headlines.count * self.view.bounds.size.width, 150); _pageScrollView.contentOffset = CGPointMake(_currentPage * self.view.bounds.size.width, 0); [_pageViews enumerateObjectsUsingBlock:^(UIView *pageView, NSUInteger index, __unused BOOL *stop) - { - pageView.frame = CGRectMake(index * self.view.bounds.size.width, (pageY - statusBarHeight), self.view.bounds.size.width, 150); - }]; + { + pageView.frame = CGRectMake(index * self.view.bounds.size.width, (pageY - statusBarHeight), self.view.bounds.size.width, 150); + }]; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; + [self loadGL]; + if (_stillLogoView == nil && !_displayedStillLogo) { _displayedStillLogo = true; @@ -465,8 +533,6 @@ static void TGDispatchOnMainThread(dispatch_block_t block) { _stillLogoView.frame = CGRectChangedOriginY(_glkView.frame, glViewY - statusBarHeight); [self.view addSubview:_stillLogoView]; } - - [self loadGL]; } - (void)viewDidAppear:(BOOL)animated @@ -485,6 +551,10 @@ static void TGDispatchOnMainThread(dispatch_block_t block) { [super viewDidDisappear:animated]; [self freeGL]; + + [_stillLogoView removeFromSuperview]; + _stillLogoView = nil; + _displayedStillLogo = false; } - (void)startButtonPress @@ -614,4 +684,8 @@ NSInteger _current_page_end; [_pageControl setCurrentPage:_currentPage]; } +- (void)alternativeLanguageButtonPressed { + +} + @end