no message
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
[submodule "submodules/libtgvoip"]
|
||||
path = submodules/libtgvoip
|
||||
url = https://bitbucket.org/grishka/libtgvoip.git
|
||||
9
Images.xcassets/Call List/Contents.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
},
|
||||
"properties" : {
|
||||
"provides-namespace" : true
|
||||
}
|
||||
}
|
||||
BIN
Images.xcassets/Call List/InfoButton.imageset/CallInfoIcon@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 598 B |
BIN
Images.xcassets/Call List/InfoButton.imageset/CallInfoIcon@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 932 B |
22
Images.xcassets/Call List/InfoButton.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "CallInfoIcon@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "CallInfoIcon@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
Images.xcassets/Call List/OutgoingIcon.imageset/CallOutgoing@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
Images.xcassets/Call List/OutgoingIcon.imageset/CallOutgoing@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 556 B |
22
Images.xcassets/Call List/OutgoingIcon.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "CallOutgoing@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "CallOutgoing@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
Images.xcassets/Call/CallBluetoothButton.imageset/CallBluetoothIcon@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 618 B |
BIN
Images.xcassets/Call/CallBluetoothButton.imageset/CallBluetoothIcon@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 1022 B |
22
Images.xcassets/Call/CallBluetoothButton.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "CallBluetoothIcon@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "CallBluetoothIcon@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
Images.xcassets/Call/CallCancelButton.imageset/CallCancelIcon@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 503 B |
BIN
Images.xcassets/Call/CallCancelButton.imageset/CallCancelIcon@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 782 B |
22
Images.xcassets/Call/CallCancelButton.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "CallCancelIcon@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "CallCancelIcon@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
Images.xcassets/Call/CallKitLogo.imageset/CallKitLogo@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
Images.xcassets/Call/CallKitLogo.imageset/CallKitLogo@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
22
Images.xcassets/Call/CallKitLogo.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "CallKitLogo@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "CallKitLogo@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
Images.xcassets/Call/CallMessageButton.imageset/CallQuickMessageIcon@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 535 B |
BIN
Images.xcassets/Call/CallMessageButton.imageset/CallQuickMessageIcon@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 805 B |
22
Images.xcassets/Call/CallMessageButton.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "CallQuickMessageIcon@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "CallQuickMessageIcon@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
Images.xcassets/Call/CallMuteButton.imageset/CallMuteIcon@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
Images.xcassets/Call/CallMuteButton.imageset/CallMuteIcon@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
22
Images.xcassets/Call/CallMuteButton.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "CallMuteIcon@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "CallMuteIcon@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
Images.xcassets/Call/CallPhoneButton.imageset/CallPhoneIcon@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 545 B |
BIN
Images.xcassets/Call/CallPhoneButton.imageset/CallPhoneIcon@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 844 B |
22
Images.xcassets/Call/CallPhoneButton.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "CallPhoneIcon@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "CallPhoneIcon@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
Images.xcassets/Call/CallSpeakerButton.imageset/CallSpeakerIcon@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
Images.xcassets/Call/CallSpeakerButton.imageset/CallSpeakerIcon@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
22
Images.xcassets/Call/CallSpeakerButton.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "CallSpeakerIcon@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "CallSpeakerIcon@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
9
Images.xcassets/Call/Contents.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
},
|
||||
"properties" : {
|
||||
"provides-namespace" : true
|
||||
}
|
||||
}
|
||||
22
Images.xcassets/Chat List/Tabs/IconCalls.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "TabIconCalls@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "TabIconCalls@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
Images.xcassets/Chat List/Tabs/IconCalls.imageset/TabIconCalls@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1019 B |
BIN
Images.xcassets/Chat List/Tabs/IconCalls.imageset/TabIconCalls@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
22
Images.xcassets/Chat List/Tabs/IconCallsSelected.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "TabIconCalls_Highlighted@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "TabIconCalls_Highlighted@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
Images.xcassets/Chat List/Tabs/IconCallsSelected.imageset/TabIconCalls_Highlighted@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 747 B |
BIN
Images.xcassets/Chat List/Tabs/IconCallsSelected.imageset/TabIconCalls_Highlighted@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
22
Images.xcassets/Chat/Info/CallButton.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "TabIconCalls@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "TabIconCalls@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
Images.xcassets/Chat/Info/CallButton.imageset/TabIconCalls@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1019 B |
BIN
Images.xcassets/Chat/Info/CallButton.imageset/TabIconCalls@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
9
Images.xcassets/Chat/Info/Contents.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
},
|
||||
"properties" : {
|
||||
"provides-namespace" : true
|
||||
}
|
||||
}
|
||||
22
Images.xcassets/Chat/Input/Search/Calendar.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "ConversationSearchCalendar@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "ConversationSearchCalendar@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
Images.xcassets/Chat/Input/Search/Calendar.imageset/ConversationSearchCalendar@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 510 B |
BIN
Images.xcassets/Chat/Input/Search/Calendar.imageset/ConversationSearchCalendar@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
9
Images.xcassets/Chat/Input/Search/Contents.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
},
|
||||
"properties" : {
|
||||
"provides-namespace" : true
|
||||
}
|
||||
}
|
||||
22
Images.xcassets/Chat/Input/Search/DownButton.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "InlineSearchDown@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "InlineSearchDown@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
Images.xcassets/Chat/Input/Search/DownButton.imageset/InlineSearchDown@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 251 B |
BIN
Images.xcassets/Chat/Input/Search/DownButton.imageset/InlineSearchDown@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 320 B |
22
Images.xcassets/Chat/Input/Search/UpButton.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "InlineSearchUp@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "InlineSearchUp@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
Images.xcassets/Chat/Input/Search/UpButton.imageset/InlineSearchUp@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 230 B |
BIN
Images.xcassets/Chat/Input/Search/UpButton.imageset/InlineSearchUp@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 319 B |
22
Images.xcassets/Chat/Message/CallIncomingArrow.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "MessageCallIncomingIcon@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "MessageCallIncomingIcon@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
Images.xcassets/Chat/Message/CallIncomingArrow.imageset/MessageCallIncomingIcon@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 162 B |
BIN
Images.xcassets/Chat/Message/CallIncomingArrow.imageset/MessageCallIncomingIcon@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 174 B |
22
Images.xcassets/Chat/Message/CallOutgoingArrow.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "MessageCallOutgoingIcon@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "MessageCallOutgoingIcon@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
Images.xcassets/Chat/Message/CallOutgoingArrow.imageset/MessageCallOutgoingIcon@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 162 B |
BIN
Images.xcassets/Chat/Message/CallOutgoingArrow.imageset/MessageCallOutgoingIcon@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 153 B |
22
Images.xcassets/Chat/Message/InstantVideoMute.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "VideoMessageMutedIcon@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "VideoMessageMutedIcon@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
Images.xcassets/Chat/Message/InstantVideoMute.imageset/VideoMessageMutedIcon@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 593 B |
BIN
Images.xcassets/Chat/Message/InstantVideoMute.imageset/VideoMessageMutedIcon@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 874 B |
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0820"
|
||||
LastUpgradeVersion = "0900"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
@ -14,7 +14,7 @@
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D0FC407E1D5B8E7400261D9D"
|
||||
BlueprintIdentifier = "D0EC6CA41EB9F4CC00EBF1C3"
|
||||
BuildableName = "TelegramUI.framework"
|
||||
BlueprintName = "TelegramUI"
|
||||
ReferencedContainer = "container:TelegramUI.xcodeproj">
|
||||
@ -45,7 +45,7 @@
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D0FC407E1D5B8E7400261D9D"
|
||||
BlueprintIdentifier = "D0EC6CA41EB9F4CC00EBF1C3"
|
||||
BuildableName = "TelegramUI.framework"
|
||||
BlueprintName = "TelegramUI"
|
||||
ReferencedContainer = "container:TelegramUI.xcodeproj">
|
||||
@ -63,7 +63,7 @@
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D0FC407E1D5B8E7400261D9D"
|
||||
BlueprintIdentifier = "D0EC6CA41EB9F4CC00EBF1C3"
|
||||
BuildableName = "TelegramUI.framework"
|
||||
BlueprintName = "TelegramUI"
|
||||
ReferencedContainer = "container:TelegramUI.xcodeproj">
|
||||
|
||||
@ -12,6 +12,11 @@
|
||||
</dict>
|
||||
<key>SuppressBuildableAutocreation</key>
|
||||
<dict>
|
||||
<key>D0EC6CA41EB9F4CC00EBF1C3</key>
|
||||
<dict>
|
||||
<key>primary</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>D0FC407E1D5B8E7400261D9D</key>
|
||||
<dict>
|
||||
<key>primary</key>
|
||||
|
||||
@ -4,4 +4,7 @@ import AsyncDisplayKit
|
||||
class AccessoryPanelNode: ASDisplayNode {
|
||||
var dismiss: (() -> Void)?
|
||||
var interfaceInteraction: ChatPanelInterfaceInteraction?
|
||||
|
||||
func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
|
||||
}
|
||||
}
|
||||
|
||||
54
TelegramUI/ActivityIndicator.swift
Normal file
@ -0,0 +1,54 @@
|
||||
import Foundation
|
||||
import AsyncDisplayKit
|
||||
|
||||
final class ActivityIndicator: ASDisplayNode {
|
||||
private let indicatorNode: ASImageNode
|
||||
|
||||
init(theme: PresentationTheme) {
|
||||
self.indicatorNode = ASImageNode()
|
||||
self.indicatorNode.isLayerBacked = true
|
||||
self.indicatorNode.displayWithoutProcessing = true
|
||||
self.indicatorNode.displaysAsynchronously = false
|
||||
|
||||
self.indicatorNode.image = PresentationResourcesRootController.navigationIndefiniteActivityImage(theme)
|
||||
|
||||
super.init()
|
||||
|
||||
self.isLayerBacked = true
|
||||
|
||||
self.addSubnode(self.indicatorNode)
|
||||
}
|
||||
|
||||
override func willEnterHierarchy() {
|
||||
super.willEnterHierarchy()
|
||||
|
||||
let basicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
|
||||
basicAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
|
||||
basicAnimation.duration = 0.5
|
||||
basicAnimation.fromValue = NSNumber(value: Float(0.0))
|
||||
basicAnimation.toValue = NSNumber(value: Float.pi * 2.0)
|
||||
basicAnimation.repeatCount = Float.infinity
|
||||
basicAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
|
||||
|
||||
self.indicatorNode.layer.add(basicAnimation, forKey: "progressRotation")
|
||||
}
|
||||
|
||||
override func didExitHierarchy() {
|
||||
super.didExitHierarchy()
|
||||
|
||||
self.indicatorNode.layer.removeAnimation(forKey: "progressRotation")
|
||||
}
|
||||
|
||||
override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
|
||||
return CGSize(width: 22.0, height: 22.0)
|
||||
}
|
||||
|
||||
override func layout() {
|
||||
super.layout()
|
||||
|
||||
let size = self.bounds.size
|
||||
|
||||
let indicatorSize = CGSize(width: 22.0, height: 22.0)
|
||||
self.indicatorNode.frame = CGRect(origin: CGPoint(x: floor((size.width - indicatorSize.width) / 2.0), y: floor((size.height - indicatorSize.height) / 2.0)), size: indicatorSize)
|
||||
}
|
||||
}
|
||||
28
TelegramUI/AddFormatToStringWithRanges.swift
Normal file
@ -0,0 +1,28 @@
|
||||
import Foundation
|
||||
|
||||
func addAttributesToStringWithRanges(_ stringWithRanges: (String, [(Int, NSRange)]), body: MarkdownAttributeSet, argumentAttributes: [Int: MarkdownAttributeSet], textAlignment: NSTextAlignment = .natural) -> NSAttributedString {
|
||||
let result = NSMutableAttributedString()
|
||||
|
||||
var bodyAttributes: [String: Any] = [NSFontAttributeName: body.font, NSForegroundColorAttributeName: body.textColor, NSParagraphStyleAttributeName: paragraphStyleWithAlignment(textAlignment)]
|
||||
if !body.additionalAttributes.isEmpty {
|
||||
for (key, value) in body.additionalAttributes {
|
||||
bodyAttributes[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
result.append(NSAttributedString(string: stringWithRanges.0, attributes: bodyAttributes))
|
||||
|
||||
for (index, range) in stringWithRanges.1 {
|
||||
if let attributes = argumentAttributes[index] {
|
||||
var argumentAttributes: [String: Any] = [NSFontAttributeName: attributes.font, NSForegroundColorAttributeName: attributes.textColor, NSParagraphStyleAttributeName: paragraphStyleWithAlignment(textAlignment)]
|
||||
if !attributes.additionalAttributes.isEmpty {
|
||||
for (key, value) in attributes.additionalAttributes {
|
||||
argumentAttributes[key] = value
|
||||
}
|
||||
}
|
||||
result.addAttributes(argumentAttributes, range: range)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
import Foundation
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
|
||||
class AlertController {
|
||||
|
||||
}
|
||||
@ -56,7 +56,7 @@ private enum ArchivedStickerPacksEntryId: Hashable {
|
||||
|
||||
private enum ArchivedStickerPacksEntry: ItemListNodeEntry {
|
||||
case info(String)
|
||||
case pack(Int32, StickerPackCollectionInfo, StickerPackItem?, Int32, Bool, ItemListStickerPackItemEditing)
|
||||
case pack(Int32, PresentationTheme, StickerPackCollectionInfo, StickerPackItem?, String, Bool, ItemListStickerPackItemEditing)
|
||||
|
||||
var section: ItemListSectionId {
|
||||
switch self {
|
||||
@ -69,7 +69,7 @@ private enum ArchivedStickerPacksEntry: ItemListNodeEntry {
|
||||
switch self {
|
||||
case .info:
|
||||
return .index(0)
|
||||
case let .pack(_, info, _, _, _, _):
|
||||
case let .pack(_, _, info, _, _, _, _):
|
||||
return .pack(info.id)
|
||||
}
|
||||
}
|
||||
@ -82,11 +82,14 @@ private enum ArchivedStickerPacksEntry: ItemListNodeEntry {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .pack(lhsIndex, lhsInfo, lhsTopItem, lhsCount, lhsEnabled, lhsEditing):
|
||||
if case let .pack(rhsIndex, rhsInfo, rhsTopItem, rhsCount, rhsEnabled, rhsEditing) = rhs {
|
||||
case let .pack(lhsIndex, lhsTheme, lhsInfo, lhsTopItem, lhsCount, lhsEnabled, lhsEditing):
|
||||
if case let .pack(rhsIndex, rhsTheme, rhsInfo, rhsTopItem, rhsCount, rhsEnabled, rhsEditing) = rhs {
|
||||
if lhsIndex != rhsIndex {
|
||||
return false
|
||||
}
|
||||
if lhsTheme !== rhsTheme {
|
||||
return false
|
||||
}
|
||||
if lhsInfo != rhsInfo {
|
||||
return false
|
||||
}
|
||||
@ -118,9 +121,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
|
||||
@ -132,8 +135,8 @@ private enum ArchivedStickerPacksEntry: ItemListNodeEntry {
|
||||
switch self {
|
||||
case let .info(text):
|
||||
return ItemListTextItem(text: .plain(text), sectionId: self.section)
|
||||
case let .pack(_, info, topItem, count, enabled, editing):
|
||||
return ItemListStickerPackItem(account: arguments.account, packInfo: info, itemCount: count, topItem: topItem, unread: false, control: .none, editing: editing, enabled: enabled, sectionId: self.section, action: { _ in
|
||||
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: { _ in
|
||||
arguments.openStickerPack(info)
|
||||
}, setPackIdWithRevealedOptions: { current, previous in
|
||||
arguments.setPackIdWithRevealedOptions(current, previous)
|
||||
@ -189,11 +192,19 @@ private struct ArchivedStickerPacksControllerState: Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
private func archivedStickerPacksControllerEntries(state: ArchivedStickerPacksControllerState, packs: [ArchivedStickerPackItem]?, installedView: CombinedView) -> [ArchivedStickerPacksEntry] {
|
||||
private func stringForStickerCount(_ count: Int32) -> String {
|
||||
if count == 1 {
|
||||
return "1 sticker"
|
||||
} else {
|
||||
return "\(count) stickers"
|
||||
}
|
||||
}
|
||||
|
||||
private func archivedStickerPacksControllerEntries(presentationData: PresentationData, state: ArchivedStickerPacksControllerState, packs: [ArchivedStickerPackItem]?, installedView: CombinedView) -> [ArchivedStickerPacksEntry] {
|
||||
var entries: [ArchivedStickerPacksEntry] = []
|
||||
|
||||
if let packs = packs {
|
||||
entries.append(.info("You can have up to 200 sticker sets installed.\nUnused stickers are archived when you add more.\n\n"))
|
||||
entries.append(.info(presentationData.strings.StickerPacksSettings_ArchivedPacks_Info + "\n\n"))
|
||||
|
||||
var installedIds = Set<ItemCollectionId>()
|
||||
if let view = installedView.views[.itemCollectionIds(namespaces: [Namespaces.ItemCollection.CloudStickerPacks])] as? ItemCollectionIdsView, let ids = view.idsByNamespace[Namespaces.ItemCollection.CloudStickerPacks] {
|
||||
@ -203,7 +214,7 @@ private func archivedStickerPacksControllerEntries(state: ArchivedStickerPacksCo
|
||||
var index: Int32 = 0
|
||||
for item in packs {
|
||||
if !installedIds.contains(item.info.id) {
|
||||
entries.append(.pack(index, item.info, item.topItems.first, 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, 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)))
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
@ -286,18 +297,19 @@ public func archivedStickerPacksController(account: Account) -> ViewController {
|
||||
|
||||
var previousPackCount: Int?
|
||||
|
||||
let signal = combineLatest(statePromise.get() |> deliverOnMainQueue, stickerPacks.get() |> deliverOnMainQueue, installedStickerPacks.get() |> deliverOnMainQueue)
|
||||
|> map { state, packs, installedView -> (ItemListControllerState, (ItemListNodeState<ArchivedStickerPacksEntry>, ArchivedStickerPacksEntry.ItemGenerationArguments)) in
|
||||
let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get() |> deliverOnMainQueue, stickerPacks.get() |> deliverOnMainQueue, installedStickerPacks.get() |> deliverOnMainQueue)
|
||||
|> deliverOnMainQueue
|
||||
|> map { presentationData, state, packs, installedView -> (ItemListControllerState, (ItemListNodeState<ArchivedStickerPacksEntry>, ArchivedStickerPacksEntry.ItemGenerationArguments)) in
|
||||
var rightNavigationButton: ItemListNavigationButton?
|
||||
if let packs = packs, packs.count != 0 {
|
||||
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 {
|
||||
$0.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 {
|
||||
$0.withUpdatedEditing(true)
|
||||
}
|
||||
@ -313,16 +325,15 @@ public func archivedStickerPacksController(account: Account) -> ViewController {
|
||||
emptyStateItem = ItemListLoadingIndicatorEmptyStateItem()
|
||||
}
|
||||
|
||||
let controllerState = ItemListControllerState(title: .text("Archived Stickers"), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, animateChanges: true)
|
||||
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)
|
||||
|
||||
let listState = ItemListNodeState(entries: archivedStickerPacksControllerEntries(state: state, packs: packs, installedView: installedView), style: .blocks, emptyStateItem: emptyStateItem, animateChanges: previous != nil && packs != nil && (previous! != 0 && previous! >= packs!.count - 10))
|
||||
let listState = ItemListNodeState(entries: archivedStickerPacksControllerEntries(presentationData: presentationData, state: state, packs: packs, installedView: installedView), style: .blocks, emptyStateItem: emptyStateItem, animateChanges: previous != nil && packs != nil && (previous! != 0 && previous! >= packs!.count - 10))
|
||||
return (controllerState, (listState, arguments))
|
||||
} |> afterDisposed {
|
||||
actionsDisposable.dispose()
|
||||
}
|
||||
|
||||
let controller = ItemListController(signal)
|
||||
controller.navigationItem.backBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil)
|
||||
let controller = ItemListController(account: account, state: signal)
|
||||
presentControllerImpl = { [weak controller] c, p in
|
||||
if let controller = controller {
|
||||
controller.present(c, in: .window, with: p)
|
||||
|
||||
@ -27,12 +27,8 @@ final class AuthorizationSequenceCodeEntryController: ViewController {
|
||||
}
|
||||
}
|
||||
|
||||
override init(navigationBar: NavigationBar = NavigationBar()) {
|
||||
super.init(navigationBar: navigationBar)
|
||||
|
||||
self.navigationBar.backgroundColor = nil
|
||||
self.navigationBar.isOpaque = false
|
||||
self.navigationBar.stripeColor = UIColor.clear
|
||||
init() {
|
||||
super.init(navigationBarTheme: AuthorizationSequenceController.navigationBarTheme)
|
||||
|
||||
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Next", style: .done, target: self, action: #selector(self.nextPressed))
|
||||
}
|
||||
|
||||
@ -41,7 +41,7 @@ func authorizationNextOptionText(_ type: AuthorizationCodeNextType?, timeout: In
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return NSAttributedString(string: "Haven't received the code?", font: Font.regular(16.0), textColor: UIColor(0x007ee5), paragraphAlignment: .center)
|
||||
return NSAttributedString(string: "Haven't received the code?", font: Font.regular(16.0), textColor: UIColor(rgb: 0x007ee5), paragraphAlignment: .center)
|
||||
}
|
||||
}
|
||||
|
||||
@ -84,11 +84,11 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF
|
||||
override init() {
|
||||
self.navigationBackgroundNode = ASDisplayNode()
|
||||
self.navigationBackgroundNode.isLayerBacked = true
|
||||
self.navigationBackgroundNode.backgroundColor = UIColor(0xefefef)
|
||||
self.navigationBackgroundNode.backgroundColor = UIColor(rgb: 0xefefef)
|
||||
|
||||
self.stripeNode = ASDisplayNode()
|
||||
self.stripeNode.isLayerBacked = true
|
||||
self.stripeNode.backgroundColor = UIColor(0xbcbbc1)
|
||||
self.stripeNode.backgroundColor = UIColor(rgb: 0xbcbbc1)
|
||||
|
||||
self.titleNode = ASTextNode()
|
||||
self.titleNode.isLayerBacked = true
|
||||
@ -105,7 +105,7 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF
|
||||
|
||||
self.codeSeparatorNode = ASDisplayNode()
|
||||
self.codeSeparatorNode.isLayerBacked = true
|
||||
self.codeSeparatorNode.backgroundColor = UIColor(0xbcbbc1)
|
||||
self.codeSeparatorNode.backgroundColor = UIColor(rgb: 0xbcbbc1)
|
||||
|
||||
self.codeField = TextFieldNode()
|
||||
self.codeField.textField.font = Font.regular(24.0)
|
||||
@ -129,7 +129,7 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF
|
||||
|
||||
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(0xbcbcc3))
|
||||
self.codeField.textField.attributedPlaceholder = NSAttributedString(string: "Code", font: Font.regular(24.0), textColor: UIColor(rgb: 0xbcbcc3))
|
||||
}
|
||||
|
||||
deinit {
|
||||
|
||||
@ -7,6 +7,8 @@ import SwiftSignalKit
|
||||
import MtProtoKitDynamic
|
||||
|
||||
public final class AuthorizationSequenceController: NavigationController {
|
||||
static let navigationBarTheme = NavigationBarTheme(buttonColor: UIColor(rgb: 0x007ee5), primaryTextColor: .black, backgroundColor: .clear, separatorColor: .clear)
|
||||
|
||||
private var account: UnauthorizedAccount
|
||||
|
||||
private var stateDisposable: Disposable?
|
||||
|
||||
@ -440,13 +440,11 @@ final class AuthorizationSequenceCountrySelectionController: ViewController {
|
||||
|
||||
var completeWithCountryCode: ((Int) -> Void)?
|
||||
|
||||
override init(navigationBar: NavigationBar = NavigationBar()) {
|
||||
init() {
|
||||
self.innerController = InnerCountrySelectionController()
|
||||
self.innerNavigationController = UINavigationController(rootViewController: self.innerController)
|
||||
|
||||
super.init(navigationBar: navigationBar)
|
||||
|
||||
self.navigationBar.isHidden = true
|
||||
super.init(navigationBarTheme: nil)
|
||||
|
||||
self.innerController.dismiss = { [weak self] in
|
||||
self?.cancelPressed()
|
||||
|
||||
@ -24,12 +24,8 @@ final class AuthorizationSequencePasswordEntryController: ViewController {
|
||||
}
|
||||
}
|
||||
|
||||
override init(navigationBar: NavigationBar = NavigationBar()) {
|
||||
super.init(navigationBar: navigationBar)
|
||||
|
||||
self.navigationBar.backgroundColor = nil
|
||||
self.navigationBar.isOpaque = false
|
||||
self.navigationBar.stripeColor = UIColor.clear
|
||||
init() {
|
||||
super.init(navigationBarTheme: AuthorizationSequenceController.navigationBarTheme)
|
||||
|
||||
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Next", style: .done, target: self, action: #selector(self.nextPressed))
|
||||
}
|
||||
|
||||
@ -30,11 +30,11 @@ final class AuthorizationSequencePasswordEntryControllerNode: ASDisplayNode, UIT
|
||||
override init() {
|
||||
self.navigationBackgroundNode = ASDisplayNode()
|
||||
self.navigationBackgroundNode.isLayerBacked = true
|
||||
self.navigationBackgroundNode.backgroundColor = UIColor(0xefefef)
|
||||
self.navigationBackgroundNode.backgroundColor = UIColor(rgb: 0xefefef)
|
||||
|
||||
self.stripeNode = ASDisplayNode()
|
||||
self.stripeNode.isLayerBacked = true
|
||||
self.stripeNode.backgroundColor = UIColor(0xbcbbc1)
|
||||
self.stripeNode.backgroundColor = UIColor(rgb: 0xbcbbc1)
|
||||
|
||||
self.titleNode = ASTextNode()
|
||||
self.titleNode.isLayerBacked = true
|
||||
@ -49,11 +49,11 @@ final class AuthorizationSequencePasswordEntryControllerNode: ASDisplayNode, UIT
|
||||
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(0x007ee5), paragraphAlignment: .center)
|
||||
self.nextOptionNode.attributedText = NSAttributedString(string: "Forgot password?", font: Font.regular(16.0), textColor: UIColor(rgb: 0x007ee5), paragraphAlignment: .center)
|
||||
|
||||
self.codeSeparatorNode = ASDisplayNode()
|
||||
self.codeSeparatorNode.isLayerBacked = true
|
||||
self.codeSeparatorNode.backgroundColor = UIColor(0xbcbbc1)
|
||||
self.codeSeparatorNode.backgroundColor = UIColor(rgb: 0xbcbbc1)
|
||||
|
||||
self.codeField = TextFieldNode()
|
||||
self.codeField.textField.font = Font.regular(20.0)
|
||||
@ -79,7 +79,7 @@ final class AuthorizationSequencePasswordEntryControllerNode: ASDisplayNode, UIT
|
||||
}
|
||||
|
||||
func updateData(hint: String) {
|
||||
self.codeField.textField.attributedPlaceholder = NSAttributedString(string: hint, font: Font.regular(20.0), textColor: UIColor(0xbcbcc3))
|
||||
self.codeField.textField.attributedPlaceholder = NSAttributedString(string: hint, font: Font.regular(20.0), textColor: UIColor(rgb: 0xbcbcc3))
|
||||
}
|
||||
|
||||
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
|
||||
@ -24,12 +24,8 @@ final class AuthorizationSequencePhoneEntryController: ViewController {
|
||||
|
||||
private let hapticFeedback = HapticFeedback()
|
||||
|
||||
override init(navigationBar: NavigationBar = NavigationBar()) {
|
||||
super.init(navigationBar: navigationBar)
|
||||
|
||||
self.navigationBar.backgroundColor = nil
|
||||
self.navigationBar.isOpaque = false
|
||||
self.navigationBar.stripeColor = UIColor.clear
|
||||
init() {
|
||||
super.init(navigationBarTheme: AuthorizationSequenceController.navigationBarTheme)
|
||||
|
||||
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Next", style: .done, target: self, action: #selector(self.nextPressed))
|
||||
}
|
||||
|
||||
@ -241,7 +241,7 @@ private let countryButtonBackground = generateImage(CGSize(width: 61.0, height:
|
||||
let arrowSize: CGFloat = 10.0
|
||||
let lineWidth = UIScreenPixel
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.setStrokeColor(UIColor(0xbcbbc1).cgColor)
|
||||
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))
|
||||
@ -254,7 +254,7 @@ private let countryButtonBackground = generateImage(CGSize(width: 61.0, height:
|
||||
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(0xbcbbc1).cgColor)
|
||||
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))
|
||||
@ -268,7 +268,7 @@ private let phoneInputBackground = generateImage(CGSize(width: 85.0, height: 57.
|
||||
let arrowSize: CGFloat = 10.0
|
||||
let lineWidth = UIScreenPixel
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.setStrokeColor(UIColor(0xbcbbc1).cgColor)
|
||||
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))
|
||||
@ -313,11 +313,11 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode {
|
||||
override init() {
|
||||
self.navigationBackgroundNode = ASDisplayNode()
|
||||
self.navigationBackgroundNode.isLayerBacked = true
|
||||
self.navigationBackgroundNode.backgroundColor = UIColor(0xefefef)
|
||||
self.navigationBackgroundNode.backgroundColor = UIColor(rgb: 0xefefef)
|
||||
|
||||
self.stripeNode = ASDisplayNode()
|
||||
self.stripeNode.isLayerBacked = true
|
||||
self.stripeNode.backgroundColor = UIColor(0xbcbbc1)
|
||||
self.stripeNode.backgroundColor = UIColor(rgb: 0xbcbbc1)
|
||||
|
||||
self.titleNode = ASTextNode()
|
||||
self.titleNode.isLayerBacked = true
|
||||
@ -327,14 +327,14 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode {
|
||||
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(0x878787), paragraphAlignment: .center)
|
||||
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(0x007ee5)))
|
||||
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
|
||||
@ -371,7 +371,7 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode {
|
||||
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(0xbcbcc3))
|
||||
self.phoneInputNode.numberField.textField.attributedPlaceholder = NSAttributedString(string: "Your phone number", font: Font.regular(20.0), textColor: UIColor(rgb: 0xbcbcc3))
|
||||
|
||||
self.countryButton.addTarget(self, action: #selector(self.countryPressed), forControlEvents: .touchUpInside)
|
||||
|
||||
|
||||
@ -24,12 +24,8 @@ final class AuthorizationSequenceSignUpController: ViewController {
|
||||
}
|
||||
}
|
||||
|
||||
override init(navigationBar: NavigationBar = NavigationBar()) {
|
||||
super.init(navigationBar: navigationBar)
|
||||
|
||||
self.navigationBar.backgroundColor = nil
|
||||
self.navigationBar.isOpaque = false
|
||||
self.navigationBar.stripeColor = UIColor.clear
|
||||
init() {
|
||||
super.init(navigationBarTheme: AuthorizationSequenceController.navigationBarTheme)
|
||||
|
||||
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Next", style: .done, target: self, action: #selector(self.nextPressed))
|
||||
}
|
||||
|
||||
@ -33,11 +33,11 @@ final class AuthorizationSequenceSignUpControllerNode: ASDisplayNode, UITextFiel
|
||||
override init() {
|
||||
self.navigationBackgroundNode = ASDisplayNode()
|
||||
self.navigationBackgroundNode.isLayerBacked = true
|
||||
self.navigationBackgroundNode.backgroundColor = UIColor(0xefefef)
|
||||
self.navigationBackgroundNode.backgroundColor = UIColor(rgb: 0xefefef)
|
||||
|
||||
self.stripeNode = ASDisplayNode()
|
||||
self.stripeNode.isLayerBacked = true
|
||||
self.stripeNode.backgroundColor = UIColor(0xbcbbc1)
|
||||
self.stripeNode.backgroundColor = UIColor(rgb: 0xbcbbc1)
|
||||
|
||||
self.titleNode = ASTextNode()
|
||||
self.titleNode.isLayerBacked = true
|
||||
@ -47,31 +47,31 @@ final class AuthorizationSequenceSignUpControllerNode: ASDisplayNode, UITextFiel
|
||||
self.currentOptionNode = ASTextNode()
|
||||
self.currentOptionNode.isLayerBacked = true
|
||||
self.currentOptionNode.displaysAsynchronously = false
|
||||
self.currentOptionNode.attributedText = NSAttributedString(string: "Enter your name and add a profile picture", font: Font.regular(16.0), textColor: UIColor(0x878787), paragraphAlignment: .center)
|
||||
self.currentOptionNode.attributedText = NSAttributedString(string: "Enter your name and add a profile picture", font: Font.regular(16.0), textColor: UIColor(rgb: 0x878787), paragraphAlignment: .center)
|
||||
|
||||
self.firstSeparatorNode = ASDisplayNode()
|
||||
self.firstSeparatorNode.isLayerBacked = true
|
||||
self.firstSeparatorNode.backgroundColor = UIColor(0xbcbbc1)
|
||||
self.firstSeparatorNode.backgroundColor = UIColor(rgb: 0xbcbbc1)
|
||||
|
||||
self.lastSeparatorNode = ASDisplayNode()
|
||||
self.lastSeparatorNode.isLayerBacked = true
|
||||
self.lastSeparatorNode.backgroundColor = UIColor(0xbcbbc1)
|
||||
self.lastSeparatorNode.backgroundColor = UIColor(rgb: 0xbcbbc1)
|
||||
|
||||
self.firstNameField = TextFieldNode()
|
||||
self.firstNameField.textField.font = Font.regular(20.0)
|
||||
self.firstNameField.textField.textAlignment = .natural
|
||||
self.firstNameField.textField.returnKeyType = .next
|
||||
self.firstNameField.textField.attributedPlaceholder = NSAttributedString(string: "First name", font: self.firstNameField.textField.font, textColor: UIColor(0xbcbcc3))
|
||||
self.firstNameField.textField.attributedPlaceholder = NSAttributedString(string: "First name", font: self.firstNameField.textField.font, textColor: UIColor(rgb: 0xbcbcc3))
|
||||
|
||||
self.lastNameField = TextFieldNode()
|
||||
self.lastNameField.textField.font = Font.regular(20.0)
|
||||
self.lastNameField.textField.textAlignment = .natural
|
||||
self.lastNameField.textField.returnKeyType = .done
|
||||
self.lastNameField.textField.attributedPlaceholder = NSAttributedString(string: "Last name", font: self.lastNameField.textField.font, textColor: UIColor(0xbcbcc3))
|
||||
self.lastNameField.textField.attributedPlaceholder = NSAttributedString(string: "Last name", font: self.lastNameField.textField.font, textColor: UIColor(rgb: 0xbcbcc3))
|
||||
|
||||
self.addPhotoButton = HighlightableButtonNode()
|
||||
self.addPhotoButton.setAttributedTitle(NSAttributedString(string: "add\nphoto", font: Font.regular(16.0), textColor: UIColor(0xbcbcc3), paragraphAlignment: .center), for: .normal)
|
||||
self.addPhotoButton.setBackgroundImage(generateCircleImage(diameter: 110.0, lineWidth: 1.0, color: UIColor(0xbcbcc3)), for: .normal)
|
||||
self.addPhotoButton.setAttributedTitle(NSAttributedString(string: "add\nphoto", font: Font.regular(16.0), textColor: UIColor(rgb: 0xbcbcc3), paragraphAlignment: .center), for: .normal)
|
||||
self.addPhotoButton.setBackgroundImage(generateCircleImage(diameter: 110.0, lineWidth: 1.0, color: UIColor(rgb: 0xbcbcc3)), for: .normal)
|
||||
|
||||
super.init(viewBlock: {
|
||||
return UITracingLayerView()
|
||||
@ -96,8 +96,8 @@ final class AuthorizationSequenceSignUpControllerNode: ASDisplayNode, UITextFiel
|
||||
}
|
||||
|
||||
func updateData(firstName: String, lastName: String) {
|
||||
self.firstNameField.textField.attributedPlaceholder = NSAttributedString(string: firstName, font: Font.regular(20.0), textColor: UIColor(0xbcbcc3))
|
||||
self.lastNameField.textField.attributedPlaceholder = NSAttributedString(string: lastName, font: Font.regular(20.0), textColor: UIColor(0xbcbcc3))
|
||||
self.firstNameField.textField.attributedPlaceholder = NSAttributedString(string: firstName, font: Font.regular(20.0), textColor: UIColor(rgb: 0xbcbcc3))
|
||||
self.lastNameField.textField.attributedPlaceholder = NSAttributedString(string: lastName, font: Font.regular(20.0), textColor: UIColor(rgb: 0xbcbcc3))
|
||||
}
|
||||
|
||||
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
|
||||
@ -13,10 +13,9 @@ final class AuthorizationSequenceSplashController: ViewController {
|
||||
|
||||
var nextPressed: (() -> Void)?
|
||||
|
||||
override init(navigationBar: NavigationBar = NavigationBar()) {
|
||||
super.init(navigationBar: navigationBar)
|
||||
init() {
|
||||
super.init(navigationBarTheme: nil)
|
||||
|
||||
self.navigationBar.isHidden = true
|
||||
self.controller.startMessaging = { [weak self] in
|
||||
self?.nextPressed?()
|
||||
}
|
||||
|
||||
@ -12,8 +12,8 @@ public struct AutomaticMediaDownloadCategoryPeers: Coding, Equatable {
|
||||
}
|
||||
|
||||
public init(decoder: Decoder) {
|
||||
self.privateChats = (decoder.decodeInt32ForKey("p") as Int32) != 0
|
||||
self.groupsAndChannels = (decoder.decodeInt32ForKey("g") as Int32) != 0
|
||||
self.privateChats = decoder.decodeInt32ForKey("p", orElse: 0) != 0
|
||||
self.groupsAndChannels = decoder.decodeInt32ForKey("g", orElse: 0) != 0
|
||||
}
|
||||
|
||||
public func encode(_ encoder: Encoder) {
|
||||
@ -115,7 +115,7 @@ public struct AutomaticMediaDownloadSettings: PreferencesEntry, Equatable {
|
||||
|
||||
public init(decoder: Decoder) {
|
||||
self.categories = decoder.decodeObjectForKey("c", decoder: { AutomaticMediaDownloadCategories(decoder: $0) }) as! AutomaticMediaDownloadCategories
|
||||
self.saveIncomingPhotos = (decoder.decodeInt32ForKey("siph") as Int32) != 0
|
||||
self.saveIncomingPhotos = decoder.decodeInt32ForKey("siph", orElse: 0) != 0
|
||||
}
|
||||
|
||||
public func encode(_ encoder: Encoder) {
|
||||
|
||||
@ -80,12 +80,7 @@ class AvatarGalleryController: ViewController {
|
||||
self.account = account
|
||||
self.replaceRootController = replaceRootController
|
||||
|
||||
super.init()
|
||||
|
||||
self.navigationBar.backgroundColor = UIColor(white: 0.0, alpha: 0.6)
|
||||
self.navigationBar.stripeColor = UIColor.clear
|
||||
self.navigationBar.foregroundColor = UIColor.white
|
||||
self.navigationBar.accentColor = UIColor.white
|
||||
super.init(navigationBarTheme: GalleryController.darkNavigationTheme)
|
||||
|
||||
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Done", style: .done, target: self, action: #selector(self.donePressed))
|
||||
|
||||
@ -147,29 +142,6 @@ class AvatarGalleryController: ViewController {
|
||||
$0.withUpdatedFooterContentNode(footerContentNode)
|
||||
}, transition: .immediate)
|
||||
}))
|
||||
|
||||
self.centralItemAttributesDisposable.add(self.centralItemNavigationStyle.get().start(next: { [weak self] style in
|
||||
if let strongSelf = self {
|
||||
switch style {
|
||||
case .dark:
|
||||
strongSelf.statusBar.statusBarStyle = .White
|
||||
strongSelf.navigationBar.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
|
||||
strongSelf.navigationBar.stripeColor = UIColor.clear
|
||||
strongSelf.navigationBar.foregroundColor = UIColor.white
|
||||
strongSelf.navigationBar.accentColor = UIColor.white
|
||||
strongSelf.galleryNode.backgroundNode.backgroundColor = UIColor.black
|
||||
strongSelf.galleryNode.isBackgroundExtendedOverNavigationBar = true
|
||||
case .light:
|
||||
strongSelf.statusBar.statusBarStyle = .Black
|
||||
strongSelf.navigationBar.backgroundColor = UIColor(red: 0.968626451, green: 0.968626451, blue: 0.968626451, alpha: 1.0)
|
||||
strongSelf.navigationBar.foregroundColor = UIColor.black
|
||||
strongSelf.navigationBar.accentColor = UIColor(0x007ee5)
|
||||
strongSelf.navigationBar.stripeColor = UIColor(red: 0.6953125, green: 0.6953125, blue: 0.6953125, alpha: 1.0)
|
||||
strongSelf.galleryNode.backgroundNode.backgroundColor = UIColor(0xbdbdc2)
|
||||
strongSelf.galleryNode.isBackgroundExtendedOverNavigationBar = false
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
required init(coder aDecoder: NSCoder) {
|
||||
|
||||
@ -23,16 +23,16 @@ private class AvatarNodeParameters: NSObject {
|
||||
}
|
||||
|
||||
private let gradientColors: [NSArray] = [
|
||||
[UIColor(0xff516a).cgColor, UIColor(0xff885e).cgColor],
|
||||
[UIColor(0xffa85c).cgColor, UIColor(0xffcd6a).cgColor],
|
||||
[UIColor(0x54cb68).cgColor, UIColor(0xa0de7e).cgColor],
|
||||
[UIColor(0x2a9ef1).cgColor, UIColor(0x72d5fd).cgColor],
|
||||
[UIColor(0x665fff).cgColor, UIColor(0x82b1ff).cgColor],
|
||||
[UIColor(0xd669ed).cgColor, UIColor(0xe0a2f3).cgColor]
|
||||
[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: 0xd669ed).cgColor, UIColor(rgb: 0xe0a2f3).cgColor]
|
||||
]
|
||||
|
||||
private let grayscaleColors: NSArray = [
|
||||
UIColor(0xefefef).cgColor, UIColor(0xeeeeee).cgColor
|
||||
UIColor(rgb: 0xefefef).cgColor, UIColor(rgb: 0xeeeeee).cgColor
|
||||
]
|
||||
|
||||
private enum AvatarNodeState: Equatable {
|
||||
|
||||
@ -44,7 +44,7 @@ private enum BlockedPeersEntryStableId: Hashable {
|
||||
}
|
||||
|
||||
private enum BlockedPeersEntry: ItemListNodeEntry {
|
||||
case peerItem(Int32, Peer, ItemListPeerItemEditing, Bool)
|
||||
case peerItem(Int32, PresentationTheme, PresentationStrings, Peer, ItemListPeerItemEditing, Bool)
|
||||
|
||||
var section: ItemListSectionId {
|
||||
switch self {
|
||||
@ -55,18 +55,24 @@ private enum BlockedPeersEntry: ItemListNodeEntry {
|
||||
|
||||
var stableId: BlockedPeersEntryStableId {
|
||||
switch self {
|
||||
case let .peerItem(_, peer, _, _):
|
||||
case let .peerItem(_, _, _, peer, _, _):
|
||||
return .peer(peer.id)
|
||||
}
|
||||
}
|
||||
|
||||
static func ==(lhs: BlockedPeersEntry, rhs: BlockedPeersEntry) -> Bool {
|
||||
switch lhs {
|
||||
case let .peerItem(lhsIndex, lhsPeer, lhsEditing, lhsEnabled):
|
||||
if case let .peerItem(rhsIndex, rhsPeer, rhsEditing, rhsEnabled) = rhs {
|
||||
case let .peerItem(lhsIndex, lhsTheme, lhsStrings, lhsPeer, lhsEditing, lhsEnabled):
|
||||
if case let .peerItem(rhsIndex, rhsTheme, rhsStrings, rhsPeer, rhsEditing, rhsEnabled) = rhs {
|
||||
if lhsIndex != rhsIndex {
|
||||
return false
|
||||
}
|
||||
if lhsTheme !== rhsTheme {
|
||||
return false
|
||||
}
|
||||
if lhsStrings !== rhsStrings {
|
||||
return false
|
||||
}
|
||||
if !lhsPeer.isEqual(rhsPeer) {
|
||||
return false
|
||||
}
|
||||
@ -85,9 +91,9 @@ private enum BlockedPeersEntry: ItemListNodeEntry {
|
||||
|
||||
static func <(lhs: BlockedPeersEntry, rhs: BlockedPeersEntry) -> Bool {
|
||||
switch lhs {
|
||||
case let .peerItem(index, _, _, _):
|
||||
case let .peerItem(index, _, _, _, _, _):
|
||||
switch rhs {
|
||||
case let .peerItem(rhsIndex, _, _, _):
|
||||
case let .peerItem(rhsIndex, _, _, _, _, _):
|
||||
return index < rhsIndex
|
||||
}
|
||||
}
|
||||
@ -95,8 +101,8 @@ private enum BlockedPeersEntry: ItemListNodeEntry {
|
||||
|
||||
func item(_ arguments: BlockedPeersControllerArguments) -> ListViewItem {
|
||||
switch self {
|
||||
case let .peerItem(_, peer, editing, enabled):
|
||||
return ItemListPeerItem(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
|
||||
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
|
||||
arguments.setPeerIdWithRevealedOptions(previousId, id)
|
||||
}, removePeer: { peerId in
|
||||
arguments.removePeer(peerId)
|
||||
@ -149,13 +155,13 @@ private struct BlockedPeersControllerState: Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
private func blockedPeersControllerEntries(state: BlockedPeersControllerState, peers: [Peer]?) -> [BlockedPeersEntry] {
|
||||
private func blockedPeersControllerEntries(presentationData: PresentationData, state: BlockedPeersControllerState, peers: [Peer]?) -> [BlockedPeersEntry] {
|
||||
var entries: [BlockedPeersEntry] = []
|
||||
|
||||
if let peers = peers {
|
||||
var index: Int32 = 0
|
||||
for peer in peers {
|
||||
entries.append(.peerItem(index, peer, ItemListPeerItemEditing(editable: true, editing: state.editing, revealed: peer.id == state.peerIdWithRevealedOptions), state.removingPeerId != peer.id))
|
||||
entries.append(.peerItem(index, presentationData.theme, presentationData.strings, peer, ItemListPeerItemEditing(editable: true, editing: state.editing, revealed: peer.id == state.peerIdWithRevealedOptions), state.removingPeerId != peer.id))
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
@ -229,9 +235,9 @@ public func blockedPeersController(account: Account) -> ViewController {
|
||||
|
||||
var previousPeers: [Peer]?
|
||||
|
||||
let signal = combineLatest(statePromise.get(), peersPromise.get())
|
||||
let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get(), peersPromise.get())
|
||||
|> deliverOnMainQueue
|
||||
|> map { state, peers -> (ItemListControllerState, (ItemListNodeState<BlockedPeersEntry>, BlockedPeersEntry.ItemGenerationArguments)) in
|
||||
|> map { presentationData, state, peers -> (ItemListControllerState, (ItemListNodeState<BlockedPeersEntry>, BlockedPeersEntry.ItemGenerationArguments)) in
|
||||
var rightNavigationButton: ItemListNavigationButton?
|
||||
if let peers = peers, !peers.isEmpty {
|
||||
if state.editing {
|
||||
@ -261,15 +267,15 @@ public func blockedPeersController(account: Account) -> ViewController {
|
||||
let previous = previousPeers
|
||||
previousPeers = peers
|
||||
|
||||
let controllerState = ItemListControllerState(title: .text("Blocked Users"), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, animateChanges: true)
|
||||
let listState = ItemListNodeState(entries: blockedPeersControllerEntries(state: state, peers: peers), style: .blocks, emptyStateItem: emptyStateItem, animateChanges: previous != nil && peers != nil && previous!.count >= peers!.count)
|
||||
let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text("Blocked Users"), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: "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))
|
||||
} |> afterDisposed {
|
||||
actionsDisposable.dispose()
|
||||
}
|
||||
|
||||
let controller = ItemListController(signal)
|
||||
let controller = ItemListController(account: account, state: signal)
|
||||
presentControllerImpl = { [weak controller] c, p in
|
||||
if let controller = controller {
|
||||
controller.present(c, in: .window, with: p)
|
||||
|
||||
160
TelegramUI/CallController.swift
Normal file
@ -0,0 +1,160 @@
|
||||
import Foundation
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
|
||||
public final class CallController: ViewController {
|
||||
private var controllerNode: CallControllerNode {
|
||||
return self.displayNode as! CallControllerNode
|
||||
}
|
||||
|
||||
private let _ready = Promise<Bool>(false)
|
||||
override public var ready: Promise<Bool> {
|
||||
return self._ready
|
||||
}
|
||||
|
||||
private let account: Account
|
||||
public let call: PresentationCall
|
||||
|
||||
private var presentationData: PresentationData
|
||||
private var animatedAppearance = false
|
||||
|
||||
private var peer: Peer?
|
||||
|
||||
private var peerDisposable: Disposable?
|
||||
private var disposable: Disposable?
|
||||
|
||||
private var callMutedDisposable: Disposable?
|
||||
private var isMuted = false
|
||||
|
||||
private var speakerModeDisposable: Disposable?
|
||||
private var speakerMode = false
|
||||
|
||||
public init(account: Account, call: PresentationCall) {
|
||||
self.account = account
|
||||
self.call = call
|
||||
|
||||
self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 }
|
||||
|
||||
super.init(navigationBarTheme: nil)
|
||||
|
||||
self.statusBar.statusBarStyle = .White
|
||||
self.statusBar.ignoreInCall = true
|
||||
|
||||
self.supportedOrientations = .portrait
|
||||
|
||||
self.disposable = (call.state |> deliverOnMainQueue).start(next: { [weak self] callState in
|
||||
self?.callStateUpdated(callState)
|
||||
})
|
||||
|
||||
self.callMutedDisposable = (call.isMuted |> deliverOnMainQueue).start(next: { [weak self] value in
|
||||
if let strongSelf = self {
|
||||
strongSelf.isMuted = value
|
||||
if strongSelf.isNodeLoaded {
|
||||
strongSelf.controllerNode.isMuted = value
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
self.speakerModeDisposable = (call.speakerMode |> deliverOnMainQueue).start(next: { [weak self] value in
|
||||
if let strongSelf = self {
|
||||
strongSelf.speakerMode = value
|
||||
if strongSelf.isNodeLoaded {
|
||||
strongSelf.controllerNode.speakerMode = value
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.peerDisposable?.dispose()
|
||||
self.disposable?.dispose()
|
||||
self.callMutedDisposable?.dispose()
|
||||
self.speakerModeDisposable?.dispose()
|
||||
}
|
||||
|
||||
private func callStateUpdated(_ callState: PresentationCallState) {
|
||||
if self.isNodeLoaded {
|
||||
self.controllerNode.updateCallState(callState)
|
||||
}
|
||||
}
|
||||
|
||||
override public func loadDisplayNode() {
|
||||
self.displayNode = CallControllerNode(account: self.account, presentationData: self.presentationData, statusBar: self.statusBar)
|
||||
self.displayNodeDidLoad()
|
||||
|
||||
self.controllerNode.toggleMute = { [weak self] in
|
||||
self?.call.toggleIsMuted()
|
||||
}
|
||||
|
||||
self.controllerNode.toggleSpeaker = { [weak self] in
|
||||
self?.call.toggleSpeaker()
|
||||
}
|
||||
|
||||
self.controllerNode.acceptCall = { [weak self] in
|
||||
let _ = self?.call.answer()
|
||||
}
|
||||
|
||||
self.controllerNode.endCall = { [weak self] in
|
||||
let _ = self?.call.hangUp()
|
||||
}
|
||||
|
||||
self.controllerNode.back = { [weak self] in
|
||||
let _ = self?.dismiss()
|
||||
}
|
||||
|
||||
self.controllerNode.disissedInteractively = { [weak self] in
|
||||
self?.animatedAppearance = false
|
||||
self?.presentingViewController?.dismiss(animated: false, completion: nil)
|
||||
}
|
||||
|
||||
self.peerDisposable = (account.postbox.peerView(id: self.call.peerId)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] view in
|
||||
if let strongSelf = self {
|
||||
if let peer = view.peers[view.peerId] {
|
||||
strongSelf.peer = peer
|
||||
strongSelf.controllerNode.updatePeer(peer: peer)
|
||||
strongSelf._ready.set(.single(true))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
self.controllerNode.isMuted = self.isMuted
|
||||
self.controllerNode.speakerMode = self.speakerMode
|
||||
}
|
||||
|
||||
override public func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
if !self.animatedAppearance {
|
||||
self.animatedAppearance = true
|
||||
|
||||
self.controllerNode.animateIn()
|
||||
}
|
||||
}
|
||||
|
||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
|
||||
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition)
|
||||
}
|
||||
|
||||
override open func dismiss(completion: (() -> Void)? = nil) {
|
||||
self.controllerNode.animateOut(completion: { [weak self] in
|
||||
self?.animatedAppearance = false
|
||||
self?.presentingViewController?.dismiss(animated: false, completion: nil)
|
||||
|
||||
completion?()
|
||||
})
|
||||
}
|
||||
|
||||
@objc func backPressed() {
|
||||
self.dismiss()
|
||||
}
|
||||
}
|
||||
195
TelegramUI/CallControllerButton.swift
Normal file
@ -0,0 +1,195 @@
|
||||
import Foundation
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
|
||||
enum CallControllerButtonType {
|
||||
case mute
|
||||
case end
|
||||
case accept
|
||||
case speaker
|
||||
case bluetooth
|
||||
}
|
||||
|
||||
private let buttonSize = CGSize(width: 75.0, height: 75.0)
|
||||
|
||||
private func generateEmptyButtonImage(icon: UIImage?, strokeColor: UIColor?, fillColor: UIColor, knockout: Bool = false, angle: CGFloat = 0.0) -> UIImage? {
|
||||
return generateImage(buttonSize, contextGenerator: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.setBlendMode(.copy)
|
||||
if let strokeColor = strokeColor {
|
||||
context.setFillColor(strokeColor.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
|
||||
context.setFillColor(fillColor.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: 1.5, y: 1.5), size: CGSize(width: size.width - 3.0, height: size.height - 3.0)))
|
||||
} else {
|
||||
context.setFillColor(fillColor.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height)))
|
||||
}
|
||||
|
||||
if let icon = icon {
|
||||
if !angle.isZero {
|
||||
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
|
||||
context.rotate(by: angle)
|
||||
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
|
||||
}
|
||||
let imageSize = icon.size
|
||||
let imageRect = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: floor((size.width - imageSize.height) / 2.0)), size: imageSize)
|
||||
if knockout {
|
||||
context.setBlendMode(.copy)
|
||||
context.clip(to: imageRect, mask: icon.cgImage!)
|
||||
context.setFillColor(UIColor.clear.cgColor)
|
||||
context.fill(imageRect)
|
||||
} else {
|
||||
context.setBlendMode(.normal)
|
||||
context.draw(icon.cgImage!, in: imageRect)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func generateFilledButtonImage(color: UIColor, icon: UIImage?, angle: CGFloat = 0.0) -> UIImage? {
|
||||
return generateImage(buttonSize, contextGenerator: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.setBlendMode(.normal)
|
||||
context.setFillColor(color.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
if let icon = icon {
|
||||
if !angle.isZero {
|
||||
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
|
||||
context.rotate(by: angle)
|
||||
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
|
||||
}
|
||||
context.draw(icon.cgImage!, in: CGRect(origin: CGPoint(x: floor((size.width - icon.size.width) / 2.0), y: floor((size.height - icon.size.height) / 2.0)), size: icon.size))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private let emptyStroke = UIColor(white: 1.0, alpha: 0.8)
|
||||
private let emptyHighlightedFill = UIColor(white: 1.0, alpha: 0.3)
|
||||
private let invertedFill = UIColor(white: 1.0, alpha: 1.0)
|
||||
|
||||
private let labelFont = Font.regular(14.5)
|
||||
|
||||
final class CallControllerButtonNode: HighlightTrackingButtonNode {
|
||||
private let regularImage: UIImage?
|
||||
private let highlightedImage: UIImage?
|
||||
private let filledImage: UIImage?
|
||||
|
||||
private let backgroundNode: ASImageNode
|
||||
private let labelNode: ASTextNode?
|
||||
|
||||
init(type: CallControllerButtonType, label: String?) {
|
||||
self.backgroundNode = ASImageNode()
|
||||
self.backgroundNode.isLayerBacked = true
|
||||
self.backgroundNode.displayWithoutProcessing = false
|
||||
self.backgroundNode.displaysAsynchronously = false
|
||||
|
||||
if let label = label {
|
||||
let labelNode = ASTextNode()
|
||||
labelNode.attributedText = NSAttributedString(string: label, font: labelFont, textColor: .white)
|
||||
self.labelNode = labelNode
|
||||
} else {
|
||||
self.labelNode = nil
|
||||
}
|
||||
|
||||
var regularImage: UIImage?
|
||||
var highlightedImage: UIImage?
|
||||
var filledImage: UIImage?
|
||||
|
||||
switch type {
|
||||
case .mute:
|
||||
regularImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallMuteButton"), strokeColor: emptyStroke, fillColor: .clear)
|
||||
highlightedImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallMuteButton"), strokeColor: emptyStroke, fillColor: emptyHighlightedFill)
|
||||
filledImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallMuteButton"), strokeColor: nil, fillColor: invertedFill, knockout: true)
|
||||
case .accept:
|
||||
regularImage = generateFilledButtonImage(color: UIColor(rgb: 0x74db58), icon: UIImage(bundleImageName: "Call/CallPhoneButton"), angle: CGFloat.pi * 3.0 / 4.0)
|
||||
highlightedImage = generateFilledButtonImage(color: UIColor(rgb: 0x74db58), icon: UIImage(bundleImageName: "Call/CallPhoneButton"), angle: CGFloat.pi * 3.0 / 4.0)
|
||||
case .end:
|
||||
regularImage = generateFilledButtonImage(color: UIColor(rgb: 0xd92326), icon: UIImage(bundleImageName: "Call/CallPhoneButton"))
|
||||
highlightedImage = generateFilledButtonImage(color: UIColor(rgb: 0xd92326), icon: UIImage(bundleImageName: "Call/CallPhoneButton"))
|
||||
case .speaker:
|
||||
regularImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallSpeakerButton"), strokeColor: emptyStroke, fillColor: .clear)
|
||||
highlightedImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallSpeakerButton"), strokeColor: emptyStroke, fillColor: emptyHighlightedFill)
|
||||
filledImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallSpeakerButton"), strokeColor: nil, fillColor: invertedFill, knockout: true)
|
||||
case .bluetooth:
|
||||
regularImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallBluetoothButton"), strokeColor: emptyStroke, fillColor: .clear)
|
||||
highlightedImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallBluetoothButton"), strokeColor: emptyStroke, fillColor: emptyHighlightedFill)
|
||||
filledImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallBluetoothButton"), strokeColor: nil, fillColor: invertedFill, knockout: true)
|
||||
}
|
||||
|
||||
self.regularImage = regularImage
|
||||
self.highlightedImage = highlightedImage
|
||||
self.filledImage = filledImage
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
|
||||
if let labelNode = self.labelNode {
|
||||
self.addSubnode(labelNode)
|
||||
}
|
||||
|
||||
self.backgroundNode.image = regularImage
|
||||
self.currentImage = regularImage
|
||||
|
||||
self.highligthedChanged = { [weak self] highlighted in
|
||||
if let strongSelf = self {
|
||||
strongSelf.internalHighlighted = highlighted
|
||||
strongSelf.updateState(highlighted: highlighted, selected: strongSelf.isSelected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var internalHighlighted = false
|
||||
|
||||
override var isSelected: Bool {
|
||||
didSet {
|
||||
self.updateState(highlighted: self.internalHighlighted, selected: self.isSelected)
|
||||
}
|
||||
}
|
||||
|
||||
private var currentImage: UIImage?
|
||||
|
||||
private func updateState(highlighted: Bool, selected: Bool) {
|
||||
let image: UIImage?
|
||||
if selected {
|
||||
image = self.filledImage
|
||||
} else if highlighted {
|
||||
image = self.highlightedImage
|
||||
} else {
|
||||
image = self.regularImage
|
||||
}
|
||||
|
||||
if self.currentImage !== image {
|
||||
let currentContents = self.backgroundNode.layer.contents
|
||||
self.backgroundNode.layer.removeAnimation(forKey: "contents")
|
||||
if let currentContents = currentContents, let image = image {
|
||||
self.backgroundNode.image = image
|
||||
self.backgroundNode.layer.animate(from: currentContents as AnyObject, to: image.cgImage!, keyPath: "contents", timingFunction: kCAMediaTimingFunctionEaseInEaseOut, duration: image === self.currentImage || image === self.filledImage ? 0.25 : 0.15)
|
||||
} else {
|
||||
self.backgroundNode.image = image
|
||||
}
|
||||
self.currentImage = image
|
||||
}
|
||||
}
|
||||
|
||||
func animateRollTransition() {
|
||||
self.backgroundNode.layer.animate(from: 0.0 as NSNumber, to: (-CGFloat.pi * 5 / 4) as NSNumber, keyPath: "transform.rotation.z", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.3, removeOnCompletion: false)
|
||||
self.labelNode?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
override func layout() {
|
||||
super.layout()
|
||||
|
||||
let size = self.bounds.size
|
||||
|
||||
self.backgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.width))
|
||||
|
||||
if let labelNode = self.labelNode {
|
||||
let labelSize = labelNode.measure(CGSize(width: 200.0, height: 100.0))
|
||||
labelNode.frame = CGRect(origin: CGPoint(x: floor((size.width - labelSize.width) / 2.0), y: 81.0), size: labelSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
196
TelegramUI/CallControllerButtonsNode.swift
Normal file
@ -0,0 +1,196 @@
|
||||
import Foundation
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
|
||||
enum CallControllerButtonsSpeakerMode {
|
||||
case bluetooth
|
||||
case speaker
|
||||
}
|
||||
|
||||
enum CallControllerButtonsMode: Equatable {
|
||||
case active(CallControllerButtonsSpeakerMode)
|
||||
case incoming
|
||||
|
||||
static func ==(lhs: CallControllerButtonsMode, rhs: CallControllerButtonsMode) -> Bool {
|
||||
switch lhs {
|
||||
case let .active(mode):
|
||||
if case .active(mode) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .incoming:
|
||||
if case .incoming = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class CallControllerButtonsNode: ASDisplayNode {
|
||||
private let acceptButton: CallControllerButtonNode
|
||||
private let declineButton: CallControllerButtonNode
|
||||
|
||||
private let muteButton: CallControllerButtonNode
|
||||
private let endButton: CallControllerButtonNode
|
||||
private let speakerButton: CallControllerButtonNode
|
||||
|
||||
private var mode: CallControllerButtonsMode?
|
||||
|
||||
private var validLayout: CGFloat?
|
||||
|
||||
var isMuted = false {
|
||||
didSet {
|
||||
self.muteButton.isSelected = self.isMuted
|
||||
}
|
||||
}
|
||||
|
||||
var speakerMode = false {
|
||||
didSet {
|
||||
self.speakerButton.isSelected = self.speakerMode
|
||||
}
|
||||
}
|
||||
|
||||
var accept: (() -> Void)?
|
||||
var mute: (() -> Void)?
|
||||
var end: (() -> Void)?
|
||||
var speaker: (() -> Void)?
|
||||
|
||||
init(strings: PresentationStrings) {
|
||||
self.acceptButton = CallControllerButtonNode(type: .accept, label: strings.Call_Accept)
|
||||
self.acceptButton.alpha = 0.0
|
||||
self.declineButton = CallControllerButtonNode(type: .end, label: strings.Call_Decline)
|
||||
self.declineButton.alpha = 0.0
|
||||
|
||||
self.muteButton = CallControllerButtonNode(type: .mute, label: nil)
|
||||
self.muteButton.alpha = 0.0
|
||||
self.endButton = CallControllerButtonNode(type: .end, label: nil)
|
||||
self.endButton.alpha = 0.0
|
||||
self.speakerButton = CallControllerButtonNode(type: .speaker, label: nil)
|
||||
self.speakerButton.alpha = 0.0
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.acceptButton)
|
||||
self.addSubnode(self.declineButton)
|
||||
self.addSubnode(self.muteButton)
|
||||
self.addSubnode(self.endButton)
|
||||
self.addSubnode(self.speakerButton)
|
||||
|
||||
self.acceptButton.addTarget(self, action: #selector(self.buttonPressed(_:)), forControlEvents: .touchUpInside)
|
||||
self.declineButton.addTarget(self, action: #selector(self.buttonPressed(_:)), forControlEvents: .touchUpInside)
|
||||
self.muteButton.addTarget(self, action: #selector(self.buttonPressed(_:)), forControlEvents: .touchUpInside)
|
||||
self.endButton.addTarget(self, action: #selector(self.buttonPressed(_:)), forControlEvents: .touchUpInside)
|
||||
self.speakerButton.addTarget(self, action: #selector(self.buttonPressed(_:)), forControlEvents: .touchUpInside)
|
||||
}
|
||||
|
||||
func updateLayout(constrainedWidth: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
let previousLayout = self.validLayout
|
||||
self.validLayout = constrainedWidth
|
||||
|
||||
if let mode = self.mode, previousLayout != self.validLayout {
|
||||
self.updateButtonsLayout(mode: mode, width: constrainedWidth, animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
func updateMode(_ mode: CallControllerButtonsMode) {
|
||||
if self.mode != mode {
|
||||
let previousMode = self.mode
|
||||
self.mode = mode
|
||||
if let validLayout = self.validLayout {
|
||||
self.updateButtonsLayout(mode: mode, width: validLayout, animated: previousMode != nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func updateButtonsLayout(mode: CallControllerButtonsMode, width: CGFloat, animated: Bool) {
|
||||
let transition: ContainedViewLayoutTransition
|
||||
if animated {
|
||||
transition = .animated(duration: 0.3, curve: .spring)
|
||||
} else {
|
||||
transition = .immediate
|
||||
}
|
||||
|
||||
let threeButtonSpacing: CGFloat = 28.0
|
||||
let twoButtonSpacing: CGFloat = 105.0
|
||||
let buttonSize = CGSize(width: 75.0, height: 75.0)
|
||||
|
||||
let threeButtonsWidth = 3.0 * buttonSize.width + max(0.0, 3.0 - 1.0) * threeButtonSpacing
|
||||
let twoButtonsWidth = 2.0 * buttonSize.width + max(0.0, 2.0 - 1.0) * twoButtonSpacing
|
||||
|
||||
var origin = CGPoint(x: floor((width - threeButtonsWidth) / 2.0), y: 0.0)
|
||||
for button in [self.muteButton, self.endButton, self.speakerButton] {
|
||||
transition.updateFrame(node: button, frame: CGRect(origin: origin, size: buttonSize))
|
||||
origin.x += buttonSize.width + threeButtonSpacing
|
||||
}
|
||||
|
||||
origin = CGPoint(x: floor((width - twoButtonsWidth) / 2.0), y: 0.0)
|
||||
for button in [self.declineButton, self.acceptButton] {
|
||||
transition.updateFrame(node: button, frame: CGRect(origin: origin, size: buttonSize))
|
||||
origin.x += buttonSize.width + twoButtonSpacing
|
||||
}
|
||||
|
||||
switch mode {
|
||||
case .incoming:
|
||||
for button in [self.declineButton, self.acceptButton] {
|
||||
button.alpha = 1.0
|
||||
}
|
||||
for button in [self.muteButton, self.endButton, self.speakerButton] {
|
||||
button.alpha = 0.0
|
||||
}
|
||||
case .active:
|
||||
for button in [self.muteButton, self.speakerButton] {
|
||||
if animated && button.alpha.isZero {
|
||||
button.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
}
|
||||
button.alpha = 1.0
|
||||
}
|
||||
var animatingAcceptButton = false
|
||||
if self.endButton.alpha.isZero {
|
||||
if animated {
|
||||
if !self.acceptButton.alpha.isZero {
|
||||
animatingAcceptButton = true
|
||||
self.endButton.layer.animatePosition(from: self.acceptButton.position, to: self.endButton.position, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
self.acceptButton.animateRollTransition()
|
||||
self.endButton.layer.animate(from: (CGFloat.pi * 5 / 4) as NSNumber, to: 0.0 as NSNumber, keyPath: "transform.rotation.z", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.3)
|
||||
self.acceptButton.layer.animatePosition(from: self.acceptButton.position, to: self.endButton.position, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { [weak self] _ in
|
||||
if let strongSelf = self {
|
||||
strongSelf.acceptButton.alpha = 0.0
|
||||
strongSelf.acceptButton.layer.removeAnimation(forKey: "position")
|
||||
strongSelf.acceptButton.layer.removeAnimation(forKey: "transform.rotation.z")
|
||||
}
|
||||
})
|
||||
}
|
||||
self.endButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
self.endButton.alpha = 1.0
|
||||
}
|
||||
|
||||
if !self.declineButton.alpha.isZero {
|
||||
if animated {
|
||||
self.declineButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
|
||||
}
|
||||
self.declineButton.alpha = 0.0
|
||||
}
|
||||
|
||||
if self.acceptButton.alpha.isZero && !animatingAcceptButton {
|
||||
self.acceptButton.alpha = 0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc func buttonPressed(_ button: CallControllerButtonNode) {
|
||||
if button === self.muteButton {
|
||||
self.mute?()
|
||||
} else if button === self.endButton || button === self.declineButton {
|
||||
self.end?()
|
||||
} else if button === self.speakerButton {
|
||||
self.speaker?()
|
||||
} else if button === self.acceptButton {
|
||||
self.accept?()
|
||||
}
|
||||
}
|
||||
}
|
||||
107
TelegramUI/CallControllerKeyPreviewNode.swift
Normal file
@ -0,0 +1,107 @@
|
||||
import Foundation
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import TelegramLegacyComponents
|
||||
|
||||
private let emojiFont = Font.regular(28.0)
|
||||
private let textFont = Font.regular(15.0)
|
||||
|
||||
final class CallControllerKeyPreviewNode: ASDisplayNode {
|
||||
private let keyTextNode: ASTextNode
|
||||
private let infoTextNode: ASTextNode
|
||||
|
||||
private let effectView: UIVisualEffectView
|
||||
|
||||
private let dismiss: () -> Void
|
||||
|
||||
init(keyText: String, infoText: String, dismiss: @escaping () -> Void) {
|
||||
self.keyTextNode = ASTextNode()
|
||||
self.keyTextNode.displaysAsynchronously = false
|
||||
self.infoTextNode = ASTextNode()
|
||||
self.infoTextNode.displaysAsynchronously = false
|
||||
self.dismiss = dismiss
|
||||
|
||||
self.effectView = UIVisualEffectView()
|
||||
if #available(iOS 9.0, *) {
|
||||
} else {
|
||||
self.effectView.effect = UIBlurEffect(style: .dark)
|
||||
self.effectView.alpha = 0.0
|
||||
}
|
||||
|
||||
super.init()
|
||||
|
||||
self.keyTextNode.attributedText = NSAttributedString(string: keyText, attributes: [NSFontAttributeName: Font.regular(58.0), NSKernAttributeName: 9.0 as NSNumber])
|
||||
|
||||
self.infoTextNode.attributedText = NSAttributedString(string: infoText, font: Font.regular(14.0), textColor: UIColor.white, paragraphAlignment: .center)
|
||||
|
||||
self.view.addSubview(self.effectView)
|
||||
self.addSubnode(self.keyTextNode)
|
||||
self.addSubnode(self.infoTextNode)
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapResture(_:))))
|
||||
}
|
||||
|
||||
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
|
||||
self.effectView.frame = CGRect(origin: CGPoint(), size: size)
|
||||
|
||||
let keyTextSize = self.keyTextNode.measure(CGSize(width: 300.0, height: 300.0))
|
||||
transition.updateFrame(node: self.keyTextNode, frame: CGRect(origin: CGPoint(x: floor((size.width - keyTextSize.width) / 2) + 6.0, y: floor((size.height - keyTextSize.height) / 2) - 50.0), size: keyTextSize))
|
||||
|
||||
let infoTextSize = self.infoTextNode.measure(CGSize(width: size.width - 20.0, height: CGFloat.greatestFiniteMagnitude))
|
||||
transition.updateFrame(node: self.infoTextNode, frame: CGRect(origin: CGPoint(x: floor((size.width - infoTextSize.width) / 2.0), y: floor((size.height - infoTextSize.height) / 2.0) + 30.0), size: infoTextSize))
|
||||
}
|
||||
|
||||
func animateIn(from rect: CGRect, fromNode: ASDisplayNode) {
|
||||
self.keyTextNode.layer.animatePosition(from: CGPoint(x: rect.midX, y: rect.midY), to: self.keyTextNode.layer.position, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
if let transitionView = fromNode.view.snapshotView(afterScreenUpdates: false) {
|
||||
self.view.addSubview(transitionView)
|
||||
transitionView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
||||
transitionView.layer.animatePosition(from: CGPoint(x: rect.midX, y: rect.midY), to: self.keyTextNode.layer.position, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { [weak transitionView] _ in
|
||||
transitionView?.removeFromSuperview()
|
||||
})
|
||||
transitionView.layer.animateScale(from: 1.0, to: self.keyTextNode.frame.size.width / rect.size.width, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
||||
}
|
||||
self.keyTextNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
||||
|
||||
self.keyTextNode.layer.animateScale(from: rect.size.width / self.keyTextNode.frame.size.width, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
|
||||
self.infoTextNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
|
||||
UIView.animate(withDuration: 0.3, animations: {
|
||||
if #available(iOS 9.0, *) {
|
||||
self.effectView.effect = TGBlurEffect.call()!
|
||||
} else {
|
||||
self.effectView.alpha = 1.0
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func animateOut(to rect: CGRect, toNode: ASDisplayNode, completion: @escaping () -> Void) {
|
||||
self.keyTextNode.layer.animatePosition(from: self.keyTextNode.layer.position, to: CGPoint(x: rect.midX, y: rect.midY), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
|
||||
completion()
|
||||
})
|
||||
self.keyTextNode.layer.animateScale(from: 1.0, to: rect.size.width / self.keyTextNode.frame.size.width, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
||||
|
||||
self.infoTextNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
|
||||
|
||||
UIView.animate(withDuration: 0.3, animations: {
|
||||
if #available(iOS 9.0, *) {
|
||||
self.effectView.effect = nil
|
||||
} else {
|
||||
self.effectView.alpha = 0.0
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@objc func tapResture(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
self.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
369
TelegramUI/CallControllerNode.swift
Normal file
@ -0,0 +1,369 @@
|
||||
import Foundation
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
|
||||
private func generateBackArrowImage(color: UIColor) -> UIImage? {
|
||||
return generateImage(CGSize(width: 13.0, height: 22.0), contextGenerator: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.setFillColor(color.cgColor)
|
||||
let _ = try? drawSvgPath(context, path: "M10.6569398,0.0 L0.0,11 L10.6569398,22 L13,19.1782395 L5.07681762,11 L13,2.82176047 Z ")
|
||||
})
|
||||
}
|
||||
|
||||
final class CallControllerNode: ASDisplayNode {
|
||||
private let account: Account
|
||||
|
||||
private let statusBar: StatusBar
|
||||
|
||||
private var presentationData: PresentationData
|
||||
private var peer: Peer?
|
||||
|
||||
private let containerNode: ASDisplayNode
|
||||
|
||||
private let imageNode: TransformImageNode
|
||||
private let dimNode: ASDisplayNode
|
||||
private let backButtonArrowNode: ASImageNode
|
||||
private let backButtonNode: HighlightableButtonNode
|
||||
private let statusNode: CallControllerStatusNode
|
||||
private let buttonsNode: CallControllerButtonsNode
|
||||
private var keyPreviewNode: CallControllerKeyPreviewNode?
|
||||
|
||||
private var keyTextData: (Data, String)?
|
||||
private let keyButtonNode: HighlightableButtonNode
|
||||
|
||||
private var validLayout: (ContainerViewLayout, CGFloat)?
|
||||
|
||||
var isMuted: Bool = false {
|
||||
didSet {
|
||||
self.buttonsNode.isMuted = self.isMuted
|
||||
}
|
||||
}
|
||||
|
||||
var speakerMode: Bool = false {
|
||||
didSet {
|
||||
self.buttonsNode.speakerMode = self.speakerMode
|
||||
}
|
||||
}
|
||||
|
||||
var toggleMute: (() -> Void)?
|
||||
var toggleSpeaker: (() -> Void)?
|
||||
var acceptCall: (() -> Void)?
|
||||
var endCall: (() -> Void)?
|
||||
var back: (() -> Void)?
|
||||
var disissedInteractively: (() -> Void)?
|
||||
|
||||
init(account: Account, presentationData: PresentationData, statusBar: StatusBar) {
|
||||
self.account = account
|
||||
self.presentationData = presentationData
|
||||
self.statusBar = statusBar
|
||||
|
||||
self.containerNode = ASDisplayNode()
|
||||
|
||||
self.imageNode = TransformImageNode()
|
||||
self.dimNode = ASDisplayNode()
|
||||
self.dimNode.isLayerBacked = true
|
||||
self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.4)
|
||||
|
||||
self.backButtonArrowNode = ASImageNode()
|
||||
self.backButtonArrowNode.displayWithoutProcessing = true
|
||||
self.backButtonArrowNode.displaysAsynchronously = false
|
||||
self.backButtonArrowNode.image = generateBackArrowImage(color: .white)
|
||||
self.backButtonNode = HighlightableButtonNode()
|
||||
|
||||
self.statusNode = CallControllerStatusNode()
|
||||
self.buttonsNode = CallControllerButtonsNode(strings: self.presentationData.strings)
|
||||
self.keyButtonNode = HighlightableButtonNode()
|
||||
|
||||
super.init(viewBlock: {
|
||||
return UITracingLayerView()
|
||||
}, didLoad: nil)
|
||||
|
||||
self.containerNode.backgroundColor = .black
|
||||
|
||||
self.addSubnode(self.containerNode)
|
||||
|
||||
self.backButtonNode.setTitle(presentationData.strings.Common_Back, with: Font.regular(17.0), with: .white, for: [])
|
||||
self.backButtonNode.hitTestSlop = UIEdgeInsets(top: -8.0, left: -20.0, bottom: -8.0, right: -8.0)
|
||||
self.backButtonNode.highligthedChanged = { [weak self] highlighted in
|
||||
if let strongSelf = self {
|
||||
if highlighted {
|
||||
strongSelf.backButtonNode.layer.removeAnimation(forKey: "opacity")
|
||||
strongSelf.backButtonArrowNode.layer.removeAnimation(forKey: "opacity")
|
||||
strongSelf.backButtonNode.alpha = 0.4
|
||||
strongSelf.backButtonArrowNode.alpha = 0.4
|
||||
} else {
|
||||
strongSelf.backButtonNode.alpha = 1.0
|
||||
strongSelf.backButtonArrowNode.alpha = 1.0
|
||||
strongSelf.backButtonNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
||||
strongSelf.backButtonArrowNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.containerNode.addSubnode(self.imageNode)
|
||||
self.containerNode.addSubnode(self.dimNode)
|
||||
self.containerNode.addSubnode(self.statusNode)
|
||||
self.containerNode.addSubnode(self.buttonsNode)
|
||||
self.containerNode.addSubnode(self.keyButtonNode)
|
||||
self.containerNode.addSubnode(self.backButtonArrowNode)
|
||||
self.containerNode.addSubnode(self.backButtonNode)
|
||||
|
||||
self.buttonsNode.mute = { [weak self] in
|
||||
self?.toggleMute?()
|
||||
}
|
||||
|
||||
self.buttonsNode.speaker = { [weak self] in
|
||||
self?.toggleSpeaker?()
|
||||
}
|
||||
|
||||
self.buttonsNode.end = { [weak self] in
|
||||
self?.endCall?()
|
||||
}
|
||||
|
||||
self.buttonsNode.accept = { [weak self] in
|
||||
self?.acceptCall?()
|
||||
}
|
||||
|
||||
self.keyButtonNode.addTarget(self, action: #selector(self.keyPressed), forControlEvents: .touchUpInside)
|
||||
|
||||
self.backButtonNode.addTarget(self, action: #selector(self.backPressed), forControlEvents: .touchUpInside)
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))))
|
||||
}
|
||||
|
||||
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.statusNode.title = peer.displayTitle
|
||||
|
||||
if let (layout, navigationBarHeight) = self.validLayout {
|
||||
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateCallState(_ callState: PresentationCallState) {
|
||||
let statusValue: CallControllerStatusValue
|
||||
switch callState {
|
||||
case .waiting, .connecting:
|
||||
statusValue = .text(self.presentationData.strings.Call_StatusConnecting)
|
||||
case let .requesting(ringing):
|
||||
if ringing {
|
||||
statusValue = .text(self.presentationData.strings.Call_StatusRinging)
|
||||
} else {
|
||||
statusValue = .text(self.presentationData.strings.Call_StatusRequesting)
|
||||
}
|
||||
case .terminating, .terminated:
|
||||
statusValue = .text(self.presentationData.strings.Call_StatusEnded)
|
||||
case .ringing:
|
||||
statusValue = .text(self.presentationData.strings.Call_StatusIncoming)
|
||||
case let .active(timestamp, keyVisualHash):
|
||||
let strings = self.presentationData.strings
|
||||
statusValue = .timer({ value in
|
||||
return strings.Call_StatusOngoing(value).0
|
||||
}, timestamp)
|
||||
if self.keyTextData?.0 != keyVisualHash {
|
||||
let text = stringForEmojiHashOfData(keyVisualHash, 4)!
|
||||
self.keyTextData = (keyVisualHash, text)
|
||||
|
||||
self.keyButtonNode.setAttributedTitle(NSAttributedString(string: text, attributes: [NSFontAttributeName: Font.regular(22.0), NSKernAttributeName: 2.5 as NSNumber]), for: [])
|
||||
|
||||
let keyTextSize = self.keyButtonNode.measure(CGSize(width: 200.0, height: 200.0))
|
||||
self.keyButtonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
self.keyButtonNode.frame = CGRect(origin: self.keyButtonNode.frame.origin, size: keyTextSize)
|
||||
|
||||
if let (layout, navigationBarHeight) = self.validLayout {
|
||||
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
|
||||
}
|
||||
}
|
||||
}
|
||||
switch callState {
|
||||
case .terminated, .terminating:
|
||||
if !self.statusNode.alpha.isEqual(to: 0.5) {
|
||||
self.statusNode.alpha = 0.5
|
||||
self.buttonsNode.alpha = 0.5
|
||||
self.keyButtonNode.alpha = 0.5
|
||||
self.backButtonArrowNode.alpha = 0.5
|
||||
self.backButtonNode.alpha = 0.5
|
||||
|
||||
self.statusNode.layer.animateAlpha(from: 1.0, to: 0.5, duration: 0.25)
|
||||
self.buttonsNode.layer.animateAlpha(from: 1.0, to: 0.5, duration: 0.25)
|
||||
self.keyButtonNode.layer.animateAlpha(from: 1.0, to: 0.5, duration: 0.25)
|
||||
}
|
||||
default:
|
||||
if !self.statusNode.alpha.isEqual(to: 1.0) {
|
||||
self.statusNode.alpha = 1.0
|
||||
self.buttonsNode.alpha = 1.0
|
||||
self.keyButtonNode.alpha = 1.0
|
||||
self.backButtonArrowNode.alpha = 1.0
|
||||
self.backButtonNode.alpha = 1.0
|
||||
}
|
||||
}
|
||||
self.statusNode.status = statusValue
|
||||
|
||||
switch callState {
|
||||
case .ringing:
|
||||
self.buttonsNode.updateMode(.incoming)
|
||||
default:
|
||||
self.buttonsNode.updateMode(.active(.speaker))
|
||||
}
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
var bounds = self.bounds
|
||||
bounds.origin = CGPoint()
|
||||
self.bounds = bounds
|
||||
self.layer.removeAnimation(forKey: "bounds")
|
||||
self.statusBar.layer.removeAnimation(forKey: "opacity")
|
||||
self.containerNode.layer.removeAnimation(forKey: "opacity")
|
||||
self.containerNode.layer.removeAnimation(forKey: "scale")
|
||||
self.statusBar.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
self.containerNode.layer.animateScale(from: 1.04, to: 1.0, duration: 0.3)
|
||||
self.containerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
|
||||
func animateOut(completion: @escaping () -> Void) {
|
||||
self.statusBar.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
|
||||
self.containerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
|
||||
self.containerNode.layer.animateScale(from: 1.0, to: 1.04, duration: 0.3, removeOnCompletion: false, completion: { _ in
|
||||
completion()
|
||||
})
|
||||
}
|
||||
|
||||
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
self.validLayout = (layout, navigationBarHeight)
|
||||
|
||||
transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
|
||||
if let keyPreviewNode = self.keyPreviewNode {
|
||||
transition.updateFrame(node: keyPreviewNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
keyPreviewNode.updateLayout(size: layout.size, transition: .immediate)
|
||||
}
|
||||
|
||||
transition.updateFrame(node: self.imageNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
let arguments = TransformImageArguments(corners: ImageCorners(), imageSize: CGSize(width: 640.0, height: 640.0).aspectFilled(layout.size), boundingSize: layout.size, intrinsicInsets: UIEdgeInsets())
|
||||
let apply = self.imageNode.asyncLayout()(arguments)
|
||||
apply()
|
||||
|
||||
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.backButtonNode, frame: CGRect(origin: CGPoint(x: 29.0, y: 31.0), size: backSize))
|
||||
|
||||
let statusOffset: CGFloat
|
||||
if layout.metrics.widthClass == .regular && layout.metrics.heightClass == .regular {
|
||||
if layout.size.height.isEqual(to: 1366.0) {
|
||||
statusOffset = 160.0
|
||||
} else {
|
||||
statusOffset = 120.0
|
||||
}
|
||||
} else {
|
||||
if layout.size.height.isEqual(to: 736.0) {
|
||||
statusOffset = 80.0
|
||||
} else if layout.size.width.isEqual(to: 320.0) {
|
||||
statusOffset = 60.0
|
||||
} else {
|
||||
statusOffset = 64.0
|
||||
}
|
||||
}
|
||||
|
||||
let buttonsHeight: CGFloat = 75.0
|
||||
let buttonsOffset: CGFloat
|
||||
if layout.size.width.isEqual(to: 320.0) {
|
||||
if layout.size.height.isEqual(to: 480.0) {
|
||||
buttonsOffset = 53.0
|
||||
} else {
|
||||
buttonsOffset = 63.0
|
||||
}
|
||||
} else {
|
||||
buttonsOffset = 83.0
|
||||
}
|
||||
|
||||
let statusHeight = self.statusNode.updateLayout(constrainedWidth: layout.size.width, transition: transition)
|
||||
transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(x: 0.0, y: statusOffset), size: CGSize(width: layout.size.width, height: statusHeight)))
|
||||
|
||||
self.buttonsNode.updateLayout(constrainedWidth: layout.size.width, transition: transition)
|
||||
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))
|
||||
}
|
||||
|
||||
@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
|
||||
if let _ = self?.keyPreviewNode {
|
||||
self?.backPressed()
|
||||
}
|
||||
})
|
||||
self.containerNode.insertSubnode(keyPreviewNode, aboveSubnode: self.dimNode)
|
||||
self.keyPreviewNode = keyPreviewNode
|
||||
|
||||
if let (validLayout, _) = self.validLayout {
|
||||
keyPreviewNode.updateLayout(size: validLayout.size, transition: .immediate)
|
||||
|
||||
self.keyButtonNode.isHidden = true
|
||||
keyPreviewNode.animateIn(from: self.keyButtonNode.frame, fromNode: self.keyButtonNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc func backPressed() {
|
||||
if let keyPreviewNode = self.keyPreviewNode {
|
||||
self.keyPreviewNode = nil
|
||||
keyPreviewNode.animateOut(to: self.keyButtonNode.frame, toNode: self.keyButtonNode, completion: { [weak self, weak keyPreviewNode] in
|
||||
self?.keyButtonNode.isHidden = false
|
||||
keyPreviewNode?.removeFromSupernode()
|
||||
})
|
||||
} else {
|
||||
self.back?()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func panGesture(_ recognizer: UIPanGestureRecognizer) {
|
||||
switch recognizer.state {
|
||||
case .changed:
|
||||
let offset = recognizer.translation(in: self.view).y
|
||||
var bounds = self.bounds
|
||||
bounds.origin.y = -offset
|
||||
self.bounds = bounds
|
||||
case .ended:
|
||||
let velocity = recognizer.velocity(in: self.view).y
|
||||
if abs(velocity) < 100.0 {
|
||||
var bounds = self.bounds
|
||||
let previous = bounds
|
||||
bounds.origin = CGPoint()
|
||||
self.bounds = bounds
|
||||
self.layer.animateBounds(from: previous, to: bounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
} else {
|
||||
var bounds = self.bounds
|
||||
let previous = bounds
|
||||
bounds.origin = CGPoint(x: 0.0, y: velocity > 0.0 ? -bounds.height: bounds.height)
|
||||
self.bounds = bounds
|
||||
self.layer.animateBounds(from: previous, to: bounds, duration: 0.15, timingFunction: kCAMediaTimingFunctionEaseOut, completion: { [weak self] _ in
|
||||
self?.disissedInteractively?()
|
||||
})
|
||||
}
|
||||
case .cancelled:
|
||||
var bounds = self.bounds
|
||||
let previous = bounds
|
||||
bounds.origin = CGPoint()
|
||||
self.bounds = bounds
|
||||
self.layer.animateBounds(from: previous, to: bounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
130
TelegramUI/CallControllerStatusNode.swift
Normal file
@ -0,0 +1,130 @@
|
||||
import Foundation
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
|
||||
private let compactNameFont = Font.light(28.0)
|
||||
private let regularNameFont = Font.light(36.0)
|
||||
|
||||
private let compactStatusFont = Font.regular(18.0)
|
||||
private let regularStatusFont = Font.regular(18.0)
|
||||
|
||||
enum CallControllerStatusValue: Equatable {
|
||||
case text(String)
|
||||
case timer((String) -> String, Double)
|
||||
|
||||
static func ==(lhs: CallControllerStatusValue, rhs: CallControllerStatusValue) -> Bool {
|
||||
switch lhs {
|
||||
case let .text(text):
|
||||
if case .text(text) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .timer(_, referenceTime):
|
||||
if case .timer(_, referenceTime) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class CallControllerStatusNode: ASDisplayNode {
|
||||
private let titleNode: TextNode
|
||||
private let statusNode: TextNode
|
||||
private let statusMeasureNode: TextNode
|
||||
|
||||
var title: String = ""
|
||||
var status: CallControllerStatusValue = .text("") {
|
||||
didSet {
|
||||
if self.status != oldValue {
|
||||
self.statusTimer?.invalidate()
|
||||
|
||||
if case .timer = self.status {
|
||||
self.statusTimer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in
|
||||
if let strongSelf = self, let validLayoutWidth = strongSelf.validLayoutWidth {
|
||||
let _ = strongSelf.updateLayout(constrainedWidth: validLayoutWidth, transition: .immediate)
|
||||
}
|
||||
}, queue: Queue.mainQueue())
|
||||
self.statusTimer?.start()
|
||||
} else {
|
||||
if let validLayoutWidth = self.validLayoutWidth {
|
||||
let _ = self.updateLayout(constrainedWidth: validLayoutWidth, transition: .immediate)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var statusTimer: SwiftSignalKit.Timer?
|
||||
private var validLayoutWidth: CGFloat?
|
||||
|
||||
override init() {
|
||||
self.titleNode = TextNode()
|
||||
self.statusNode = TextNode()
|
||||
self.statusNode.displaysAsynchronously = false
|
||||
self.statusMeasureNode = TextNode()
|
||||
|
||||
super.init()
|
||||
|
||||
self.isUserInteractionEnabled = false
|
||||
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.statusNode)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.statusTimer?.invalidate()
|
||||
}
|
||||
|
||||
func updateLayout(constrainedWidth: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
|
||||
self.validLayoutWidth = constrainedWidth
|
||||
|
||||
let nameFont: UIFont
|
||||
let statusFont: UIFont
|
||||
if constrainedWidth < 330.0 {
|
||||
nameFont = compactNameFont
|
||||
statusFont = compactStatusFont
|
||||
} else {
|
||||
nameFont = regularNameFont
|
||||
statusFont = regularStatusFont
|
||||
}
|
||||
|
||||
let statusText: String
|
||||
let statusMeasureText: String
|
||||
switch self.status {
|
||||
case let .text(text):
|
||||
statusText = text
|
||||
statusMeasureText = text
|
||||
case let .timer(format, referenceTime):
|
||||
let duration = Int32(CFAbsoluteTimeGetCurrent() - referenceTime)
|
||||
let durationString: String
|
||||
let measureDurationString: String
|
||||
if duration > 60 * 60 {
|
||||
durationString = String(format: "%02d:%02d:%02d", arguments: [duration / 3600, (duration / 60) % 60, duration % 60])
|
||||
measureDurationString = "00:00:00"
|
||||
} else {
|
||||
durationString = String(format: "%02d:%02d", arguments: [(duration / 60) % 60, duration % 60])
|
||||
measureDurationString = "00:00"
|
||||
}
|
||||
statusText = format(durationString)
|
||||
statusMeasureText = format(measureDurationString)
|
||||
}
|
||||
|
||||
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 _ = titleApply()
|
||||
let _ = statusApply()
|
||||
let _ = statusMeasureApply()
|
||||
|
||||
self.titleNode.frame = CGRect(origin: CGPoint(x: floor((constrainedWidth - titleLayout.size.width) / 2.0), y: 0.0), size: titleLayout.size)
|
||||
self.statusNode.frame = CGRect(origin: CGPoint(x: floor((constrainedWidth - statusMeasureLayout.size.width) / 2.0), y: titleLayout.size.height + spacing), size: statusLayout.size)
|
||||
|
||||
return titleLayout.size.height + spacing + statusLayout.size.height
|
||||
}
|
||||
}
|
||||
229
TelegramUI/CallKitIntergation.swift
Normal file
@ -0,0 +1,229 @@
|
||||
import Foundation
|
||||
import CallKit
|
||||
import AVFoundation
|
||||
import Postbox
|
||||
import SwiftSignalKit
|
||||
|
||||
final class CallKitIntegration {
|
||||
private let providerDelegate: AnyObject
|
||||
|
||||
private let audioSessionActivePromise = ValuePromise<Bool>(false, ignoreRepeated: true)
|
||||
var audioSessionActive: Signal<Bool, NoError> {
|
||||
return self.audioSessionActivePromise.get()
|
||||
}
|
||||
|
||||
init?(startCall: @escaping (UUID, String) -> Signal<Bool, NoError>, answerCall: @escaping (UUID) -> Void, endCall: @escaping (UUID) -> Signal<Bool, NoError>, audioSessionActivationChanged: @escaping (Bool) -> Void) {
|
||||
#if (arch(i386) || arch(x86_64)) && os(iOS)
|
||||
return nil
|
||||
#else
|
||||
if #available(iOSApplicationExtension 10.0, *) {
|
||||
self.providerDelegate = CallKitProviderDelegate(audioSessionActivePromise: self.audioSessionActivePromise, startCall: startCall, answerCall: answerCall, endCall: endCall, audioSessionActivationChanged: audioSessionActivationChanged)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
func startCall(peerId: PeerId, displayTitle: String) {
|
||||
if #available(iOSApplicationExtension 10.0, *) {
|
||||
(self.providerDelegate as! CallKitProviderDelegate).startCall(peerId: peerId, displayTitle: displayTitle)
|
||||
}
|
||||
}
|
||||
|
||||
func answerCall(uuid: UUID) {
|
||||
if #available(iOSApplicationExtension 10.0, *) {
|
||||
(self.providerDelegate as! CallKitProviderDelegate).answerCall(uuid: uuid)
|
||||
}
|
||||
}
|
||||
|
||||
func dropCall(uuid: UUID) {
|
||||
if #available(iOSApplicationExtension 10.0, *) {
|
||||
(self.providerDelegate as! CallKitProviderDelegate).dropCall(uuid: uuid)
|
||||
}
|
||||
}
|
||||
|
||||
func reportIncomingCall(uuid: UUID, handle: String, displayTitle: String, completion: ((NSError?) -> Void)?) {
|
||||
if #available(iOSApplicationExtension 10.0, *) {
|
||||
(self.providerDelegate as! CallKitProviderDelegate).reportIncomingCall(uuid: uuid, handle: handle, displayTitle: displayTitle, completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
func reportOutgoingCallConnected(uuid: UUID, at date: Date) {
|
||||
if #available(iOSApplicationExtension 10.0, *) {
|
||||
(self.providerDelegate as! CallKitProviderDelegate).reportOutgoingCallConnected(uuid: uuid, at: date)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOSApplicationExtension 10.0, *)
|
||||
class CallKitProviderDelegate: NSObject, CXProviderDelegate {
|
||||
private let provider: CXProvider
|
||||
private let callController = CXCallController()
|
||||
|
||||
private let startCall: (UUID, String) -> Signal<Bool, NoError>
|
||||
private let answerCall: (UUID) -> Void
|
||||
private let endCall: (UUID) -> Signal<Bool, NoError>
|
||||
private let audioSessionActivationChanged: (Bool) -> Void
|
||||
|
||||
private let disposableSet = DisposableSet()
|
||||
|
||||
fileprivate let audioSessionActivePromise: ValuePromise<Bool>
|
||||
|
||||
init(audioSessionActivePromise: ValuePromise<Bool>, startCall: @escaping (UUID, String) -> Signal<Bool, NoError>, answerCall: @escaping (UUID) -> Void, endCall: @escaping (UUID) -> Signal<Bool, NoError>, audioSessionActivationChanged: @escaping (Bool) -> Void) {
|
||||
self.audioSessionActivePromise = audioSessionActivePromise
|
||||
self.startCall = startCall
|
||||
self.answerCall = answerCall
|
||||
self.endCall = endCall
|
||||
self.audioSessionActivationChanged = audioSessionActivationChanged
|
||||
|
||||
self.provider = CXProvider(configuration: CallKitProviderDelegate.providerConfiguration)
|
||||
|
||||
super.init()
|
||||
|
||||
self.provider.setDelegate(self, queue: nil)
|
||||
}
|
||||
|
||||
static var providerConfiguration: CXProviderConfiguration {
|
||||
let providerConfiguration = CXProviderConfiguration(localizedName: "Telegram")
|
||||
|
||||
providerConfiguration.supportsVideo = false
|
||||
providerConfiguration.maximumCallsPerCallGroup = 1
|
||||
providerConfiguration.maximumCallGroups = 1
|
||||
providerConfiguration.supportedHandleTypes = [.phoneNumber, .generic]
|
||||
if let image = UIImage(bundleImageName: "Call/CallKitLogo") {
|
||||
providerConfiguration.iconTemplateImageData = UIImagePNGRepresentation(image)
|
||||
}
|
||||
|
||||
return providerConfiguration
|
||||
}
|
||||
|
||||
private func requestTransaction(_ transaction: CXTransaction, completion: ((Bool) -> Void)? = nil) {
|
||||
self.callController.request(transaction) { error in
|
||||
if let error = error {
|
||||
print("Error requesting transaction: \(error)")
|
||||
}
|
||||
completion?(error == nil)
|
||||
}
|
||||
}
|
||||
|
||||
func endCall(uuid: UUID) {
|
||||
let endCallAction = CXEndCallAction(call: uuid)
|
||||
let transaction = CXTransaction(action: endCallAction)
|
||||
self.requestTransaction(transaction)
|
||||
}
|
||||
|
||||
func dropCall(uuid: UUID) {
|
||||
self.provider.reportCall(with: uuid, endedAt: nil, reason: CXCallEndedReason.remoteEnded)
|
||||
}
|
||||
|
||||
func answerCall(uuid: UUID) {
|
||||
|
||||
}
|
||||
|
||||
func startCall(peerId: PeerId, displayTitle: String) {
|
||||
let uuid = UUID()
|
||||
let handle = CXHandle(type: .generic, value: "\(peerId.id)")
|
||||
let startCallAction = CXStartCallAction(call: uuid, handle: handle)
|
||||
startCallAction.contactIdentifier = displayTitle
|
||||
|
||||
startCallAction.isVideo = false
|
||||
let transaction = CXTransaction(action: startCallAction)
|
||||
|
||||
self.requestTransaction(transaction, completion: { _ in
|
||||
let update = CXCallUpdate()
|
||||
update.remoteHandle = handle
|
||||
update.localizedCallerName = displayTitle
|
||||
update.supportsHolding = false
|
||||
update.supportsGrouping = false
|
||||
update.supportsUngrouping = false
|
||||
update.supportsDTMF = false
|
||||
|
||||
self.provider.reportCall(with: uuid, updated: update)
|
||||
})
|
||||
}
|
||||
|
||||
func reportIncomingCall(uuid: UUID, handle: String, displayTitle: String, completion: ((NSError?) -> Void)?) {
|
||||
let update = CXCallUpdate()
|
||||
update.remoteHandle = CXHandle(type: .generic, value: handle)
|
||||
update.localizedCallerName = displayTitle
|
||||
update.supportsHolding = false
|
||||
update.supportsGrouping = false
|
||||
update.supportsUngrouping = false
|
||||
update.supportsDTMF = false
|
||||
|
||||
self.provider.reportNewIncomingCall(with: uuid, update: update, completion: { error in
|
||||
completion?(error as NSError?)
|
||||
})
|
||||
}
|
||||
|
||||
func reportOutgoingCallConnecting(uuid: UUID, at date: Date) {
|
||||
self.provider.reportOutgoingCall(with: uuid, startedConnectingAt: date)
|
||||
}
|
||||
|
||||
func reportOutgoingCallConnected(uuid: UUID, at date: Date) {
|
||||
self.provider.reportOutgoingCall(with: uuid, connectedAt: date)
|
||||
}
|
||||
|
||||
func providerDidReset(_ provider: CXProvider) {
|
||||
/*stopAudio()
|
||||
|
||||
for call in callManager.calls {
|
||||
call.end()
|
||||
}
|
||||
|
||||
callManager.removeAllCalls()*/
|
||||
}
|
||||
|
||||
func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
|
||||
let disposable = MetaDisposable()
|
||||
self.disposableSet.add(disposable)
|
||||
disposable.set((self.startCall(action.callUUID, action.handle.value)
|
||||
|> deliverOnMainQueue
|
||||
|> afterDisposed { [weak self, weak disposable] in
|
||||
if let strongSelf = self, let disposable = disposable {
|
||||
strongSelf.disposableSet.remove(disposable)
|
||||
}
|
||||
}).start(next: { result in
|
||||
if result {
|
||||
action.fulfill()
|
||||
} else {
|
||||
action.fail()
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
|
||||
self.answerCall(action.callUUID)
|
||||
|
||||
action.fulfill()
|
||||
}
|
||||
|
||||
func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
|
||||
let disposable = MetaDisposable()
|
||||
self.disposableSet.add(disposable)
|
||||
disposable.set((self.endCall(action.callUUID)
|
||||
|> deliverOnMainQueue
|
||||
|> afterDisposed { [weak self, weak disposable] in
|
||||
if let strongSelf = self, let disposable = disposable {
|
||||
strongSelf.disposableSet.remove(disposable)
|
||||
}
|
||||
}).start(next: { result in
|
||||
if result {
|
||||
action.fulfill(withDateEnded: Date())
|
||||
} else {
|
||||
action.fail()
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
|
||||
self.audioSessionActivationChanged(true)
|
||||
self.audioSessionActivePromise.set(true)
|
||||
}
|
||||
|
||||
func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {
|
||||
self.audioSessionActivationChanged(false)
|
||||
self.audioSessionActivePromise.set(false)
|
||||
}
|
||||
}
|
||||
|
||||
536
TelegramUI/CallListCallItem.swift
Normal file
@ -0,0 +1,536 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Postbox
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
|
||||
private let titleFont = Font.regular(17.0)
|
||||
private let statusFont = Font.regular(14.0)
|
||||
private let dateFont = Font.regular(15.0)
|
||||
|
||||
private func callDurationString(strings: PresentationStrings, duration: Int32) -> String {
|
||||
if duration < 60 {
|
||||
return strings.Call_ShortSeconds(duration)
|
||||
} else {
|
||||
return strings.Call_ShortMinutes(duration / 60)
|
||||
}
|
||||
}
|
||||
|
||||
class CallListCallItem: ListViewItem {
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let account: Account
|
||||
let topMessage: Message
|
||||
let messages: [Message]
|
||||
let editing: Bool
|
||||
let revealed: Bool
|
||||
let interaction: CallListNodeInteraction
|
||||
|
||||
let selectable: Bool = true
|
||||
let headerAccessoryItem: ListViewAccessoryItem?
|
||||
let header: ListViewItemHeader?
|
||||
|
||||
init(theme: PresentationTheme, strings: PresentationStrings, account: Account, topMessage: Message, messages: [Message], editing: Bool, revealed: Bool, interaction: CallListNodeInteraction) {
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.account = account
|
||||
self.topMessage = topMessage
|
||||
self.messages = messages
|
||||
self.editing = editing
|
||||
self.revealed = revealed
|
||||
self.interaction = interaction
|
||||
|
||||
self.headerAccessoryItem = nil
|
||||
self.header = nil
|
||||
}
|
||||
|
||||
func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, () -> 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)
|
||||
node.contentSize = nodeLayout.contentSize
|
||||
node.insets = nodeLayout.insets
|
||||
|
||||
completion(node, {
|
||||
return (nil, {
|
||||
nodeApply().1(false)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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? 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)
|
||||
var animated = true
|
||||
if case .None = animation {
|
||||
animated = false
|
||||
}
|
||||
Queue.mainQueue().async {
|
||||
completion(nodeLayout, {
|
||||
apply().1(animated)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func selected(listView: ListView) {
|
||||
listView.clearHighlightAnimated(true)
|
||||
self.interaction.call(self.topMessage.id.peerId)
|
||||
}
|
||||
|
||||
static func mergeType(item: CallListCallItem, previousItem: ListViewItem?, nextItem: ListViewItem?) -> (first: Bool, last: Bool, firstWithHeader: Bool) {
|
||||
var first = false
|
||||
var last = false
|
||||
var firstWithHeader = false
|
||||
if let previousItem = previousItem {
|
||||
if let header = item.header {
|
||||
if let previousItem = previousItem as? CallListCallItem {
|
||||
firstWithHeader = header.id != previousItem.header?.id
|
||||
} else {
|
||||
firstWithHeader = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
first = true
|
||||
firstWithHeader = item.header != nil
|
||||
}
|
||||
if let nextItem = nextItem {
|
||||
if let header = item.header {
|
||||
if let nextItem = nextItem as? CallListCallItem {
|
||||
last = header.id != nextItem.header?.id
|
||||
} else {
|
||||
last = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
last = true
|
||||
}
|
||||
return (first, last, firstWithHeader)
|
||||
}
|
||||
}
|
||||
|
||||
private let separatorHeight = 1.0 / UIScreen.main.scale
|
||||
|
||||
class CallListCallItemNode: ItemListRevealOptionsItemNode {
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private let separatorNode: ASDisplayNode
|
||||
private let highlightedBackgroundNode: ASDisplayNode
|
||||
|
||||
private let avatarNode: AvatarNode
|
||||
private let titleNode: TextNode
|
||||
private let statusNode: TextNode
|
||||
private let dateNode: TextNode
|
||||
private let typeIconNode: ASImageNode
|
||||
private let infoButtonNode: HighlightableButtonNode
|
||||
|
||||
var editableControlNode: ItemListEditableControlNode?
|
||||
|
||||
private var avatarState: (Account, Peer?)?
|
||||
private var layoutParams: (CallListCallItem, CGFloat, Bool, Bool, Bool)?
|
||||
|
||||
required init() {
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.backgroundNode.isLayerBacked = true
|
||||
|
||||
self.separatorNode = ASDisplayNode()
|
||||
self.separatorNode.isLayerBacked = true
|
||||
|
||||
self.highlightedBackgroundNode = ASDisplayNode()
|
||||
self.highlightedBackgroundNode.isLayerBacked = true
|
||||
|
||||
self.avatarNode = AvatarNode(font: Font.regular(15.0))
|
||||
self.avatarNode.isLayerBacked = true
|
||||
|
||||
self.titleNode = TextNode()
|
||||
self.statusNode = TextNode()
|
||||
self.dateNode = TextNode()
|
||||
|
||||
self.typeIconNode = ASImageNode()
|
||||
self.typeIconNode.isLayerBacked = true
|
||||
self.typeIconNode.displayWithoutProcessing = true
|
||||
self.typeIconNode.displaysAsynchronously = false
|
||||
|
||||
self.infoButtonNode = HighlightableButtonNode()
|
||||
self.infoButtonNode.hitTestSlop = UIEdgeInsets(top: 4.0, left: 4.0, bottom: 4.0, right: 4.0)
|
||||
|
||||
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)
|
||||
self.addSubnode(self.statusNode)
|
||||
self.addSubnode(self.dateNode)
|
||||
self.addSubnode(self.infoButtonNode)
|
||||
|
||||
self.infoButtonNode.addTarget(self, action: #selector(self.infoPressed), forControlEvents: .touchUpInside)
|
||||
}
|
||||
|
||||
override func layoutForWidth(_ width: CGFloat, 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)
|
||||
let makeLayout = self.asyncLayout()
|
||||
let (nodeLayout, nodeApply) = makeLayout(item, width, 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)
|
||||
|
||||
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() -> (_ item: CallListCallItem, _ width: CGFloat, _ first: Bool, _ last: Bool, _ firstWithHeader: Bool) -> (ListViewItemNodeLayout, () -> (Signal<Void, NoError>?, (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
|
||||
var updatedTheme: PresentationTheme?
|
||||
var updatedInfoIcon = false
|
||||
|
||||
if currentItem?.theme !== item.theme {
|
||||
updatedTheme = item.theme
|
||||
|
||||
updatedInfoIcon = true
|
||||
}
|
||||
|
||||
let editingOffset: CGFloat
|
||||
var editableControlSizeAndApply: (CGSize, () -> ItemListEditableControlNode)?
|
||||
if item.editing {
|
||||
let sizeAndApply = editableControlLayout(56.0)
|
||||
editableControlSizeAndApply = sizeAndApply
|
||||
editingOffset = sizeAndApply.0.width
|
||||
} else {
|
||||
editingOffset = 0.0
|
||||
}
|
||||
|
||||
var leftInset: CGFloat = 86.0
|
||||
let rightInset: CGFloat = 13.0
|
||||
var infoIconRightInset: CGFloat = rightInset
|
||||
|
||||
var dateRightInset: CGFloat = 43.0
|
||||
if item.editing {
|
||||
leftInset += editingOffset
|
||||
dateRightInset += 5.0
|
||||
infoIconRightInset -= 36.0
|
||||
}
|
||||
|
||||
var titleAttributedString: NSAttributedString?
|
||||
var statusAttributedString: NSAttributedString?
|
||||
|
||||
var titleColor = item.theme.list.itemPrimaryTextColor
|
||||
var hasMissed = false
|
||||
var hasIncoming = false
|
||||
var hasOutgoing = false
|
||||
|
||||
var hadDuration = false
|
||||
var callDuration: Int32?
|
||||
|
||||
for message in item.messages {
|
||||
inner: for media in message.media {
|
||||
if let action = media as? TelegramMediaAction {
|
||||
if case let .phoneCall(_, discardReason, duration) = action.action {
|
||||
if message.flags.contains(.Incoming) {
|
||||
hasIncoming = true
|
||||
|
||||
if let discardReason = discardReason, case .missed = discardReason {
|
||||
titleColor = item.theme.list.itemDestructiveColor
|
||||
hasMissed = true
|
||||
}
|
||||
} else {
|
||||
hasOutgoing = true
|
||||
}
|
||||
if callDuration == nil && !hadDuration {
|
||||
hadDuration = true
|
||||
callDuration = duration
|
||||
} else {
|
||||
callDuration = nil
|
||||
}
|
||||
}
|
||||
break inner
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let peer = item.topMessage.peers[item.topMessage.id.peerId] {
|
||||
if let user = peer as? TelegramUser {
|
||||
if let firstName = user.firstName, let lastName = user.lastName, !firstName.isEmpty, !lastName.isEmpty {
|
||||
let string = NSMutableAttributedString()
|
||||
string.append(NSAttributedString(string: firstName, font: titleFont, textColor: titleColor))
|
||||
string.append(NSAttributedString(string: " ", font: titleFont, textColor: titleColor))
|
||||
string.append(NSAttributedString(string: lastName, font: titleFont, textColor: titleColor))
|
||||
if item.messages.count > 1 {
|
||||
string.append(NSAttributedString(string: " (\(item.messages.count))", font: titleFont, textColor: titleColor))
|
||||
}
|
||||
titleAttributedString = string
|
||||
} else if let firstName = user.firstName, !firstName.isEmpty {
|
||||
titleAttributedString = NSAttributedString(string: firstName, font: titleFont, textColor: titleColor)
|
||||
} else if let lastName = user.lastName, !lastName.isEmpty {
|
||||
titleAttributedString = NSAttributedString(string: lastName, font: titleFont, textColor: titleColor)
|
||||
} else {
|
||||
titleAttributedString = NSAttributedString(string: item.strings.User_DeletedAccount, font: titleFont, textColor: titleColor)
|
||||
}
|
||||
} else if let group = peer as? TelegramGroup {
|
||||
titleAttributedString = NSAttributedString(string: group.title, font: titleFont, textColor: titleColor)
|
||||
} else if let channel = peer as? TelegramChannel {
|
||||
titleAttributedString = NSAttributedString(string: channel.title, font: titleFont, textColor: titleColor)
|
||||
}
|
||||
|
||||
if hasMissed {
|
||||
statusAttributedString = NSAttributedString(string: item.strings.Calls_Missed, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor)
|
||||
} else if hasIncoming && hasOutgoing {
|
||||
statusAttributedString = NSAttributedString(string: item.strings.Notification_CallOutgoingShort + ", " + item.strings.Notification_CallIncomingShort, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor)
|
||||
} else if hasIncoming {
|
||||
if let callDuration = callDuration, callDuration != 0 {
|
||||
statusAttributedString = NSAttributedString(string: item.strings.Notification_CallTimeFormat(item.strings.Notification_CallIncomingShort, callDurationString(strings: item.strings, duration: callDuration)).0, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor)
|
||||
} else {
|
||||
statusAttributedString = NSAttributedString(string: item.strings.Notification_CallIncomingShort, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor)
|
||||
}
|
||||
} else {
|
||||
if let callDuration = callDuration, callDuration != 0 {
|
||||
statusAttributedString = NSAttributedString(string: item.strings.Notification_CallTimeFormat(item.strings.Notification_CallOutgoingShort, callDurationString(strings: item.strings, duration: callDuration)).0, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor)
|
||||
} else {
|
||||
statusAttributedString = NSAttributedString(string: item.strings.Notification_CallOutgoingShort, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var t = Int(item.topMessage.timestamp)
|
||||
var timeinfo = tm()
|
||||
localtime_r(&t, &timeinfo)
|
||||
|
||||
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
|
||||
let dateText = stringForRelativeTimestamp(strings: item.strings, relativeTimestamp: item.topMessage.timestamp, relativeTo: timestamp)
|
||||
|
||||
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 (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 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 outgoingIcon = PresentationResourcesCallList.outgoingIcon(item.theme)
|
||||
let infoIcon = PresentationResourcesCallList.infoButton(item.theme)
|
||||
|
||||
return (nodeLayout, { [weak self] in
|
||||
if let strongSelf = self {
|
||||
if let peer = item.topMessage.peers[item.topMessage.id.peerId] {
|
||||
strongSelf.avatarNode.setPeer(account: item.account, peer: peer)
|
||||
}
|
||||
|
||||
return (strongSelf.avatarNode.ready, { [weak strongSelf] animated in
|
||||
if let strongSelf = strongSelf {
|
||||
strongSelf.layoutParams = (item, width, first, last, firstWithHeader)
|
||||
|
||||
let revealOffset = strongSelf.revealOffset
|
||||
|
||||
let transition: ContainedViewLayoutTransition
|
||||
if animated {
|
||||
transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
|
||||
} else {
|
||||
transition = .immediate
|
||||
}
|
||||
|
||||
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 editableControlSizeAndApply = editableControlSizeAndApply {
|
||||
if strongSelf.editableControlNode == nil {
|
||||
let editableControlNode = editableControlSizeAndApply.1()
|
||||
editableControlNode.tapped = {
|
||||
if let strongSelf = self {
|
||||
strongSelf.setRevealOptionsOpened(true, animated: true)
|
||||
strongSelf.revealOptionsInteractivelyOpened()
|
||||
}
|
||||
}
|
||||
strongSelf.editableControlNode = editableControlNode
|
||||
strongSelf.addSubnode(editableControlNode)
|
||||
let editableControlFrame = CGRect(origin: CGPoint(x: 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))
|
||||
editableControlNode.alpha = 0.0
|
||||
transition.updateAlpha(node: editableControlNode, alpha: 1.0)
|
||||
}
|
||||
} else if let editableControlNode = strongSelf.editableControlNode {
|
||||
var editableControlFrame = editableControlNode.frame
|
||||
editableControlFrame.origin.x = -editableControlFrame.size.width
|
||||
strongSelf.editableControlNode = nil
|
||||
transition.updateAlpha(node: editableControlNode, alpha: 0.0)
|
||||
transition.updateFrame(node: editableControlNode, frame: editableControlFrame, completion: { [weak editableControlNode] _ in
|
||||
editableControlNode?.removeFromSupernode()
|
||||
})
|
||||
}
|
||||
|
||||
transition.updateFrame(node: strongSelf.avatarNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset - 52.0, y: 8.0), size: CGSize(width: 40.0, height: 40.0)))
|
||||
|
||||
let _ = titleApply()
|
||||
transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset, y: 8.0), size: titleLayout.size))
|
||||
|
||||
let _ = statusApply()
|
||||
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))
|
||||
|
||||
if let outgoingIcon = outgoingIcon {
|
||||
if strongSelf.typeIconNode.image !== outgoingIcon {
|
||||
strongSelf.typeIconNode.image = outgoingIcon
|
||||
}
|
||||
transition.updateFrame(node: strongSelf.typeIconNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset - 76.0, y: floor((nodeLayout.contentSize.height - outgoingIcon.size.height) / 2.0)), size: outgoingIcon.size))
|
||||
}
|
||||
strongSelf.typeIconNode.isHidden = !hasOutgoing
|
||||
|
||||
if let infoIcon = infoIcon {
|
||||
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.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))])
|
||||
}
|
||||
})
|
||||
} else {
|
||||
return (nil, { _ in })
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override func layoutHeaderAccessoryItemNode(_ accessoryItemNode: ListViewAccessoryItemNode) {
|
||||
let bounds = self.bounds
|
||||
accessoryItemNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -29.0), size: CGSize(width: bounds.size.width, height: 29.0))
|
||||
}
|
||||
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5)
|
||||
}
|
||||
|
||||
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.3, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
override public func header() -> ListViewItemHeader? {
|
||||
if let (item, _, _, _, _) = self.layoutParams {
|
||||
return item.header
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@objc func infoPressed() {
|
||||
if let item = self.layoutParams?.0 {
|
||||
item.interaction.openInfo(item.topMessage.id.peerId)
|
||||
}
|
||||
}
|
||||
|
||||
override func revealOptionsInteractivelyOpened() {
|
||||
if let item = self.layoutParams?.0 {
|
||||
item.interaction.setMessageIdWithRevealedOptions(item.topMessage.id, nil)
|
||||
}
|
||||
}
|
||||
|
||||
override func revealOptionsInteractivelyClosed() {
|
||||
if let item = self.layoutParams?.0 {
|
||||
item.interaction.setMessageIdWithRevealedOptions(nil, item.topMessage.id)
|
||||
}
|
||||
}
|
||||
|
||||
override func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
super.updateRevealOffset(offset: offset, transition: transition)
|
||||
|
||||
if let item = self.layoutParams?.0 {
|
||||
let revealOffset = offset
|
||||
|
||||
let editingOffset: CGFloat
|
||||
if let editableControlNode = self.editableControlNode {
|
||||
editingOffset = editableControlNode.bounds.size.width
|
||||
var editableControlFrame = editableControlNode.frame
|
||||
editableControlFrame.origin.x = offset
|
||||
transition.updateFrame(node: editableControlNode, frame: editableControlFrame)
|
||||
} else {
|
||||
editingOffset = 0.0
|
||||
}
|
||||
|
||||
let leftInset: CGFloat = 86.0 + editingOffset
|
||||
let rightInset: CGFloat = 13.0
|
||||
var infoIconRightInset: CGFloat = rightInset
|
||||
|
||||
var dateRightInset: CGFloat = 43.0
|
||||
if item.editing {
|
||||
dateRightInset += 5.0
|
||||
infoIconRightInset -= 36.0
|
||||
}
|
||||
|
||||
transition.updateFrame(node: self.avatarNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset - 52.0, y: 8.0), size: CGSize(width: 40.0, height: 40.0)))
|
||||
|
||||
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset, y: 8.0), size: self.titleNode.bounds.size))
|
||||
|
||||
transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset, y: 30.0), size: self.statusNode.bounds.size))
|
||||
|
||||
transition.updateFrame(node: self.dateNode, frame: CGRect(origin: CGPoint(x: editingOffset + revealOffset + self.bounds.size.width - dateRightInset - self.dateNode.bounds.size.width, y: self.dateNode.frame.minY), size: self.dateNode.bounds.size))
|
||||
|
||||
transition.updateFrame(node: self.typeIconNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset - 76.0, y: self.typeIconNode.frame.minY), size: self.typeIconNode.bounds.size))
|
||||
|
||||
transition.updateFrame(node: self.infoButtonNode, frame: CGRect(origin: CGPoint(x: revealOffset + self.bounds.size.width - infoIconRightInset - self.infoButtonNode.bounds.width, y: self.infoButtonNode.frame.minY), size: self.infoButtonNode.bounds.size))
|
||||
}
|
||||
}
|
||||
|
||||
override func revealOptionSelected(_ option: ItemListRevealOption) {
|
||||
self.setRevealOptionsOpened(false, animated: true)
|
||||
self.revealOptionsInteractivelyClosed()
|
||||
if let item = self.layoutParams?.0 {
|
||||
item.interaction.delete(item.messages.map { $0.id })
|
||||
}
|
||||
}
|
||||
}
|
||||
196
TelegramUI/CallListController.swift
Normal file
@ -0,0 +1,196 @@
|
||||
import Foundation
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
|
||||
public final class CallListController: ViewController {
|
||||
private var controllerNode: CallListControllerNode {
|
||||
return self.displayNode as! CallListControllerNode
|
||||
}
|
||||
|
||||
private let _ready = Promise<Bool>(false)
|
||||
override public var ready: Promise<Bool> {
|
||||
return self._ready
|
||||
}
|
||||
|
||||
private let account: Account
|
||||
|
||||
private var presentationData: PresentationData
|
||||
private var presentationDataDisposable: Disposable?
|
||||
|
||||
private let segmentedTitleView: ItemListControllerSegmentedTitleView
|
||||
|
||||
private let createActionDisposable = MetaDisposable()
|
||||
|
||||
public init(account: Account) {
|
||||
self.account = account
|
||||
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)
|
||||
|
||||
super.init(navigationBarTheme: NavigationBarTheme(rootControllerTheme: self.presentationData.theme))
|
||||
|
||||
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style
|
||||
|
||||
self.navigationItem.titleView = self.segmentedTitleView
|
||||
self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationCallIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.callPressed))
|
||||
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.tabBarItem.image = UIImage(bundleImageName: "Chat List/Tabs/IconCalls")
|
||||
self.tabBarItem.selectedImage = UIImage(bundleImageName: "Chat List/Tabs/IconCallsSelected")
|
||||
|
||||
self.segmentedTitleView.indexUpdated = { [weak self] index in
|
||||
if let strongSelf = self {
|
||||
strongSelf.controllerNode.updateType(index == 0 ? .all : .missed)
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
self.scrollToTop = { [weak self] in
|
||||
self?.controllerNode.scrollToLatest()
|
||||
}
|
||||
}
|
||||
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.createActionDisposable.dispose()
|
||||
self.presentationDataDisposable?.dispose()
|
||||
}
|
||||
|
||||
private func updateThemeAndStrings() {
|
||||
self.segmentedTitleView.segments = [self.presentationData.strings.Calls_All, self.presentationData.strings.Calls_Missed]
|
||||
self.segmentedTitleView.color = self.presentationData.theme.rootController.navigationBar.accentTextColor
|
||||
|
||||
self.tabBarItem.title = self.presentationData.strings.Calls_TabTitle
|
||||
self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationCallIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.callPressed))
|
||||
self.navigationItem.leftBarButtonItem = 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))
|
||||
|
||||
if self.isNodeLoaded {
|
||||
self.controllerNode.updateThemeAndStrings(theme: self.presentationData.theme, strings: self.presentationData.strings)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override public func loadDisplayNode() {
|
||||
self.displayNode = CallListControllerNode(account: self.account, presentationData: self.presentationData, call: { [weak self] peerId in
|
||||
if let strongSelf = self {
|
||||
strongSelf.call(peerId)
|
||||
}
|
||||
}, openInfo: { [weak self] peerId in
|
||||
if let strongSelf = self {
|
||||
let _ = (strongSelf.account.postbox.loadedPeerWithId(peerId)
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { peer in
|
||||
if let strongSelf = self {
|
||||
if let infoController = peerInfoController(account: strongSelf.account, peer: peer) {
|
||||
(strongSelf.navigationController as? NavigationController)?.pushViewController(infoController)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
self._ready.set(self.controllerNode.ready)
|
||||
self.displayNodeDidLoad()
|
||||
}
|
||||
|
||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
|
||||
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition)
|
||||
}
|
||||
|
||||
@objc func callPressed() {
|
||||
let controller = ContactSelectionController(account: self.account, title: { $0.Calls_NewCall })
|
||||
self.createActionDisposable.set((controller.result
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak controller, weak self] peerId in
|
||||
controller?.dismissSearch()
|
||||
if let strongSelf = self, let peerId = peerId {
|
||||
strongSelf.call(peerId, began: {
|
||||
if let strongSelf = self {
|
||||
if let hasOngoingCall = strongSelf.account.telegramApplicationContext.hasOngoingCall {
|
||||
let _ = (hasOngoingCall
|
||||
|> filter { $0 }
|
||||
|> timeout(1.0, queue: Queue.mainQueue(), alternate: .single(true))
|
||||
|> delay(0.5, queue: Queue.mainQueue())
|
||||
|> deliverOnMainQueue).start(next: { _ in
|
||||
if let strongSelf = self, let controller = controller, let navigationController = controller.navigationController as? NavigationController {
|
||||
if navigationController.viewControllers.last === controller {
|
||||
let _ = navigationController.popViewController(animated: true)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}))
|
||||
(self.navigationController as? NavigationController)?.pushViewController(controller)
|
||||
}
|
||||
|
||||
@objc func editPressed() {
|
||||
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed))
|
||||
|
||||
self.controllerNode.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))
|
||||
|
||||
self.controllerNode.updateState { state in
|
||||
return state.withUpdatedEditing(false)
|
||||
}
|
||||
}
|
||||
|
||||
private func call(_ peerId: PeerId, began: (() -> Void)? = nil) {
|
||||
let callResult = self.account.telegramApplicationContext.callManager?.requestCall(peerId: peerId, endCurrentIfAny: false)
|
||||
if let callResult = callResult {
|
||||
if case let .alreadyInProgress(currentPeerId) = callResult {
|
||||
if currentPeerId == peerId {
|
||||
began?()
|
||||
self.account.telegramApplicationContext.navigateToCurrentCall?()
|
||||
} else {
|
||||
let presentationData = self.presentationData
|
||||
let _ = (self.account.postbox.modify { modifier -> (Peer?, Peer?) in
|
||||
return (modifier.getPeer(peerId), modifier.getPeer(currentPeerId))
|
||||
} |> deliverOnMainQueue).start(next: { [weak self] peer, current in
|
||||
if let strongSelf = self, let peer = peer, let current = current {
|
||||
strongSelf.present(standardTextAlertController(title: presentationData.strings.Call_CallInProgressTitle, text: presentationData.strings.Call_CallInProgressMessage(current.compactDisplayTitle, peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {
|
||||
if let strongSelf = self {
|
||||
let _ = strongSelf.account.telegramApplicationContext.callManager?.requestCall(peerId: peerId, endCurrentIfAny: true)
|
||||
began?()
|
||||
}
|
||||
})]), in: .window)
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
began?()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
417
TelegramUI/CallListControllerNode.swift
Normal file
@ -0,0 +1,417 @@
|
||||
import Foundation
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
|
||||
private struct CallListNodeListViewTransition {
|
||||
let callListView: CallListNodeView
|
||||
let deleteItems: [ListViewDeleteItem]
|
||||
let insertItems: [ListViewInsertItem]
|
||||
let updateItems: [ListViewUpdateItem]
|
||||
let options: ListViewDeleteAndInsertOptions
|
||||
let scrollToItem: ListViewScrollToItem?
|
||||
let stationaryItemRange: (Int, Int)?
|
||||
}
|
||||
|
||||
private extension CallListViewEntry {
|
||||
var lowestIndex: MessageIndex {
|
||||
switch self {
|
||||
case let .hole(index):
|
||||
return index
|
||||
case let .message(_, messages):
|
||||
var lowest = MessageIndex(messages[0])
|
||||
for i in 1 ..< messages.count {
|
||||
let index = MessageIndex(messages[i])
|
||||
if index < lowest {
|
||||
lowest = index
|
||||
}
|
||||
}
|
||||
return lowest
|
||||
}
|
||||
}
|
||||
|
||||
var highestIndex: MessageIndex {
|
||||
switch self {
|
||||
case let .hole(index):
|
||||
return index
|
||||
case let .message(_, messages):
|
||||
var highest = MessageIndex(messages[0])
|
||||
for i in 1 ..< messages.count {
|
||||
let index = MessageIndex(messages[i])
|
||||
if index > highest {
|
||||
highest = index
|
||||
}
|
||||
}
|
||||
return highest
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class CallListNodeInteraction {
|
||||
let setMessageIdWithRevealedOptions: (MessageId?, MessageId?) -> Void
|
||||
let call: (PeerId) -> Void
|
||||
let openInfo: (PeerId) -> Void
|
||||
let delete: ([MessageId]) -> Void
|
||||
|
||||
init(setMessageIdWithRevealedOptions: @escaping (MessageId?, MessageId?) -> Void, call: @escaping (PeerId) -> Void, openInfo: @escaping (PeerId) -> Void, delete: @escaping ([MessageId]) -> Void) {
|
||||
self.setMessageIdWithRevealedOptions = setMessageIdWithRevealedOptions
|
||||
self.call = call
|
||||
self.openInfo = openInfo
|
||||
self.delete = delete
|
||||
}
|
||||
}
|
||||
|
||||
struct CallListNodeState: Equatable {
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let editing: Bool
|
||||
let messageIdWithRevealedOptions: MessageId?
|
||||
|
||||
func withUpdatedPresentationData(theme: PresentationTheme, strings: PresentationStrings) -> CallListNodeState {
|
||||
return CallListNodeState(theme: theme, strings: strings, editing: self.editing, messageIdWithRevealedOptions: self.messageIdWithRevealedOptions)
|
||||
}
|
||||
|
||||
func withUpdatedEditing(_ editing: Bool) -> CallListNodeState {
|
||||
return CallListNodeState(theme: self.theme, strings: self.strings, editing: editing, messageIdWithRevealedOptions: self.messageIdWithRevealedOptions)
|
||||
}
|
||||
|
||||
func withUpdatedMessageIdWithRevealedOptions(_ messageIdWithRevealedOptions: MessageId?) -> CallListNodeState {
|
||||
return CallListNodeState(theme: self.theme, strings: self.strings, editing: self.editing, messageIdWithRevealedOptions: messageIdWithRevealedOptions)
|
||||
}
|
||||
|
||||
static func ==(lhs: CallListNodeState, rhs: CallListNodeState) -> Bool {
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.strings !== rhs.strings {
|
||||
return false
|
||||
}
|
||||
if lhs.editing != rhs.editing {
|
||||
return false
|
||||
}
|
||||
if lhs.messageIdWithRevealedOptions != rhs.messageIdWithRevealedOptions {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private func mappedInsertEntries(account: Account, nodeInteraction: CallListNodeInteraction, entries: [CallListNodeViewTransitionInsertEntry]) -> [ListViewInsertItem] {
|
||||
return entries.map { entry -> ListViewInsertItem in
|
||||
switch entry.entry {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func mappedUpdateEntries(account: Account, nodeInteraction: CallListNodeInteraction, entries: [CallListNodeViewTransitionUpdateEntry]) -> [ListViewUpdateItem] {
|
||||
return entries.map { entry -> ListViewUpdateItem in
|
||||
switch entry.entry {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 final class CallListOpaqueTransactionState {
|
||||
let callListView: CallListNodeView
|
||||
|
||||
init(callListView: CallListNodeView) {
|
||||
self.callListView = callListView
|
||||
}
|
||||
}
|
||||
|
||||
final class CallListControllerNode: ASDisplayNode {
|
||||
private let account: Account
|
||||
private var presentationData: PresentationData
|
||||
|
||||
private var containerLayout: (ContainerViewLayout, CGFloat)?
|
||||
|
||||
private let _ready = ValuePromise<Bool>()
|
||||
private var didSetReady = false
|
||||
var ready: Signal<Bool, NoError> {
|
||||
return _ready.get()
|
||||
}
|
||||
|
||||
var peerSelected: ((PeerId) -> Void)?
|
||||
var activateSearch: (() -> Void)?
|
||||
var deletePeerChat: ((PeerId) -> Void)?
|
||||
|
||||
private let viewProcessingQueue = Queue()
|
||||
private var callListView: CallListNodeView?
|
||||
|
||||
private var dequeuedInitialTransitionOnLayout = false
|
||||
private var enqueuedTransition: (CallListNodeListViewTransition, () -> Void)?
|
||||
|
||||
private var currentState: CallListNodeState
|
||||
private let statePromise: ValuePromise<CallListNodeState>
|
||||
|
||||
private var currentLocationAndType = CallListNodeLocationAndType(location: .initial(count: 50), type: .all)
|
||||
private let callListLocationAndType = ValuePromise<CallListNodeLocationAndType>()
|
||||
private let callListDisposable = MetaDisposable()
|
||||
|
||||
private let listNode: ListView
|
||||
|
||||
private let call: (PeerId) -> Void
|
||||
private let openInfo: (PeerId) -> Void
|
||||
|
||||
init(account: Account, presentationData: PresentationData, call: @escaping (PeerId) -> Void, openInfo: @escaping (PeerId) -> Void) {
|
||||
self.account = account
|
||||
self.presentationData = presentationData
|
||||
self.call = call
|
||||
self.openInfo = openInfo
|
||||
|
||||
self.currentState = CallListNodeState(theme: presentationData.theme, strings: presentationData.strings, editing: false, messageIdWithRevealedOptions: nil)
|
||||
self.statePromise = ValuePromise(self.currentState, ignoreRepeated: true)
|
||||
|
||||
self.listNode = ListView()
|
||||
|
||||
super.init(viewBlock: {
|
||||
return UITracingLayerView()
|
||||
}, didLoad: nil)
|
||||
|
||||
self.addSubnode(self.listNode)
|
||||
|
||||
self.backgroundColor = presentationData.theme.chatList.backgroundColor
|
||||
|
||||
let nodeInteraction = CallListNodeInteraction(setMessageIdWithRevealedOptions: { [weak self] messageId, fromMessageId in
|
||||
if let strongSelf = self {
|
||||
strongSelf.updateState { state in
|
||||
if (messageId == nil && fromMessageId == state.messageIdWithRevealedOptions) || (messageId != nil && fromMessageId == nil) {
|
||||
return state.withUpdatedMessageIdWithRevealedOptions(messageId)
|
||||
} else {
|
||||
return state
|
||||
}
|
||||
}
|
||||
}
|
||||
}, call: { [weak self] peerId in
|
||||
self?.call(peerId)
|
||||
}, openInfo: { [weak self] peerId in
|
||||
self?.openInfo(peerId)
|
||||
}, delete: { [weak self] messageIds in
|
||||
if let strongSelf = self {
|
||||
let _ = deleteMessagesInteractively(postbox: strongSelf.account.postbox, messageIds: messageIds, type: .forLocalPeer).start()
|
||||
}
|
||||
})
|
||||
|
||||
let viewProcessingQueue = self.viewProcessingQueue
|
||||
|
||||
let callListViewUpdate = self.callListLocationAndType.get()
|
||||
|> distinctUntilChanged
|
||||
|> mapToSignal { locationAndType in
|
||||
return callListViewForLocationAndType(locationAndType: locationAndType, account: account)
|
||||
}
|
||||
|
||||
let previousView = Atomic<CallListNodeView?>(value: nil)
|
||||
|
||||
let callListNodeViewTransition = combineLatest(callListViewUpdate, self.statePromise.get()) |> mapToQueue { (update, state) -> Signal<CallListNodeListViewTransition, NoError> in
|
||||
let processedView = CallListNodeView(originalView: update.view, filteredEntries: callListNodeEntriesForView(update.view, state: state))
|
||||
let previous = previousView.swap(processedView)
|
||||
|
||||
let reason: CallListNodeViewTransitionReason
|
||||
var prepareOnMainQueue = false
|
||||
|
||||
var previousWasEmptyOrSingleHole = false
|
||||
if let previous = previous {
|
||||
if previous.filteredEntries.count == 1 {
|
||||
if case .holeEntry = previous.filteredEntries[0] {
|
||||
previousWasEmptyOrSingleHole = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
previousWasEmptyOrSingleHole = true
|
||||
}
|
||||
|
||||
if previousWasEmptyOrSingleHole {
|
||||
reason = .initial
|
||||
if previous == nil {
|
||||
prepareOnMainQueue = true
|
||||
}
|
||||
} else {
|
||||
switch update.type {
|
||||
case .InitialUnread:
|
||||
reason = .initial
|
||||
prepareOnMainQueue = true
|
||||
case .Generic:
|
||||
reason = .interactiveChanges
|
||||
case .UpdateVisible:
|
||||
reason = .reload
|
||||
case .FillHole:
|
||||
reason = .reload
|
||||
}
|
||||
}
|
||||
|
||||
return preparedCallListNodeViewTransition(from: previous, to: processedView, reason: reason, account: account, scrollPosition: update.scrollPosition)
|
||||
|> map({ mappedCallListNodeViewListTransition(account: account, nodeInteraction: nodeInteraction, transition: $0) })
|
||||
|> runOn(prepareOnMainQueue ? Queue.mainQueue() : viewProcessingQueue)
|
||||
}
|
||||
|
||||
let appliedTransition = callListNodeViewTransition |> deliverOnMainQueue |> mapToQueue { [weak self] transition -> Signal<Void, NoError> in
|
||||
if let strongSelf = self {
|
||||
return strongSelf.enqueueTransition(transition)
|
||||
}
|
||||
return .complete()
|
||||
}
|
||||
|
||||
self.listNode.displayedItemRangeChanged = { [weak self] range, transactionOpaqueState in
|
||||
if let strongSelf = self, let range = range.loadedRange, let view = (transactionOpaqueState as? CallListOpaqueTransactionState)?.callListView.originalView {
|
||||
var location: CallListNodeLocation?
|
||||
if range.firstIndex < 5 && view.later != nil {
|
||||
location = .navigation(index: view.entries[view.entries.count - 1].highestIndex)
|
||||
} else if range.firstIndex >= 5 && range.lastIndex >= view.entries.count - 5 && view.earlier != nil {
|
||||
location = .navigation(index: view.entries[0].lowestIndex)
|
||||
}
|
||||
|
||||
if let location = location, location != strongSelf.currentLocationAndType.location {
|
||||
strongSelf.currentLocationAndType = CallListNodeLocationAndType(location: location, type: strongSelf.currentLocationAndType.type)
|
||||
strongSelf.callListLocationAndType.set(strongSelf.currentLocationAndType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.callListDisposable.set(appliedTransition.start())
|
||||
|
||||
self.callListLocationAndType.set(self.currentLocationAndType)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.callListDisposable.dispose()
|
||||
}
|
||||
|
||||
func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
|
||||
if theme !== self.currentState.theme || strings !== self.currentState.strings {
|
||||
self.backgroundColor = theme.chatList.backgroundColor
|
||||
|
||||
self.updateState {
|
||||
return $0.withUpdatedPresentationData(theme: theme, strings: strings)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateState(_ f: (CallListNodeState) -> CallListNodeState) {
|
||||
let state = f(self.currentState)
|
||||
if state != self.currentState {
|
||||
self.currentState = state
|
||||
self.statePromise.set(state)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
self.callListLocationAndType.set(self.currentLocationAndType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func enqueueTransition(_ transition: CallListNodeListViewTransition) -> Signal<Void, NoError> {
|
||||
return Signal { [weak self] subscriber in
|
||||
if let strongSelf = self {
|
||||
if let _ = strongSelf.enqueuedTransition {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
strongSelf.enqueuedTransition = (transition, {
|
||||
subscriber.putCompletion()
|
||||
})
|
||||
|
||||
if strongSelf.isNodeLoaded {
|
||||
strongSelf.dequeueTransition()
|
||||
} else {
|
||||
if !strongSelf.didSetReady {
|
||||
strongSelf.didSetReady = true
|
||||
strongSelf._ready.set(true)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
|
||||
return EmptyDisposable
|
||||
} |> runOn(Queue.mainQueue())
|
||||
}
|
||||
|
||||
private func dequeueTransition() {
|
||||
if let (transition, completion) = self.enqueuedTransition {
|
||||
self.enqueuedTransition = nil
|
||||
|
||||
let completion: (ListViewDisplayedItemRange) -> Void = { [weak self] visibleRange in
|
||||
if let strongSelf = self {
|
||||
strongSelf.callListView = transition.callListView
|
||||
|
||||
if !strongSelf.didSetReady {
|
||||
strongSelf.didSetReady = true
|
||||
strongSelf._ready.set(true)
|
||||
}
|
||||
|
||||
completion()
|
||||
}
|
||||
}
|
||||
|
||||
self.listNode.transaction(deleteIndices: transition.deleteItems, insertIndicesAndItems: transition.insertItems, updateIndicesAndItems: transition.updateItems, options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, updateOpaqueState: CallListOpaqueTransactionState(callListView: transition.callListView), completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
func scrollToLatest() {
|
||||
if let view = self.callListView?.originalView, view.later == nil {
|
||||
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: ListViewScrollToItem(index: 0, position: .Top, animated: true, curve: .Default, directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
||||
} else {
|
||||
let location: CallListNodeLocation = .scroll(index: MessageIndex.absoluteUpperBound(), sourceIndex: MessageIndex.absoluteLowerBound(), scrollPosition: .Top, animated: true)
|
||||
self.currentLocationAndType = CallListNodeLocationAndType(location: location, type: self.currentLocationAndType.type)
|
||||
self.callListLocationAndType.set(self.currentLocationAndType)
|
||||
}
|
||||
}
|
||||
|
||||
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
self.containerLayout = (layout, navigationBarHeight)
|
||||
|
||||
var insets = layout.insets(options: [.input])
|
||||
insets.top += max(navigationBarHeight, layout.insets(options: [.statusBar]).top)
|
||||
|
||||
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)
|
||||
|
||||
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: layout.size, insets: insets, duration: duration, curve: listViewCurve)
|
||||
|
||||
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
||||
|
||||
if !self.dequeuedInitialTransitionOnLayout {
|
||||
self.dequeuedInitialTransitionOnLayout = true
|
||||
self.dequeueTransition()
|
||||
}
|
||||
}
|
||||
}
|
||||
128
TelegramUI/CallListNodeEntries.swift
Normal file
@ -0,0 +1,128 @@
|
||||
import Foundation
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
|
||||
enum CallListNodeEntryId: Hashable {
|
||||
case hole(MessageIndex)
|
||||
case message(MessageIndex)
|
||||
|
||||
var hashValue: Int {
|
||||
switch self {
|
||||
case let .hole(index):
|
||||
return index.hashValue
|
||||
case let .message(index):
|
||||
return index.hashValue
|
||||
}
|
||||
}
|
||||
|
||||
static func <(lhs: CallListNodeEntryId, rhs: CallListNodeEntryId) -> Bool {
|
||||
return lhs.hashValue < rhs.hashValue
|
||||
}
|
||||
|
||||
static func ==(lhs: CallListNodeEntryId, rhs: CallListNodeEntryId) -> Bool {
|
||||
switch lhs {
|
||||
case let .hole(index):
|
||||
if case .hole(index) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .message(index):
|
||||
if case .message(index) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func areMessagesEqual(_ lhsMessage: Message, _ rhsMessage: Message) -> Bool {
|
||||
if lhsMessage.stableVersion != rhsMessage.stableVersion {
|
||||
return false
|
||||
}
|
||||
if lhsMessage.id != rhsMessage.id || lhsMessage.flags != rhsMessage.flags {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
enum CallListNodeEntry: Comparable, Identifiable {
|
||||
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 let .messageEntry(message, _, _, _, _, _):
|
||||
return MessageIndex(message)
|
||||
case let .holeEntry(index, _):
|
||||
return index
|
||||
}
|
||||
}
|
||||
|
||||
var stableId: CallListNodeEntryId {
|
||||
switch self {
|
||||
case let .messageEntry(message, _, _, _, _, _):
|
||||
return .message(MessageIndex(message))
|
||||
case let .holeEntry(index, _):
|
||||
return .hole(index)
|
||||
}
|
||||
}
|
||||
|
||||
static func <(lhs: CallListNodeEntry, rhs: CallListNodeEntry) -> Bool {
|
||||
return lhs.index < rhs.index
|
||||
}
|
||||
|
||||
static func ==(lhs: CallListNodeEntry, rhs: CallListNodeEntry) -> Bool {
|
||||
switch lhs {
|
||||
case let .messageEntry(lhsMessage, lhsMessages, lhsTheme, lhsStrings, lhsEditing, lhsHasRevealControls):
|
||||
if case let .messageEntry(rhsMessage, rhsMessages, rhsTheme, rhsStrings, rhsEditing, rhsHasRevealControls) = rhs {
|
||||
if lhsTheme !== rhsTheme {
|
||||
return false
|
||||
}
|
||||
if lhsStrings !== rhsStrings {
|
||||
return false
|
||||
}
|
||||
if lhsEditing != rhsEditing {
|
||||
return false
|
||||
}
|
||||
if lhsHasRevealControls != rhsHasRevealControls {
|
||||
return false
|
||||
}
|
||||
if !areMessagesEqual(lhsMessage, rhsMessage) {
|
||||
return false
|
||||
}
|
||||
if lhsMessages.count != rhsMessages.count {
|
||||
return false
|
||||
}
|
||||
for i in 0 ..< lhsMessages.count {
|
||||
if !areMessagesEqual(lhsMessages[i], rhsMessages[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .holeEntry(lhsIndex, lhsTheme):
|
||||
if case let .holeEntry(rhsIndex, rhsTheme) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func callListNodeEntriesForView(_ view: CallListView, state: CallListNodeState) -> [CallListNodeEntry] {
|
||||
var result: [CallListNodeEntry] = []
|
||||
for entry in view.entries {
|
||||
switch entry {
|
||||
case let .message(topMessage, messages):
|
||||
result.append(.messageEntry(topMessage: topMessage, messages: messages, theme: state.theme, strings: state.strings, editing: state.editing, hasActiveRevealControls: state.messageIdWithRevealedOptions == topMessage.id))
|
||||
case let .hole(index):
|
||||
result.append(.holeEntry(index: index, theme: state.theme))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
83
TelegramUI/CallListNodeLocation.swift
Normal file
@ -0,0 +1,83 @@
|
||||
import Foundation
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import Display
|
||||
|
||||
enum CallListNodeLocation: Equatable {
|
||||
case initial(count: Int)
|
||||
case changeType(index: MessageIndex)
|
||||
case navigation(index: MessageIndex)
|
||||
case scroll(index: MessageIndex, sourceIndex: MessageIndex, scrollPosition: ListViewScrollPosition, animated: Bool)
|
||||
|
||||
static func ==(lhs: CallListNodeLocation, rhs: CallListNodeLocation) -> Bool {
|
||||
switch lhs {
|
||||
case let .navigation(index):
|
||||
switch rhs {
|
||||
case .navigation(index):
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CallListNodeLocationAndType: Equatable {
|
||||
let location: CallListNodeLocation
|
||||
let type: CallListViewType
|
||||
|
||||
static func ==(lhs: CallListNodeLocationAndType, rhs: CallListNodeLocationAndType) -> Bool {
|
||||
return lhs.location == rhs.location && lhs.type == rhs.type
|
||||
}
|
||||
}
|
||||
|
||||
struct CallListNodeViewUpdate {
|
||||
let view: CallListView
|
||||
let type: ViewUpdateType
|
||||
let scrollPosition: CallListNodeViewScrollPosition?
|
||||
}
|
||||
|
||||
func callListViewForLocationAndType(locationAndType: CallListNodeLocationAndType, account: Account) -> Signal<CallListNodeViewUpdate, NoError> {
|
||||
switch locationAndType.location {
|
||||
case let .initial(count):
|
||||
return account.viewTracker.callListView(type: locationAndType.type, index: MessageIndex.absoluteUpperBound(), count: count) |> map { view -> CallListNodeViewUpdate in
|
||||
return CallListNodeViewUpdate(view: view, type: .Generic, scrollPosition: nil)
|
||||
}
|
||||
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)
|
||||
}
|
||||
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
|
||||
if first {
|
||||
first = false
|
||||
genericType = .UpdateVisible
|
||||
} else {
|
||||
genericType = .Generic
|
||||
}
|
||||
return CallListNodeViewUpdate(view: view, type: genericType, scrollPosition: nil)
|
||||
}
|
||||
case let .scroll(index, sourceIndex, scrollPosition, animated):
|
||||
let directionHint: ListViewScrollToItemDirectionHint = sourceIndex > index ? .Down : .Up
|
||||
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 scrollPosition: CallListNodeViewScrollPosition? = first ? callScrollPosition : nil
|
||||
if first {
|
||||
first = false
|
||||
genericType = .UpdateVisible
|
||||
} else {
|
||||
genericType = .Generic
|
||||
}
|
||||
return CallListNodeViewUpdate(view: view, type: genericType, scrollPosition: scrollPosition)
|
||||
}
|
||||
}
|
||||
}
|
||||
170
TelegramUI/CallListViewTransition.swift
Normal file
@ -0,0 +1,170 @@
|
||||
import Foundation
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import Display
|
||||
|
||||
struct CallListNodeView {
|
||||
let originalView: CallListView
|
||||
let filteredEntries: [CallListNodeEntry]
|
||||
}
|
||||
|
||||
enum CallListNodeViewTransitionReason {
|
||||
case initial
|
||||
case interactiveChanges
|
||||
case holeChanges(filledHoleDirections: [MessageIndex: HoleFillDirection], removeHoleDirections: [MessageIndex: HoleFillDirection])
|
||||
case reload
|
||||
}
|
||||
|
||||
struct CallListNodeViewTransitionInsertEntry {
|
||||
let index: Int
|
||||
let previousIndex: Int?
|
||||
let entry: CallListNodeEntry
|
||||
let directionHint: ListViewItemOperationDirectionHint?
|
||||
}
|
||||
|
||||
struct CallListNodeViewTransitionUpdateEntry {
|
||||
let index: Int
|
||||
let previousIndex: Int
|
||||
let entry: CallListNodeEntry
|
||||
let directionHint: ListViewItemOperationDirectionHint?
|
||||
}
|
||||
|
||||
struct CallListNodeViewTransition {
|
||||
let callListView: CallListNodeView
|
||||
let deleteItems: [ListViewDeleteItem]
|
||||
let insertEntries: [CallListNodeViewTransitionInsertEntry]
|
||||
let updateEntries: [CallListNodeViewTransitionUpdateEntry]
|
||||
let options: ListViewDeleteAndInsertOptions
|
||||
let scrollToItem: ListViewScrollToItem?
|
||||
let stationaryItemRange: (Int, Int)?
|
||||
}
|
||||
|
||||
enum CallListNodeViewScrollPosition {
|
||||
case index(index: MessageIndex, position: ListViewScrollPosition, directionHint: ListViewScrollToItemDirectionHint, animated: Bool)
|
||||
}
|
||||
|
||||
func preparedCallListNodeViewTransition(from fromView: CallListNodeView?, to toView: CallListNodeView, reason: CallListNodeViewTransitionReason, account: Account, scrollPosition: CallListNodeViewScrollPosition?) -> Signal<CallListNodeViewTransition, NoError> {
|
||||
return Signal { subscriber in
|
||||
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromView?.filteredEntries ?? [], rightList: toView.filteredEntries)
|
||||
|
||||
var adjustedDeleteIndices: [ListViewDeleteItem] = []
|
||||
let previousCount: Int
|
||||
if let fromView = fromView {
|
||||
previousCount = fromView.filteredEntries.count
|
||||
} else {
|
||||
previousCount = 0;
|
||||
}
|
||||
for index in deleteIndices {
|
||||
adjustedDeleteIndices.append(ListViewDeleteItem(index: previousCount - 1 - index, directionHint: nil))
|
||||
}
|
||||
var adjustedIndicesAndItems: [CallListNodeViewTransitionInsertEntry] = []
|
||||
var adjustedUpdateItems: [CallListNodeViewTransitionUpdateEntry] = []
|
||||
let updatedCount = toView.filteredEntries.count
|
||||
|
||||
var options: ListViewDeleteAndInsertOptions = []
|
||||
var maxAnimatedInsertionIndex = -1
|
||||
var stationaryItemRange: (Int, Int)?
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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 .AroundIndex:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (index, entry, previousIndex) in indicesAndItems {
|
||||
let adjustedIndex = updatedCount - 1 - index
|
||||
|
||||
let adjustedPrevousIndex: Int?
|
||||
if let previousIndex = previousIndex {
|
||||
adjustedPrevousIndex = previousCount - 1 - previousIndex
|
||||
} else {
|
||||
adjustedPrevousIndex = nil
|
||||
}
|
||||
|
||||
var directionHint: ListViewItemOperationDirectionHint?
|
||||
if maxAnimatedInsertionIndex >= 0 && adjustedIndex <= maxAnimatedInsertionIndex {
|
||||
directionHint = .Down
|
||||
}
|
||||
|
||||
adjustedIndicesAndItems.append(CallListNodeViewTransitionInsertEntry(index: adjustedIndex, previousIndex: adjustedPrevousIndex, entry: entry, directionHint: directionHint))
|
||||
}
|
||||
|
||||
for (index, entry, previousIndex) in updateIndices {
|
||||
let adjustedIndex = updatedCount - 1 - index
|
||||
let adjustedPreviousIndex = previousCount - 1 - previousIndex
|
||||
|
||||
let directionHint: ListViewItemOperationDirectionHint? = nil
|
||||
adjustedUpdateItems.append(CallListNodeViewTransitionUpdateEntry(index: adjustedIndex, previousIndex: adjustedPreviousIndex, entry: entry, directionHint: directionHint))
|
||||
}
|
||||
|
||||
if let scrollPosition = scrollPosition {
|
||||
switch scrollPosition {
|
||||
case let .index(scrollIndex, position, directionHint, animated):
|
||||
var index = toView.filteredEntries.count - 1
|
||||
for entry in toView.filteredEntries {
|
||||
if entry.index >= scrollIndex {
|
||||
scrollToItem = ListViewScrollToItem(index: index, position: position, animated: animated, curve: .Default, directionHint: directionHint)
|
||||
break
|
||||
}
|
||||
index -= 1
|
||||
}
|
||||
|
||||
if scrollToItem == nil {
|
||||
var index = 0
|
||||
for entry in toView.filteredEntries.reversed() {
|
||||
if entry.index < scrollIndex {
|
||||
scrollToItem = ListViewScrollToItem(index: index, position: position, animated: animated, curve: .Default, directionHint: directionHint)
|
||||
break
|
||||
}
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subscriber.putNext(CallListNodeViewTransition(callListView: toView, deleteItems: adjustedDeleteIndices, insertEntries: adjustedIndicesAndItems, updateEntries: adjustedUpdateItems, options: options, scrollToItem: scrollToItem, stationaryItemRange: stationaryItemRange))
|
||||
subscriber.putCompletion()
|
||||
|
||||
return EmptyDisposable
|
||||
}
|
||||
}
|
||||
@ -38,8 +38,8 @@ private enum ChangePhoneNumberCodeTag: ItemListItemTag {
|
||||
}
|
||||
|
||||
private enum ChangePhoneNumberCodeEntry: ItemListNodeEntry {
|
||||
case codeEntry(String)
|
||||
case codeInfo(String)
|
||||
case codeEntry(PresentationTheme, String)
|
||||
case codeInfo(PresentationTheme, String)
|
||||
|
||||
var section: ItemListSectionId {
|
||||
return ChangePhoneNumberCodeSection.code.rawValue
|
||||
@ -56,14 +56,14 @@ private enum ChangePhoneNumberCodeEntry: ItemListNodeEntry {
|
||||
|
||||
static func ==(lhs: ChangePhoneNumberCodeEntry, rhs: ChangePhoneNumberCodeEntry) -> Bool {
|
||||
switch lhs {
|
||||
case let .codeEntry(text):
|
||||
if case .codeEntry(text) = rhs {
|
||||
case let .codeEntry(lhsTheme, lhsText):
|
||||
if case let .codeEntry(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .codeInfo(text):
|
||||
if case .codeInfo(text) = rhs {
|
||||
case let .codeInfo(lhsTheme, lhsText):
|
||||
if case let .codeInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
@ -77,14 +77,14 @@ private enum ChangePhoneNumberCodeEntry: ItemListNodeEntry {
|
||||
|
||||
func item(_ arguments: ChangePhoneNumberCodeControllerArguments) -> ListViewItem {
|
||||
switch self {
|
||||
case let .codeEntry(text):
|
||||
return ItemListSingleLineInputItem(title: NSAttributedString(string: "Code", textColor: .black), text: text, placeholder: "", type: .number, spacing: 10.0, tag: ChangePhoneNumberCodeTag.input, sectionId: self.section, textUpdated: { updatedText in
|
||||
case let .codeEntry(theme, text):
|
||||
return ItemListSingleLineInputItem(theme: theme, title: NSAttributedString(string: "Code", textColor: .black), text: text, placeholder: "", type: .number, spacing: 10.0, tag: ChangePhoneNumberCodeTag.input, sectionId: self.section, textUpdated: { updatedText in
|
||||
arguments.updateEntryText(updatedText)
|
||||
}, action: {
|
||||
arguments.next()
|
||||
})
|
||||
case let .codeInfo(text):
|
||||
return ItemListTextItem(text: .plain(text), sectionId: self.section)
|
||||
case let .codeInfo(theme, text):
|
||||
return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -126,15 +126,15 @@ private struct ChangePhoneNumberCodeControllerState: Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
private func changePhoneNumberCodeControllerEntries(state: ChangePhoneNumberCodeControllerState, codeData: ChangeAccountPhoneNumberData, timeout: Int32?) -> [ChangePhoneNumberCodeEntry] {
|
||||
private func changePhoneNumberCodeControllerEntries(presentationData: PresentationData, state: ChangePhoneNumberCodeControllerState, codeData: ChangeAccountPhoneNumberData, timeout: Int32?) -> [ChangePhoneNumberCodeEntry] {
|
||||
var entries: [ChangePhoneNumberCodeEntry] = []
|
||||
|
||||
entries.append(.codeEntry(state.codeText))
|
||||
entries.append(.codeEntry(presentationData.theme, state.codeText))
|
||||
var text = authorizationCurrentOptionText(codeData.type).string
|
||||
if let nextType = codeData.nextType {
|
||||
text += "\n\n" + authorizationNextOptionText(nextType, timeout: timeout).string
|
||||
}
|
||||
entries.append(.codeInfo(text))
|
||||
entries.append(.codeInfo(presentationData.theme, text))
|
||||
|
||||
return entries
|
||||
}
|
||||
@ -260,8 +260,9 @@ func changePhoneNumberCodeController(account: Account, phoneNumber: String, code
|
||||
checkCode()
|
||||
})
|
||||
|
||||
let signal = combineLatest(statePromise.get() |> deliverOnMainQueue, currentDataPromise.get() |> deliverOnMainQueue, timeout.get() |> deliverOnMainQueue)
|
||||
|> map { state, data, timeout -> (ItemListControllerState, (ItemListNodeState<ChangePhoneNumberCodeEntry>, ChangePhoneNumberCodeEntry.ItemGenerationArguments)) in
|
||||
let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get() |> deliverOnMainQueue, currentDataPromise.get() |> deliverOnMainQueue, timeout.get() |> deliverOnMainQueue)
|
||||
|> deliverOnMainQueue
|
||||
|> map { presentationData, state, data, timeout -> (ItemListControllerState, (ItemListNodeState<ChangePhoneNumberCodeEntry>, ChangePhoneNumberCodeEntry.ItemGenerationArguments)) in
|
||||
var rightNavigationButton: ItemListNavigationButton?
|
||||
if state.checking {
|
||||
rightNavigationButton = ItemListNavigationButton(title: "", style: .activity, enabled: true, action: {})
|
||||
@ -270,21 +271,20 @@ func changePhoneNumberCodeController(account: Account, phoneNumber: String, code
|
||||
if state.codeText.isEmpty {
|
||||
nextEnabled = false
|
||||
}
|
||||
rightNavigationButton = ItemListNavigationButton(title: "Next", style: .bold, enabled: nextEnabled, action: {
|
||||
rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Next, style: .bold, enabled: nextEnabled, action: {
|
||||
checkCode()
|
||||
})
|
||||
}
|
||||
|
||||
let controllerState = ItemListControllerState(title: .text(formatPhoneNumber(phoneNumber)), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, animateChanges: false)
|
||||
let listState = ItemListNodeState(entries: changePhoneNumberCodeControllerEntries(state: state, codeData: data, timeout: timeout), style: .blocks, focusItemTag: ChangePhoneNumberCodeTag.input, emptyStateItem: nil, animateChanges: false)
|
||||
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)
|
||||
|
||||
return (controllerState, (listState, arguments))
|
||||
} |> afterDisposed {
|
||||
actionsDisposable.dispose()
|
||||
}
|
||||
|
||||
let controller = ItemListController(signal)
|
||||
controller.navigationItem.backBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil)
|
||||
let controller = ItemListController(account: account, state: signal)
|
||||
|
||||
presentControllerImpl = { [weak controller] c, p in
|
||||
if let controller = controller {
|
||||
|
||||
@ -29,14 +29,20 @@ final class ChangePhoneNumberController: ViewController {
|
||||
|
||||
private let hapticFeedback = HapticFeedback()
|
||||
|
||||
private var presentationData: PresentationData
|
||||
|
||||
init(account: Account) {
|
||||
self.account = account
|
||||
|
||||
super.init(navigationBar: NavigationBar())
|
||||
self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 }
|
||||
|
||||
self.title = "Change Number"
|
||||
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Next", style: .done, target: self, action: #selector(self.nextPressed))
|
||||
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil)
|
||||
super.init(navigationBarTheme: NavigationBarTheme(rootControllerTheme: self.presentationData.theme))
|
||||
|
||||
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style
|
||||
|
||||
self.title = self.presentationData.strings.ChangePhoneNumberNumber_Title
|
||||
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Next, style: .done, target: self, action: #selector(self.nextPressed))
|
||||
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
|
||||
}
|
||||
|
||||
required init(coder aDecoder: NSCoder) {
|
||||
@ -57,7 +63,7 @@ final class ChangePhoneNumberController: ViewController {
|
||||
}
|
||||
|
||||
override public func loadDisplayNode() {
|
||||
self.displayNode = ChangePhoneNumberControllerNode()
|
||||
self.displayNode = ChangePhoneNumberControllerNode(presentationData: self.presentationData)
|
||||
self.displayNodeDidLoad()
|
||||
self.controllerNode.selectCountryCode = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
|
||||
@ -17,7 +17,7 @@ private let countryButtonBackground = generateImage(CGSize(width: 45.0, height:
|
||||
context.closePath()
|
||||
context.fillPath()
|
||||
|
||||
context.setStrokeColor(UIColor(0xbcbbc1).cgColor)
|
||||
context.setStrokeColor(UIColor(rgb: 0xbcbbc1).cgColor)
|
||||
context.setLineWidth(lineWidth)
|
||||
|
||||
context.move(to: CGPoint(x: size.width, y: size.height - arrowSize - lineWidth / 2.0))
|
||||
@ -35,7 +35,7 @@ private let countryButtonBackground = generateImage(CGSize(width: 45.0, height:
|
||||
private let countryButtonHighlightedBackground = generateImage(CGSize(width: 45.0, height: 44.0 + 6.0), rotatedContext: { size, context in
|
||||
let arrowSize: CGFloat = 6.0
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.setFillColor(UIColor(0xbcbbc1).cgColor)
|
||||
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))
|
||||
@ -50,7 +50,7 @@ private let phoneInputBackground = generateImage(CGSize(width: 60.0, height: 44.
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.setFillColor(UIColor.white.cgColor)
|
||||
context.fill(CGRect(origin: CGPoint(), size: size))
|
||||
context.setStrokeColor(UIColor(0xbcbbc1).cgColor)
|
||||
context.setStrokeColor(UIColor(rgb: 0xbcbbc1).cgColor)
|
||||
context.setLineWidth(lineWidth)
|
||||
context.move(to: CGPoint(x: 0.0, y: size.height - lineWidth / 2.0))
|
||||
context.addLine(to: CGPoint(x: size.width, y: size.height - lineWidth / 2.0))
|
||||
@ -89,16 +89,20 @@ final class ChangePhoneNumberControllerNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
override init() {
|
||||
var presentationData: PresentationData
|
||||
|
||||
init(presentationData: PresentationData) {
|
||||
self.presentationData = presentationData
|
||||
|
||||
self.titleNode = ASTextNode()
|
||||
self.titleNode.isLayerBacked = true
|
||||
self.titleNode.displaysAsynchronously = false
|
||||
self.titleNode.attributedText = NSAttributedString(string: "NEW NUMBER", font: Font.regular(14.0), textColor: UIColor(0x6d6d72))
|
||||
self.titleNode.attributedText = NSAttributedString(string: self.presentationData.strings.ChangePhoneNumberNumber_NewNumber, font: Font.regular(14.0), textColor: self.presentationData.theme.list.sectionHeaderTextColor)
|
||||
|
||||
self.noticeNode = ASTextNode()
|
||||
self.noticeNode.isLayerBacked = true
|
||||
self.noticeNode.displaysAsynchronously = false
|
||||
self.noticeNode.attributedText = NSAttributedString(string: "We will send an SMS with a confirmation code to your new number.", font: Font.regular(14.0), textColor: UIColor(0x6d6d72))
|
||||
self.noticeNode.attributedText = NSAttributedString(string: self.presentationData.strings.ChangePhoneNumberNumber_Help, font: Font.regular(14.0), textColor: self.presentationData.theme.list.freeTextColor)
|
||||
|
||||
self.countryButton = ASButtonNode()
|
||||
self.countryButton.setBackgroundImage(countryButtonBackground, for: [])
|
||||
@ -116,7 +120,7 @@ final class ChangePhoneNumberControllerNode: ASDisplayNode {
|
||||
return UITracingLayerView()
|
||||
}, didLoad: nil)
|
||||
|
||||
self.backgroundColor = UIColor(0xefefef)
|
||||
self.backgroundColor = self.presentationData.theme.list.blocksBackgroundColor
|
||||
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.noticeNode)
|
||||
@ -127,7 +131,7 @@ final class ChangePhoneNumberControllerNode: ASDisplayNode {
|
||||
self.countryButton.contentEdgeInsets = UIEdgeInsets(top: 0.0, left: 15.0, bottom: 4.0, right: 0.0)
|
||||
self.countryButton.contentHorizontalAlignment = .left
|
||||
|
||||
self.phoneInputNode.numberField.textField.attributedPlaceholder = NSAttributedString(string: "Your phone number", font: Font.regular(17.0), textColor: UIColor(0xbcbcc3))
|
||||
self.phoneInputNode.numberField.textField.attributedPlaceholder = NSAttributedString(string: self.presentationData.strings.Login_PhonePlaceholder, font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemPlaceholderTextColor)
|
||||
|
||||
self.countryButton.addTarget(self, action: #selector(self.countryPressed), forControlEvents: .touchUpInside)
|
||||
|
||||
@ -136,7 +140,7 @@ final class ChangePhoneNumberControllerNode: ASDisplayNode {
|
||||
if let code = Int(code), let countryName = countryCodeToName[code] {
|
||||
strongSelf.countryButton.setTitle(countryName, with: Font.regular(17.0), with: .black, for: [])
|
||||
} else {
|
||||
strongSelf.countryButton.setTitle("Select Country", with: Font.regular(17.0), with: .black, for: [])
|
||||
strongSelf.countryButton.setTitle(strongSelf.presentationData.strings.Login_CountryCode, with: Font.regular(17.0), with: .black, for: [])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,6 +4,8 @@ import AsyncDisplayKit
|
||||
import TelegramCore
|
||||
|
||||
private final class ChangePhoneNumberIntroControllerNode: ASDisplayNode {
|
||||
var presentationData: PresentationData
|
||||
|
||||
let iconNode: ASImageNode
|
||||
let labelNode: ASTextNode
|
||||
let buttonNode: HighlightableButtonNode
|
||||
@ -11,7 +13,9 @@ private final class ChangePhoneNumberIntroControllerNode: ASDisplayNode {
|
||||
var dismiss: (() -> Void)?
|
||||
var action: (() -> Void)?
|
||||
|
||||
override init() {
|
||||
init(presentationData: PresentationData) {
|
||||
self.presentationData = presentationData
|
||||
|
||||
self.iconNode = ASImageNode()
|
||||
self.labelNode = ASTextNode()
|
||||
self.buttonNode = HighlightableButtonNode()
|
||||
@ -20,16 +24,17 @@ private final class ChangePhoneNumberIntroControllerNode: ASDisplayNode {
|
||||
return UITracingLayerView()
|
||||
}, didLoad: nil)
|
||||
|
||||
self.iconNode.image = UIImage(bundleImageName: "Settings/ChangePhoneIntroIcon")?.precomposed()
|
||||
self.labelNode.attributedText = NSAttributedString(string: "You can change your Telegram number here. Your account and all your cloud data — messages, media, contacts, etc. will be moved to the new number.\n\nImportant: all your Telegram contacts will get your new number added to their address book, provided they had your old number and you haven't blocked them in Telegram.", font: Font.regular(14.0), textColor: UIColor(0x6d6d72), paragraphAlignment: .center)
|
||||
self.buttonNode.setTitle("Change Number", with: Font.regular(19.0), with: UIColor(0x007ee5), for: .normal)
|
||||
self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Settings/ChangePhoneIntroIcon"), color: presentationData.theme.list.freeTextColor)
|
||||
let textColor = self.presentationData.theme.list.freeTextColor
|
||||
self.labelNode.attributedText = parseMarkdownIntoAttributedString(self.presentationData.strings.PhoneNumberHelp_Help, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: Font.regular(14.0), textColor: textColor), bold: MarkdownAttributeSet(font: Font.semibold(14.0), textColor: textColor), link: MarkdownAttributeSet(font: Font.regular(14.0), textColor: textColor), linkAttribute: { _ in return nil }), textAlignment: .center)
|
||||
self.buttonNode.setTitle(self.presentationData.strings.PhoneNumberHelp_ChangeNumber, with: Font.regular(19.0), with: self.presentationData.theme.list.itemAccentColor, for: .normal)
|
||||
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
|
||||
|
||||
self.addSubnode(self.iconNode)
|
||||
self.addSubnode(self.labelNode)
|
||||
self.addSubnode(self.buttonNode)
|
||||
|
||||
self.backgroundColor = UIColor(0xefeff4)
|
||||
self.backgroundColor = self.presentationData.theme.list.blocksBackgroundColor
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
@ -72,13 +77,19 @@ final class ChangePhoneNumberIntroController: ViewController {
|
||||
private let account: Account
|
||||
private var didPlayPresentationAnimation = false
|
||||
|
||||
private var presentationData: PresentationData
|
||||
|
||||
init(account: Account, phoneNumber: String) {
|
||||
self.account = account
|
||||
|
||||
super.init(navigationBar: NavigationBar())
|
||||
self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 }
|
||||
|
||||
super.init(navigationBarTheme: NavigationBarTheme(rootControllerTheme: self.presentationData.theme))
|
||||
|
||||
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style
|
||||
|
||||
self.title = phoneNumber
|
||||
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil)
|
||||
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
|
||||
//self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Cancel", style: .plain, target: self, action: #selector(self.cancelPressed))
|
||||
}
|
||||
|
||||
@ -87,7 +98,7 @@ final class ChangePhoneNumberIntroController: ViewController {
|
||||
}
|
||||
|
||||
override func loadDisplayNode() {
|
||||
self.displayNode = ChangePhoneNumberIntroControllerNode()
|
||||
self.displayNode = ChangePhoneNumberIntroControllerNode(presentationData: self.presentationData)
|
||||
(self.displayNode as! ChangePhoneNumberIntroControllerNode).dismiss = { [weak self] in
|
||||
self?.presentingViewController?.dismiss(animated: false, completion: nil)
|
||||
}
|
||||
@ -117,7 +128,7 @@ final class ChangePhoneNumberIntroController: ViewController {
|
||||
}
|
||||
|
||||
func proceed() {
|
||||
self.present(standardTextAlertController(title: nil, text: "All your Telegram contacts will get your new number added to their address book, provided they had your old number and you haven't blocked them in Telegram.", actions: [TextAlertAction(type: .defaultAction, title: "Cancel", action: {}), TextAlertAction(type: .genericAction, title: "OK", action: { [weak self] in
|
||||
self.present(standardTextAlertController(title: nil, text: self.presentationData.strings.PhoneNumberHelp_Alert, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_OK, action: { [weak self] in
|
||||
if let strongSelf = self {
|
||||
(strongSelf.navigationController as? NavigationController)?.replaceTopController(ChangePhoneNumberController(account: strongSelf.account), animated: true)
|
||||
}
|
||||
|
||||
478
TelegramUI/ChannelAdminController.swift
Normal file
@ -0,0 +1,478 @@
|
||||
import Foundation
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
|
||||
private final class ChannelAdminControllerArguments {
|
||||
let account: Account
|
||||
let toggleRight: (TelegramChannelAdminRightsFlags, TelegramChannelAdminRightsFlags) -> Void
|
||||
let dismissAdmin: () -> Void
|
||||
|
||||
init(account: Account, toggleRight: @escaping (TelegramChannelAdminRightsFlags, TelegramChannelAdminRightsFlags) -> Void, dismissAdmin: @escaping () -> Void) {
|
||||
self.account = account
|
||||
self.toggleRight = toggleRight
|
||||
self.dismissAdmin = dismissAdmin
|
||||
}
|
||||
}
|
||||
|
||||
private enum ChannelAdminSection: Int32 {
|
||||
case info
|
||||
case rights
|
||||
case dismiss
|
||||
}
|
||||
|
||||
private enum ChannelAdminEntryStableId: Hashable {
|
||||
case info
|
||||
case right(TelegramChannelAdminRightsFlags)
|
||||
case dismiss
|
||||
|
||||
var hashValue: Int {
|
||||
switch self {
|
||||
case .info:
|
||||
return 0
|
||||
case .dismiss:
|
||||
return 1
|
||||
case let .right(flags):
|
||||
return flags.rawValue.hashValue
|
||||
}
|
||||
}
|
||||
|
||||
static func ==(lhs: ChannelAdminEntryStableId, rhs: ChannelAdminEntryStableId) -> Bool {
|
||||
switch lhs {
|
||||
case .info:
|
||||
if case .info = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let right(flags):
|
||||
if case .right(flags) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .dismiss:
|
||||
if case .dismiss = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum ChannelAdminEntry: ItemListNodeEntry {
|
||||
case info(PresentationTheme, PresentationStrings, Peer, TelegramUserPresence?)
|
||||
case rightItem(PresentationTheme, Int, String, TelegramChannelAdminRightsFlags, TelegramChannelAdminRightsFlags, Bool, Bool)
|
||||
case dismiss(PresentationTheme, String)
|
||||
|
||||
var section: ItemListSectionId {
|
||||
switch self {
|
||||
case .info:
|
||||
return ChannelAdminSection.info.rawValue
|
||||
case .rightItem:
|
||||
return ChannelAdminSection.rights.rawValue
|
||||
case .dismiss:
|
||||
return ChannelAdminSection.dismiss.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
var stableId: ChannelAdminEntryStableId {
|
||||
switch self {
|
||||
case .info:
|
||||
return .info
|
||||
case let .rightItem(_, _, _, right, _, _, _):
|
||||
return .right(right)
|
||||
case .dismiss:
|
||||
return .dismiss
|
||||
}
|
||||
}
|
||||
|
||||
static func ==(lhs: ChannelAdminEntry, rhs: ChannelAdminEntry) -> Bool {
|
||||
switch lhs {
|
||||
case let .info(lhsTheme, lhsStrings, lhsPeer, lhsPresence):
|
||||
if case let .info(rhsTheme, rhsStrings, rhsPeer, rhsPresence) = rhs {
|
||||
if lhsTheme !== rhsTheme {
|
||||
return false
|
||||
}
|
||||
if lhsStrings !== rhsStrings {
|
||||
return false
|
||||
}
|
||||
if !arePeersEqual(lhsPeer, rhsPeer) {
|
||||
return false
|
||||
}
|
||||
if lhsPresence != rhsPresence {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .rightItem(lhsTheme, lhsIndex, lhsText, lhsRight, lhsFlags, lhsValue, lhsEnabled):
|
||||
if case let .rightItem(rhsTheme, rhsIndex, rhsText, rhsRight, rhsFlags, rhsValue, rhsEnabled) = rhs {
|
||||
if lhsTheme !== rhsTheme {
|
||||
return false
|
||||
}
|
||||
if lhsIndex != rhsIndex {
|
||||
return false
|
||||
}
|
||||
if lhsText != rhsText {
|
||||
return false
|
||||
}
|
||||
if lhsRight != rhsRight {
|
||||
return false
|
||||
}
|
||||
if lhsFlags != rhsFlags {
|
||||
return false
|
||||
}
|
||||
if lhsValue != rhsValue {
|
||||
return false
|
||||
}
|
||||
if lhsEnabled != rhsEnabled {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .dismiss(lhsTheme, lhsText):
|
||||
if case let .dismiss(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func <(lhs: ChannelAdminEntry, rhs: ChannelAdminEntry) -> Bool {
|
||||
switch lhs {
|
||||
case .info:
|
||||
switch rhs {
|
||||
case .info:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
case let .rightItem(_, lhsIndex, _, _, _, _, _):
|
||||
switch rhs {
|
||||
case .info:
|
||||
return false
|
||||
case let .rightItem(_, rhsIndex, _, _, _, _, _):
|
||||
return lhsIndex < rhsIndex
|
||||
default:
|
||||
return true
|
||||
}
|
||||
case .dismiss:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}, avatarTapped: {
|
||||
})
|
||||
case let .rightItem(theme, _, text, right, flags, value, enabled):
|
||||
return ItemListSwitchItem(theme: theme, title: text, value: value, enabled: enabled, sectionId: self.section, style: .blocks, updated: { _ in
|
||||
arguments.toggleRight(right, flags)
|
||||
})
|
||||
case let .dismiss(theme, text):
|
||||
return ItemListActionItem(theme: theme, title: text, kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: {
|
||||
arguments.dismissAdmin()
|
||||
}, tag: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct ChannelAdminControllerState: Equatable {
|
||||
let updatedFlags: TelegramChannelAdminRightsFlags?
|
||||
let updating: Bool
|
||||
|
||||
init(updatedFlags: TelegramChannelAdminRightsFlags? = nil, updating: Bool = false) {
|
||||
self.updatedFlags = updatedFlags
|
||||
self.updating = updating
|
||||
}
|
||||
|
||||
static func ==(lhs: ChannelAdminControllerState, rhs: ChannelAdminControllerState) -> Bool {
|
||||
if lhs.updatedFlags != rhs.updatedFlags {
|
||||
return false
|
||||
}
|
||||
if lhs.updating != rhs.updating {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func withUpdatedUpdatedFlags(_ updatedFlags: TelegramChannelAdminRightsFlags?) -> ChannelAdminControllerState {
|
||||
return ChannelAdminControllerState(updatedFlags: updatedFlags, updating: self.updating)
|
||||
}
|
||||
|
||||
func withUpdatedUpdating(_ updating: Bool) -> ChannelAdminControllerState {
|
||||
return ChannelAdminControllerState(updatedFlags: self.updatedFlags, updating: updating)
|
||||
}
|
||||
}
|
||||
|
||||
private func stringForRight(strings: PresentationStrings, right: TelegramChannelAdminRightsFlags, isGroup: Bool) -> String {
|
||||
if right.contains(.canChangeInfo) {
|
||||
return isGroup ? strings.Group_EditAdmin_PermissionChangeInfo : strings.Channel_EditAdmin_PermissionChangeInfo
|
||||
} else if right.contains(.canPostMessages) {
|
||||
return strings.Channel_EditAdmin_PermissionPostMessages
|
||||
} else if right.contains(.canEditMessages) {
|
||||
return strings.Channel_EditAdmin_PermissionEditMessages
|
||||
} else if right.contains(.canDeleteMessages) {
|
||||
return strings.Channel_EditAdmin_PermissionDeleteMessages
|
||||
} else if right.contains(.canBanUsers) {
|
||||
return strings.Channel_EditAdmin_PermissionBanUsers
|
||||
} else if right.contains(.canInviteUsers) {
|
||||
return strings.Channel_EditAdmin_PermissionInviteUsers
|
||||
} else if right.contains(.canChangeInviteLink) {
|
||||
return strings.Channel_EditAdmin_PermissionChangeInviteLink
|
||||
} else if right.contains(.canPinMessages) {
|
||||
return strings.Channel_EditAdmin_PermissionPinMessages
|
||||
} else if right.contains(.canAddAdmins) {
|
||||
return strings.Channel_EditAdmin_PermissionAddAdmins
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
private func rightDependencies(_ right: TelegramChannelAdminRightsFlags) -> [TelegramChannelAdminRightsFlags] {
|
||||
if right.contains(.canChangeInfo) {
|
||||
return []
|
||||
} else if right.contains(.canPostMessages) {
|
||||
return []
|
||||
} else if right.contains(.canEditMessages) {
|
||||
return []
|
||||
} else if right.contains(.canDeleteMessages) {
|
||||
return []
|
||||
} else if right.contains(.canBanUsers) {
|
||||
return []
|
||||
} else if right.contains(.canInviteUsers) {
|
||||
return []
|
||||
} else if right.contains(.canChangeInviteLink) {
|
||||
return [.canInviteUsers]
|
||||
} else if right.contains(.canPinMessages) {
|
||||
return []
|
||||
} else if right.contains(.canAddAdmins) {
|
||||
return []
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
private func canEditAdminRights(accountPeerId: PeerId, channelView: PeerView, initialParticipant: ChannelParticipant?) -> Bool {
|
||||
if let channel = channelView.peers[channelView.peerId] as? TelegramChannel {
|
||||
if channel.flags.contains(.isCreator) {
|
||||
return true
|
||||
} else if let initialParticipant = initialParticipant {
|
||||
switch initialParticipant {
|
||||
case .creator:
|
||||
return false
|
||||
case let .member(_, _, adminInfo, _):
|
||||
if let adminInfo = adminInfo {
|
||||
return adminInfo.canBeEditedByAccountPeer || adminInfo.promotedBy == accountPeerId
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return channel.hasAdminRights(.canAddAdmins)
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private func channelAdminControllerEntries(presentationData: PresentationData, state: ChannelAdminControllerState, accountPeerId: PeerId, channelView: PeerView, adminView: PeerView, initialParticipant: ChannelParticipant?) -> [ChannelAdminEntry] {
|
||||
var entries: [ChannelAdminEntry] = []
|
||||
|
||||
if let channel = channelView.peers[channelView.peerId] as? TelegramChannel, let admin = adminView.peers[adminView.peerId] {
|
||||
entries.append(.info(presentationData.theme, presentationData.strings, admin, adminView.peerPresences[admin.id] as? TelegramUserPresence))
|
||||
|
||||
let isGroup: Bool
|
||||
let maskRightsFlags: TelegramChannelAdminRightsFlags
|
||||
let rightsOrder: [TelegramChannelAdminRightsFlags]
|
||||
|
||||
switch channel.info {
|
||||
case .broadcast:
|
||||
isGroup = false
|
||||
maskRightsFlags = .broadcastSpecific
|
||||
rightsOrder = [
|
||||
.canChangeInfo,
|
||||
.canPostMessages,
|
||||
.canEditMessages,
|
||||
.canDeleteMessages,
|
||||
.canAddAdmins
|
||||
]
|
||||
case .group:
|
||||
isGroup = true
|
||||
maskRightsFlags = .groupSpecific
|
||||
rightsOrder = [
|
||||
.canChangeInfo,
|
||||
.canDeleteMessages,
|
||||
.canBanUsers,
|
||||
.canInviteUsers,
|
||||
.canChangeInviteLink,
|
||||
.canPinMessages,
|
||||
.canAddAdmins
|
||||
]
|
||||
}
|
||||
|
||||
if canEditAdminRights(accountPeerId: accountPeerId, channelView: channelView, initialParticipant: initialParticipant) {
|
||||
let accountUserRightsFlags: TelegramChannelAdminRightsFlags
|
||||
if channel.flags.contains(.isCreator) {
|
||||
accountUserRightsFlags = maskRightsFlags
|
||||
} else if let adminRights = channel.adminRights {
|
||||
accountUserRightsFlags = maskRightsFlags.intersection(adminRights.flags)
|
||||
} else {
|
||||
accountUserRightsFlags = []
|
||||
}
|
||||
|
||||
let currentRightsFlags: TelegramChannelAdminRightsFlags
|
||||
if let updatedFlags = state.updatedFlags {
|
||||
currentRightsFlags = updatedFlags
|
||||
} else if let initialParticipant = initialParticipant, case let .member(_, _, maybeAdminRights, _) = initialParticipant, let adminRights = maybeAdminRights {
|
||||
currentRightsFlags = adminRights.rights.flags
|
||||
} else {
|
||||
currentRightsFlags = accountUserRightsFlags.subtracting(.canAddAdmins)
|
||||
}
|
||||
|
||||
var index = 0
|
||||
for right in rightsOrder {
|
||||
if accountUserRightsFlags.contains(right) {
|
||||
entries.append(.rightItem(presentationData.theme, index, stringForRight(strings: presentationData.strings, right: right, isGroup: isGroup), right, currentRightsFlags, currentRightsFlags.contains(right), !state.updating))
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
|
||||
if let initialParticipant = initialParticipant {
|
||||
var canDismiss = false
|
||||
if channel.flags.contains(.isCreator) {
|
||||
canDismiss = true
|
||||
} else {
|
||||
switch initialParticipant {
|
||||
case .creator:
|
||||
break
|
||||
case let .member(_, _, adminInfo, _):
|
||||
if let adminInfo = adminInfo {
|
||||
if adminInfo.promotedBy == accountPeerId || adminInfo.canBeEditedByAccountPeer {
|
||||
canDismiss = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if canDismiss {
|
||||
entries.append(.dismiss(presentationData.theme, presentationData.strings.Channel_Moderator_AccessLevelRevoke))
|
||||
}
|
||||
}
|
||||
} else if let initialParticipant = initialParticipant, case let .member(_, _, maybeAdminInfo, _) = initialParticipant, let adminInfo = maybeAdminInfo {
|
||||
var index = 0
|
||||
for right in rightsOrder {
|
||||
entries.append(.rightItem(presentationData.theme, index, stringForRight(strings: presentationData.strings, right: right, isGroup: isGroup), right, adminInfo.rights.flags, adminInfo.rights.flags.contains(right), false))
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return entries
|
||||
}
|
||||
|
||||
public func channelAdminController(account: Account, peerId: PeerId, adminId: PeerId, initialParticipant: ChannelParticipant?, updated: @escaping (TelegramChannelAdminRights) -> Void) -> ViewController {
|
||||
let statePromise = ValuePromise(ChannelAdminControllerState(), ignoreRepeated: true)
|
||||
let stateValue = Atomic(value: ChannelAdminControllerState())
|
||||
let updateState: ((ChannelAdminControllerState) -> ChannelAdminControllerState) -> Void = { f in
|
||||
statePromise.set(stateValue.modify { f($0) })
|
||||
}
|
||||
|
||||
let actionsDisposable = DisposableSet()
|
||||
|
||||
let updateRightsDisposable = MetaDisposable()
|
||||
actionsDisposable.add(updateRightsDisposable)
|
||||
|
||||
var dismissImpl: (() -> Void)?
|
||||
|
||||
let arguments = ChannelAdminControllerArguments(account: account, toggleRight: { right, flags in
|
||||
updateState { current in
|
||||
var updated = flags
|
||||
if flags.contains(right) {
|
||||
updated.remove(right)
|
||||
} else {
|
||||
updated.insert(right)
|
||||
}
|
||||
return current.withUpdatedUpdatedFlags(updated)
|
||||
}
|
||||
}, dismissAdmin: {
|
||||
updateState { current in
|
||||
return current.withUpdatedUpdating(true)
|
||||
}
|
||||
updateRightsDisposable.set((updatePeerAdminRights(account: account, peerId: peerId, adminId: adminId, rights: TelegramChannelAdminRights(flags: [])) |> deliverOnMainQueue).start(error: { _ in
|
||||
|
||||
}, completed: {
|
||||
updated(TelegramChannelAdminRights(flags: []))
|
||||
dismissImpl?()
|
||||
}))
|
||||
})
|
||||
|
||||
let combinedView = account.postbox.combinedView(keys: [.peer(peerId: peerId), .peer(peerId: adminId)])
|
||||
|
||||
let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get(), combinedView)
|
||||
|> deliverOnMainQueue
|
||||
|> map { presentationData, state, combinedView -> (ItemListControllerState, (ItemListNodeState<ChannelAdminEntry>, ChannelAdminEntry.ItemGenerationArguments)) in
|
||||
let channelView = combinedView.views[.peer(peerId: peerId)] as! PeerView
|
||||
let adminView = combinedView.views[.peer(peerId: adminId)] as! PeerView
|
||||
let canEdit = canEditAdminRights(accountPeerId: account.peerId, channelView: channelView, initialParticipant: initialParticipant)
|
||||
|
||||
let leftNavigationButton: ItemListNavigationButton
|
||||
if canEdit {
|
||||
leftNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Cancel, style: .regular, enabled: true, action: {
|
||||
dismissImpl?()
|
||||
})
|
||||
} else {
|
||||
leftNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Done, style: .bold, enabled: true, action: {
|
||||
dismissImpl?()
|
||||
})
|
||||
}
|
||||
|
||||
var rightNavigationButton: ItemListNavigationButton?
|
||||
if state.updating {
|
||||
rightNavigationButton = ItemListNavigationButton(title: "", style: .activity, enabled: true, action: {})
|
||||
} else if canEdit {
|
||||
rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Done, style: .bold, enabled: true, action: {
|
||||
var updateFlags: TelegramChannelAdminRightsFlags?
|
||||
updateState { current in
|
||||
updateFlags = current.updatedFlags
|
||||
if let _ = updateFlags {
|
||||
return current.withUpdatedUpdating(true)
|
||||
} else {
|
||||
return current
|
||||
}
|
||||
}
|
||||
if let updateFlags = updateFlags {
|
||||
updateRightsDisposable.set((updatePeerAdminRights(account: account, peerId: peerId, adminId: adminId, rights: TelegramChannelAdminRights(flags: updateFlags)) |> deliverOnMainQueue).start(error: { _ in
|
||||
|
||||
}, completed: {
|
||||
updated(TelegramChannelAdminRights(flags: updateFlags))
|
||||
dismissImpl?()
|
||||
}))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.Channel_Management_LabelEditor), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
|
||||
|
||||
let listState = ItemListNodeState(entries: channelAdminControllerEntries(presentationData: presentationData, state: state, accountPeerId: account.peerId, channelView: channelView, adminView: adminView, initialParticipant: initialParticipant), style: .blocks, emptyStateItem: nil, animateChanges: true)
|
||||
|
||||
return (controllerState, (listState, arguments))
|
||||
} |> afterDisposed {
|
||||
actionsDisposable.dispose()
|
||||
}
|
||||
|
||||
let controller = ItemListController(account: account, state: signal)
|
||||
dismissImpl = { [weak controller] in
|
||||
controller?.dismiss()
|
||||
}
|
||||
return controller
|
||||
}
|
||||