Merge branch 'igor/media-updates' into release/5.31.0
@ -754,7 +754,6 @@
|
||||
4C4AE6A1224AF35700D4AF6F /* SendMediaNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4AE69F224AF21900D4AF6F /* SendMediaNavigationController.swift */; };
|
||||
4C4AEC4520EC343B0020E72B /* DismissableTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4AEC4420EC343B0020E72B /* DismissableTextField.swift */; };
|
||||
4C4BC6C32102D697004040C9 /* ContactDiscoveryOperationTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4BC6C22102D697004040C9 /* ContactDiscoveryOperationTest.swift */; };
|
||||
4C4F5EBC22711EEB00F3DD01 /* SendMediaBottomButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4F5EBB22711EEB00F3DD01 /* SendMediaBottomButton.swift */; };
|
||||
4C5250D221E7BD7D00CE3D95 /* PhoneNumberValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5250D121E7BD7D00CE3D95 /* PhoneNumberValidator.swift */; };
|
||||
4C5250D421E7C51900CE3D95 /* PhoneNumberValidatorTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5250D321E7C51900CE3D95 /* PhoneNumberValidatorTest.swift */; };
|
||||
4C63CC00210A620B003AE45C /* SignalTSan.supp in Resources */ = {isa = PBXBuildFile; fileRef = 4C63CBFF210A620B003AE45C /* SignalTSan.supp */; };
|
||||
@ -793,6 +792,7 @@
|
||||
768A1A2B17FC9CD300E00ED8 /* libz.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 768A1A2A17FC9CD300E00ED8 /* libz.dylib */; };
|
||||
76C87F19181EFCE600C4ACAB /* MediaPlayer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 76C87F18181EFCE600C4ACAB /* MediaPlayer.framework */; };
|
||||
76EB054018170B33006006FC /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 76EB03C318170B33006006FC /* AppDelegate.m */; };
|
||||
76FCCDBC27AB8FBE00BAA7F0 /* MediaControls.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76FCCDBB27AB8FBE00BAA7F0 /* MediaControls.swift */; };
|
||||
8806EF19248DBD7200E764C7 /* NotificationPermissionReminderMegaphone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8806EF18248DBD7200E764C7 /* NotificationPermissionReminderMegaphone.swift */; };
|
||||
8806EF1B248DBFC100E764C7 /* ContactPermissionReminderMegaphone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8806EF1A248DBFC100E764C7 /* ContactPermissionReminderMegaphone.swift */; };
|
||||
8809CE8722F8FE6D00D38867 /* AttachmentKeyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8809CE8622F8FE6D00D38867 /* AttachmentKeyboard.swift */; };
|
||||
@ -1943,7 +1943,6 @@
|
||||
4C4AE69F224AF21900D4AF6F /* SendMediaNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMediaNavigationController.swift; sourceTree = "<group>"; };
|
||||
4C4AEC4420EC343B0020E72B /* DismissableTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DismissableTextField.swift; sourceTree = "<group>"; };
|
||||
4C4BC6C22102D697004040C9 /* ContactDiscoveryOperationTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ContactDiscoveryOperationTest.swift; path = contact/ContactDiscoveryOperationTest.swift; sourceTree = "<group>"; };
|
||||
4C4F5EBB22711EEB00F3DD01 /* SendMediaBottomButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMediaBottomButton.swift; sourceTree = "<group>"; };
|
||||
4C5250D121E7BD7D00CE3D95 /* PhoneNumberValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneNumberValidator.swift; sourceTree = "<group>"; };
|
||||
4C5250D321E7C51900CE3D95 /* PhoneNumberValidatorTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneNumberValidatorTest.swift; sourceTree = "<group>"; };
|
||||
4C63CBFF210A620B003AE45C /* SignalTSan.supp */ = {isa = PBXFileReference; lastKnownFileType = text; path = SignalTSan.supp; sourceTree = "<group>"; };
|
||||
@ -1989,6 +1988,7 @@
|
||||
76C87F18181EFCE600C4ACAB /* MediaPlayer.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MediaPlayer.framework; path = System/Library/Frameworks/MediaPlayer.framework; sourceTree = SDKROOT; };
|
||||
76EB03C218170B33006006FC /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
|
||||
76EB03C318170B33006006FC /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
|
||||
76FCCDBB27AB8FBE00BAA7F0 /* MediaControls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaControls.swift; sourceTree = "<group>"; };
|
||||
7856A9F703AAD99E22B75A9B /* Pods-SignalShareExtension.profiling.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalShareExtension.profiling.xcconfig"; path = "Pods/Target Support Files/Pods-SignalShareExtension/Pods-SignalShareExtension.profiling.xcconfig"; sourceTree = "<group>"; };
|
||||
7BB1CB6F2D7841356BE367EA /* Pods-Signal.testable release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Signal.testable release.xcconfig"; path = "Pods/Target Support Files/Pods-Signal/Pods-Signal.testable release.xcconfig"; sourceTree = "<group>"; };
|
||||
7C5EABE2C09180BC71C4E097 /* Pods-SignalShareExtension.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalShareExtension.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-SignalShareExtension/Pods-SignalShareExtension.app store release.xcconfig"; sourceTree = "<group>"; };
|
||||
@ -2978,13 +2978,13 @@
|
||||
children = (
|
||||
32C584A725B81C6600256804 /* AvatarViewController.swift */,
|
||||
34969559219B605E00DCFE74 /* ImagePickerController.swift */,
|
||||
76FCCDBB27AB8FBE00BAA7F0 /* MediaControls.swift */,
|
||||
4C21D5D7223AC60F00EF8A77 /* PhotoCapture.swift */,
|
||||
E44AD4E524E98F430035D7B8 /* PhotoCaptureDismiss.swift */,
|
||||
4CA485BA2232339F004B9E7D /* PhotoCaptureViewController.swift */,
|
||||
3496955A219B605E00DCFE74 /* PhotoCollectionPickerController.swift */,
|
||||
3496955B219B605E00DCFE74 /* PhotoLibrary.swift */,
|
||||
4C4F5EBB22711EEB00F3DD01 /* SendMediaBottomButton.swift */,
|
||||
4C4AE69F224AF21900D4AF6F /* SendMediaNavigationController.swift */,
|
||||
E44AD4E524E98F430035D7B8 /* PhotoCaptureDismiss.swift */,
|
||||
);
|
||||
path = Photos;
|
||||
sourceTree = "<group>";
|
||||
@ -6037,7 +6037,6 @@
|
||||
343A65981FC4CFE7000477A1 /* ConversationScrollButton.m in Sources */,
|
||||
34386A51207D0C01009F5D9C /* HomeViewController.m in Sources */,
|
||||
348EE28E25B897BF00814FC2 /* CVMediaCache.swift in Sources */,
|
||||
4C4F5EBC22711EEB00F3DD01 /* SendMediaBottomButton.swift in Sources */,
|
||||
343417F12530A7480034FE0C /* CVComponentReactions.swift in Sources */,
|
||||
88D1D40222EBB5A100F472C5 /* MessageRequestView.swift in Sources */,
|
||||
45CD81EF1DC030E7004C9430 /* SyncPushTokensJob.swift in Sources */,
|
||||
@ -6359,6 +6358,7 @@
|
||||
34ACA7DE2733159600E47AD4 /* OnboardingPhoneNumberDiscoverabilityViewController.swift in Sources */,
|
||||
88A9729222FA5D4B004B4FBF /* AttachmentFormatPickerView.swift in Sources */,
|
||||
346C19E125ACE9AE00061D3A /* MediaDownloadSettingsViewController.swift in Sources */,
|
||||
76FCCDBC27AB8FBE00BAA7F0 /* MediaControls.swift in Sources */,
|
||||
8827004E23208A1900F01C46 /* AppearanceSettingsTableViewController.swift in Sources */,
|
||||
344A761324B36C8C009D69A5 /* TestingViewController.swift in Sources */,
|
||||
3495FF0D25F934C500959D6E /* PaymentsRestoreWalletCompleteViewController.swift in Sources */,
|
||||
|
||||
@ -1,23 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "flash-auto-28@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "flash-auto-28@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "flash-auto-28@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 524 B |
|
Before Width: | Height: | Size: 995 B |
|
Before Width: | Height: | Size: 1.6 KiB |
@ -1,23 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "flash-outline-28@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "flash-outline-28@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "flash-outline-28@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 493 B |
|
Before Width: | Height: | Size: 860 B |
|
Before Width: | Height: | Size: 1.4 KiB |
@ -1,23 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "flash-filled-28@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "flash-filled-28@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "flash-filled-28@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 399 B |
|
Before Width: | Height: | Size: 750 B |
|
Before Width: | Height: | Size: 1.0 KiB |
@ -1,23 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "switch-camera-28@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "switch-camera-28@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "switch-camera-28@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 425 B |
|
Before Width: | Height: | Size: 808 B |
|
Before Width: | Height: | Size: 1.2 KiB |
@ -1,23 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "x-24@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "x-24@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "x-24@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 243 B |
|
Before Width: | Height: | Size: 398 B |
|
Before Width: | Height: | Size: 573 B |
15
Signal/Images.xcassets/media-composer-checkmark.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "media-composer-checkmark.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
BIN
Signal/Images.xcassets/media-composer-checkmark.imageset/media-composer-checkmark.pdf
vendored
Normal file
15
Signal/Images.xcassets/media-composer-close.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "media_composer_close.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
229
Signal/Images.xcassets/media-composer-close.imageset/media_composer_close.pdf
vendored
Normal file
@ -0,0 +1,229 @@
|
||||
%PDF-1.7
|
||||
|
||||
1 0 obj
|
||||
<< >>
|
||||
endobj
|
||||
|
||||
2 0 obj
|
||||
<< /Length 3 0 R >>
|
||||
stream
|
||||
/DeviceRGB CS
|
||||
/DeviceRGB cs
|
||||
q
|
||||
1.000000 0.000000 -0.000000 1.000000 4.469971 4.469727 cm
|
||||
0.000000 0.000000 0.000000 scn
|
||||
15.060001 14.000058 m
|
||||
14.000000 15.060059 l
|
||||
7.530000 8.591059 l
|
||||
1.060000 15.060059 l
|
||||
0.000000 14.000058 l
|
||||
6.469000 7.530058 l
|
||||
0.000000 1.060059 l
|
||||
1.060000 0.000057 l
|
||||
7.530000 6.469059 l
|
||||
14.000000 0.000057 l
|
||||
15.060001 1.060059 l
|
||||
8.591000 7.530058 l
|
||||
15.060001 14.000058 l
|
||||
h
|
||||
f
|
||||
n
|
||||
Q
|
||||
q
|
||||
1.000000 0.000000 -0.000000 1.000000 4.469971 3.409180 cm
|
||||
0.000000 0.000000 0.000000 scn
|
||||
15.060001 15.060605 m
|
||||
15.325187 14.795461 l
|
||||
15.590311 15.060626 l
|
||||
15.325167 15.325770 l
|
||||
15.060001 15.060605 l
|
||||
h
|
||||
14.000000 16.120605 m
|
||||
14.265165 16.385771 l
|
||||
14.000020 16.650915 l
|
||||
13.734856 16.385792 l
|
||||
14.000000 16.120605 l
|
||||
h
|
||||
7.530000 9.651606 m
|
||||
7.264856 9.386419 l
|
||||
7.530000 9.121316 l
|
||||
7.795145 9.386419 l
|
||||
7.530000 9.651606 l
|
||||
h
|
||||
1.060000 16.120605 m
|
||||
1.325145 16.385792 l
|
||||
1.059980 16.650915 l
|
||||
0.794835 16.385771 l
|
||||
1.060000 16.120605 l
|
||||
h
|
||||
0.000000 15.060605 m
|
||||
-0.265165 15.325770 l
|
||||
-0.530310 15.060625 l
|
||||
-0.265186 14.795461 l
|
||||
0.000000 15.060605 l
|
||||
h
|
||||
6.469000 8.590605 m
|
||||
6.734186 8.325460 l
|
||||
6.999290 8.590605 l
|
||||
6.734186 8.855749 l
|
||||
6.469000 8.590605 l
|
||||
h
|
||||
0.000000 2.120605 m
|
||||
-0.265186 2.385750 l
|
||||
-0.530309 2.120585 l
|
||||
-0.265165 1.855440 l
|
||||
0.000000 2.120605 l
|
||||
h
|
||||
1.060000 1.060604 m
|
||||
0.794835 0.795439 l
|
||||
1.059980 0.530294 l
|
||||
1.325145 0.795419 l
|
||||
1.060000 1.060604 l
|
||||
h
|
||||
7.530000 7.529606 m
|
||||
7.795145 7.794791 l
|
||||
7.530000 8.059896 l
|
||||
7.264855 7.794791 l
|
||||
7.530000 7.529606 l
|
||||
h
|
||||
14.000000 1.060604 m
|
||||
13.734856 0.795419 l
|
||||
14.000020 0.530294 l
|
||||
14.265165 0.795439 l
|
||||
14.000000 1.060604 l
|
||||
h
|
||||
15.060001 2.120605 m
|
||||
15.325167 1.855440 l
|
||||
15.590311 2.120585 l
|
||||
15.325187 2.385750 l
|
||||
15.060001 2.120605 l
|
||||
h
|
||||
8.591000 8.590605 m
|
||||
8.325814 8.855750 l
|
||||
8.060710 8.590605 l
|
||||
8.325814 8.325460 l
|
||||
8.591000 8.590605 l
|
||||
h
|
||||
15.325167 15.325770 m
|
||||
14.265165 16.385771 l
|
||||
13.734835 15.855440 l
|
||||
14.794836 14.795440 l
|
||||
15.325167 15.325770 l
|
||||
h
|
||||
13.734856 16.385792 m
|
||||
7.264856 9.916790 l
|
||||
7.795145 9.386419 l
|
||||
14.265144 15.855420 l
|
||||
13.734856 16.385792 l
|
||||
h
|
||||
7.795145 9.916790 m
|
||||
1.325145 16.385792 l
|
||||
0.794856 15.855420 l
|
||||
7.264856 9.386419 l
|
||||
7.795145 9.916790 l
|
||||
h
|
||||
0.794835 16.385771 m
|
||||
-0.265165 15.325770 l
|
||||
0.265165 14.795440 l
|
||||
1.325166 15.855440 l
|
||||
0.794835 16.385771 l
|
||||
h
|
||||
-0.265186 14.795461 m
|
||||
6.203815 8.325460 l
|
||||
6.734186 8.855749 l
|
||||
0.265186 15.325749 l
|
||||
-0.265186 14.795461 l
|
||||
h
|
||||
6.203815 8.855749 m
|
||||
-0.265186 2.385750 l
|
||||
0.265186 1.855461 l
|
||||
6.734186 8.325460 l
|
||||
6.203815 8.855749 l
|
||||
h
|
||||
-0.265165 1.855440 m
|
||||
0.794835 0.795439 l
|
||||
1.325166 1.325769 l
|
||||
0.265165 2.385771 l
|
||||
-0.265165 1.855440 l
|
||||
h
|
||||
1.325145 0.795419 m
|
||||
7.795145 7.264421 l
|
||||
7.264855 7.794791 l
|
||||
0.794856 1.325789 l
|
||||
1.325145 0.795419 l
|
||||
h
|
||||
7.264855 7.264421 m
|
||||
13.734856 0.795419 l
|
||||
14.265144 1.325789 l
|
||||
7.795145 7.794791 l
|
||||
7.264855 7.264421 l
|
||||
h
|
||||
14.265165 0.795439 m
|
||||
15.325167 1.855440 l
|
||||
14.794836 2.385771 l
|
||||
13.734835 1.325769 l
|
||||
14.265165 0.795439 l
|
||||
h
|
||||
15.325187 2.385750 m
|
||||
8.856185 8.855750 l
|
||||
8.325814 8.325460 l
|
||||
14.794816 1.855461 l
|
||||
15.325187 2.385750 l
|
||||
h
|
||||
8.856185 8.325460 m
|
||||
15.325187 14.795461 l
|
||||
14.794816 15.325749 l
|
||||
8.325814 8.855750 l
|
||||
8.856185 8.325460 l
|
||||
h
|
||||
f
|
||||
n
|
||||
Q
|
||||
|
||||
endstream
|
||||
endobj
|
||||
|
||||
3 0 obj
|
||||
3034
|
||||
endobj
|
||||
|
||||
4 0 obj
|
||||
<< /Annots []
|
||||
/Type /Page
|
||||
/MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
|
||||
/Resources 1 0 R
|
||||
/Contents 2 0 R
|
||||
/Parent 5 0 R
|
||||
>>
|
||||
endobj
|
||||
|
||||
5 0 obj
|
||||
<< /Kids [ 4 0 R ]
|
||||
/Count 1
|
||||
/Type /Pages
|
||||
>>
|
||||
endobj
|
||||
|
||||
6 0 obj
|
||||
<< /Pages 5 0 R
|
||||
/Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
|
||||
xref
|
||||
0 7
|
||||
0000000000 65535 f
|
||||
0000000010 00000 n
|
||||
0000000034 00000 n
|
||||
0000003124 00000 n
|
||||
0000003147 00000 n
|
||||
0000003320 00000 n
|
||||
0000003394 00000 n
|
||||
trailer
|
||||
<< /ID [ (some) (id) ]
|
||||
/Root 6 0 R
|
||||
/Size 7
|
||||
>>
|
||||
startxref
|
||||
3453
|
||||
%%EOF
|
||||
15
Signal/Images.xcassets/media-composer-create-album-outline-24.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "create-album-outline-24.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
121
Signal/Images.xcassets/media-composer-create-album-outline-24.imageset/create-album-outline-24.pdf
vendored
Normal file
@ -0,0 +1,121 @@
|
||||
%PDF-1.7
|
||||
|
||||
1 0 obj
|
||||
<< >>
|
||||
endobj
|
||||
|
||||
2 0 obj
|
||||
<< /Length 3 0 R >>
|
||||
stream
|
||||
/DeviceRGB CS
|
||||
/DeviceRGB cs
|
||||
q
|
||||
1.000000 0.000000 -0.000000 1.000000 2.000000 2.000000 cm
|
||||
0.000000 0.000000 0.000000 scn
|
||||
16.584000 1.500000 m
|
||||
16.322899 1.045122 15.946626 0.667013 15.493025 0.403700 c
|
||||
15.039424 0.140387 14.524487 0.001152 14.000000 0.000000 c
|
||||
4.500000 0.000000 l
|
||||
3.306526 0.000000 2.161933 0.474106 1.318019 1.318020 c
|
||||
0.474106 2.161934 0.000000 3.306526 0.000000 4.500000 c
|
||||
0.000000 14.000000 l
|
||||
0.001153 14.524487 0.140387 15.039425 0.403700 15.493025 c
|
||||
0.667013 15.946627 1.045122 16.322899 1.500000 16.584000 c
|
||||
1.500000 4.500000 l
|
||||
1.500000 3.704351 1.816071 2.941288 2.378680 2.378679 c
|
||||
2.941289 1.816071 3.704350 1.500000 4.500000 1.500000 c
|
||||
16.584000 1.500000 l
|
||||
h
|
||||
17.000000 18.500000 m
|
||||
6.000000 18.500000 l
|
||||
5.602175 18.500000 5.220644 18.341965 4.939340 18.060659 c
|
||||
4.658035 17.779356 4.500000 17.397825 4.500000 17.000000 c
|
||||
4.500000 6.000000 l
|
||||
4.500000 5.602176 4.658035 5.220645 4.939340 4.939341 c
|
||||
5.220644 4.658036 5.602175 4.500000 6.000000 4.500000 c
|
||||
17.000000 4.500000 l
|
||||
17.397825 4.500000 17.779354 4.658036 18.060659 4.939341 c
|
||||
18.341965 5.220645 18.500000 5.602176 18.500000 6.000000 c
|
||||
18.500000 17.000000 l
|
||||
18.500000 17.397825 18.341965 17.779356 18.060659 18.060659 c
|
||||
17.779354 18.341965 17.397825 18.500000 17.000000 18.500000 c
|
||||
h
|
||||
17.000000 20.000000 m
|
||||
17.795649 20.000000 18.558712 19.683929 19.121321 19.121321 c
|
||||
19.683929 18.558712 20.000000 17.795650 20.000000 17.000000 c
|
||||
20.000000 6.000000 l
|
||||
20.000000 5.204350 19.683929 4.441288 19.121321 3.878679 c
|
||||
18.558712 3.316071 17.795649 3.000000 17.000000 3.000000 c
|
||||
6.000000 3.000000 l
|
||||
5.204350 3.000000 4.441289 3.316071 3.878680 3.878679 c
|
||||
3.316071 4.441288 3.000000 5.204350 3.000000 6.000000 c
|
||||
3.000000 17.000000 l
|
||||
3.000000 17.795650 3.316071 18.558712 3.878680 19.121321 c
|
||||
4.441289 19.683929 5.204350 20.000000 6.000000 20.000000 c
|
||||
17.000000 20.000000 l
|
||||
h
|
||||
15.500000 12.250000 m
|
||||
12.250000 12.250000 l
|
||||
12.250000 15.500000 l
|
||||
10.750000 15.500000 l
|
||||
10.750000 12.250000 l
|
||||
7.500000 12.250000 l
|
||||
7.500000 10.750000 l
|
||||
10.750000 10.750000 l
|
||||
10.750000 7.500000 l
|
||||
12.250000 7.500000 l
|
||||
12.250000 10.750000 l
|
||||
15.500000 10.750000 l
|
||||
15.500000 12.250000 l
|
||||
h
|
||||
f
|
||||
n
|
||||
Q
|
||||
|
||||
endstream
|
||||
endobj
|
||||
|
||||
3 0 obj
|
||||
2134
|
||||
endobj
|
||||
|
||||
4 0 obj
|
||||
<< /Annots []
|
||||
/Type /Page
|
||||
/MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
|
||||
/Resources 1 0 R
|
||||
/Contents 2 0 R
|
||||
/Parent 5 0 R
|
||||
>>
|
||||
endobj
|
||||
|
||||
5 0 obj
|
||||
<< /Kids [ 4 0 R ]
|
||||
/Count 1
|
||||
/Type /Pages
|
||||
>>
|
||||
endobj
|
||||
|
||||
6 0 obj
|
||||
<< /Pages 5 0 R
|
||||
/Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
|
||||
xref
|
||||
0 7
|
||||
0000000000 65535 f
|
||||
0000000010 00000 n
|
||||
0000000034 00000 n
|
||||
0000002224 00000 n
|
||||
0000002247 00000 n
|
||||
0000002420 00000 n
|
||||
0000002494 00000 n
|
||||
trailer
|
||||
<< /ID [ (some) (id) ]
|
||||
/Root 6 0 R
|
||||
/Size 7
|
||||
>>
|
||||
startxref
|
||||
2553
|
||||
%%EOF
|
||||
15
Signal/Images.xcassets/media-composer-create-album-solid-24.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "create-album-solid-24.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
BIN
Signal/Images.xcassets/media-composer-create-album-solid-24.imageset/create-album-solid-24.pdf
vendored
Normal file
15
Signal/Images.xcassets/media-composer-flash-auto-24.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "media-composer-flash-auto-24.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
125
Signal/Images.xcassets/media-composer-flash-auto-24.imageset/media-composer-flash-auto-24.pdf
vendored
Normal file
@ -0,0 +1,125 @@
|
||||
%PDF-1.7
|
||||
|
||||
1 0 obj
|
||||
<< >>
|
||||
endobj
|
||||
|
||||
2 0 obj
|
||||
<< /Length 3 0 R >>
|
||||
stream
|
||||
/DeviceRGB CS
|
||||
/DeviceRGB cs
|
||||
q
|
||||
1.000000 0.000000 -0.000000 1.000000 3.148315 0.646362 cm
|
||||
0.000000 0.000000 0.000000 scn
|
||||
10.394505 22.621460 m
|
||||
10.186519 22.621460 9.971912 22.518707 9.860236 22.307106 c
|
||||
0.186246 9.979193 l
|
||||
0.027322 9.816003 -0.018069 9.583500 0.006074 9.378285 c
|
||||
0.030281 9.172525 0.128247 8.961831 0.296594 8.849599 c
|
||||
0.303070 8.845283 l
|
||||
0.312180 8.840727 l
|
||||
0.396315 8.798658 0.484727 8.754453 0.563120 8.720855 c
|
||||
0.602809 8.703845 0.642242 8.688539 0.679090 8.677324 c
|
||||
0.714395 8.666579 0.754920 8.657176 0.794503 8.657176 c
|
||||
8.269956 8.657176 l
|
||||
6.672553 0.838310 l
|
||||
6.566589 0.407591 6.894166 0.102909 7.183547 0.006449 c
|
||||
7.202790 0.000034 l
|
||||
7.308789 0.000034 l
|
||||
7.515537 0.000034 7.718171 0.100496 7.825749 0.208075 c
|
||||
7.831357 0.213682 l
|
||||
17.517363 12.728341 l
|
||||
17.676031 12.891515 17.721346 13.123846 17.697220 13.328923 c
|
||||
17.673504 13.530506 17.578995 13.736824 17.416866 13.850653 c
|
||||
17.307270 13.954901 17.109980 14.050033 16.908789 14.050033 c
|
||||
9.433671 14.050033 l
|
||||
11.030709 21.783056 l
|
||||
11.136769 22.213842 10.809155 22.518576 10.519747 22.615046 c
|
||||
10.515336 22.601814 l
|
||||
10.495173 22.615263 10.479262 22.621460 10.479256 22.621460 c
|
||||
10.394505 22.621460 l
|
||||
h
|
||||
8.522223 18.133879 m
|
||||
8.967959 18.822742 l
|
||||
8.731995 18.077103 l
|
||||
7.495820 12.514318 l
|
||||
15.035454 12.514318 l
|
||||
15.499903 12.607208 l
|
||||
15.100375 12.207681 l
|
||||
9.094956 4.401608 l
|
||||
8.752861 3.854254 l
|
||||
8.971349 4.544376 l
|
||||
9.830936 8.498480 l
|
||||
10.207474 10.192890 l
|
||||
2.755587 10.192890 l
|
||||
2.144394 10.091025 l
|
||||
2.600486 10.496440 l
|
||||
8.522223 18.133879 l
|
||||
h
|
||||
17.606695 7.792892 m
|
||||
15.095354 7.792892 l
|
||||
13.494057 0.942892 l
|
||||
15.034959 0.942892 l
|
||||
15.377815 2.314320 l
|
||||
17.411192 2.314320 l
|
||||
17.754049 0.942892 l
|
||||
19.296955 0.942892 l
|
||||
17.606695 7.792892 l
|
||||
h
|
||||
15.693610 3.764320 m
|
||||
16.394505 6.879395 l
|
||||
17.095398 3.764320 l
|
||||
15.693610 3.764320 l
|
||||
h
|
||||
f*
|
||||
n
|
||||
Q
|
||||
|
||||
endstream
|
||||
endobj
|
||||
|
||||
3 0 obj
|
||||
1797
|
||||
endobj
|
||||
|
||||
4 0 obj
|
||||
<< /Annots []
|
||||
/Type /Page
|
||||
/MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
|
||||
/Resources 1 0 R
|
||||
/Contents 2 0 R
|
||||
/Parent 5 0 R
|
||||
>>
|
||||
endobj
|
||||
|
||||
5 0 obj
|
||||
<< /Kids [ 4 0 R ]
|
||||
/Count 1
|
||||
/Type /Pages
|
||||
>>
|
||||
endobj
|
||||
|
||||
6 0 obj
|
||||
<< /Pages 5 0 R
|
||||
/Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
|
||||
xref
|
||||
0 7
|
||||
0000000000 65535 f
|
||||
0000000010 00000 n
|
||||
0000000034 00000 n
|
||||
0000001887 00000 n
|
||||
0000001910 00000 n
|
||||
0000002083 00000 n
|
||||
0000002157 00000 n
|
||||
trailer
|
||||
<< /ID [ (some) (id) ]
|
||||
/Root 6 0 R
|
||||
/Size 7
|
||||
>>
|
||||
startxref
|
||||
2216
|
||||
%%EOF
|
||||
15
Signal/Images.xcassets/media-composer-flash-filled-24.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "media-composer-flash-filled-24.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,93 @@
|
||||
%PDF-1.7
|
||||
|
||||
1 0 obj
|
||||
<< >>
|
||||
endobj
|
||||
|
||||
2 0 obj
|
||||
<< /Length 3 0 R >>
|
||||
stream
|
||||
/DeviceRGB CS
|
||||
/DeviceRGB cs
|
||||
q
|
||||
1.000000 0.000000 -0.000000 1.000000 3.148315 0.646362 cm
|
||||
0.000000 0.000000 0.000000 scn
|
||||
10.394504 22.621460 m
|
||||
10.186520 22.621460 9.971915 22.518707 9.860238 22.307110 c
|
||||
0.186246 9.979193 l
|
||||
0.027322 9.816003 -0.018069 9.583500 0.006074 9.378285 c
|
||||
0.030281 9.172525 0.128247 8.961831 0.296594 8.849599 c
|
||||
0.303070 8.845283 l
|
||||
0.312179 8.840727 l
|
||||
0.396315 8.798658 0.484726 8.754453 0.563120 8.720855 c
|
||||
0.602809 8.703845 0.642242 8.688539 0.679090 8.677324 c
|
||||
0.714395 8.666579 0.754920 8.657176 0.794503 8.657176 c
|
||||
8.269956 8.657176 l
|
||||
6.672552 0.838310 l
|
||||
6.566589 0.407591 6.894166 0.102909 7.183546 0.006449 c
|
||||
7.202790 0.000034 l
|
||||
7.308788 0.000034 l
|
||||
7.515537 0.000034 7.718170 0.100496 7.825748 0.208075 c
|
||||
7.831357 0.213682 l
|
||||
17.517353 12.728332 l
|
||||
17.676027 12.891506 17.721346 13.123842 17.697218 13.328923 c
|
||||
17.673502 13.530502 17.578995 13.736816 17.416872 13.850647 c
|
||||
17.307278 13.954897 17.109980 14.050033 16.908789 14.050033 c
|
||||
9.433671 14.050033 l
|
||||
11.030708 21.783056 l
|
||||
11.136768 22.213842 10.809155 22.518576 10.519746 22.615046 c
|
||||
10.515335 22.601814 l
|
||||
10.495173 22.615263 10.479261 22.621460 10.479255 22.621460 c
|
||||
10.394504 22.621460 l
|
||||
h
|
||||
f*
|
||||
n
|
||||
Q
|
||||
|
||||
endstream
|
||||
endobj
|
||||
|
||||
3 0 obj
|
||||
1182
|
||||
endobj
|
||||
|
||||
4 0 obj
|
||||
<< /Annots []
|
||||
/Type /Page
|
||||
/MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
|
||||
/Resources 1 0 R
|
||||
/Contents 2 0 R
|
||||
/Parent 5 0 R
|
||||
>>
|
||||
endobj
|
||||
|
||||
5 0 obj
|
||||
<< /Kids [ 4 0 R ]
|
||||
/Count 1
|
||||
/Type /Pages
|
||||
>>
|
||||
endobj
|
||||
|
||||
6 0 obj
|
||||
<< /Pages 5 0 R
|
||||
/Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
|
||||
xref
|
||||
0 7
|
||||
0000000000 65535 f
|
||||
0000000010 00000 n
|
||||
0000000034 00000 n
|
||||
0000001272 00000 n
|
||||
0000001295 00000 n
|
||||
0000001468 00000 n
|
||||
0000001542 00000 n
|
||||
trailer
|
||||
<< /ID [ (some) (id) ]
|
||||
/Root 6 0 R
|
||||
/Size 7
|
||||
>>
|
||||
startxref
|
||||
1601
|
||||
%%EOF
|
||||
15
Signal/Images.xcassets/media-composer-flash-outline-24.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "media-composer-flash-outline-24.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
110
Signal/Images.xcassets/media-composer-flash-outline-24.imageset/media-composer-flash-outline-24.pdf
vendored
Normal file
@ -0,0 +1,110 @@
|
||||
%PDF-1.7
|
||||
|
||||
1 0 obj
|
||||
<< >>
|
||||
endobj
|
||||
|
||||
2 0 obj
|
||||
<< /Length 3 0 R >>
|
||||
stream
|
||||
/DeviceRGB CS
|
||||
/DeviceRGB cs
|
||||
q
|
||||
1.000000 0.000000 -0.000000 1.000000 3.148315 0.646362 cm
|
||||
0.000000 0.000000 0.000000 scn
|
||||
10.394504 22.621460 m
|
||||
10.186520 22.621460 9.971915 22.518707 9.860238 22.307110 c
|
||||
0.186246 9.979193 l
|
||||
0.027322 9.816003 -0.018069 9.583500 0.006074 9.378285 c
|
||||
0.030281 9.172525 0.128247 8.961831 0.296594 8.849599 c
|
||||
0.303070 8.845283 l
|
||||
0.312179 8.840727 l
|
||||
0.396315 8.798658 0.484726 8.754453 0.563120 8.720855 c
|
||||
0.602809 8.703845 0.642242 8.688539 0.679090 8.677324 c
|
||||
0.714395 8.666579 0.754920 8.657176 0.794503 8.657176 c
|
||||
8.269956 8.657176 l
|
||||
6.672552 0.838310 l
|
||||
6.566589 0.407591 6.894166 0.102909 7.183546 0.006449 c
|
||||
7.202790 0.000034 l
|
||||
7.308788 0.000034 l
|
||||
7.515537 0.000034 7.718170 0.100496 7.825748 0.208075 c
|
||||
7.831357 0.213682 l
|
||||
17.517353 12.728332 l
|
||||
17.676027 12.891506 17.721346 13.123842 17.697218 13.328923 c
|
||||
17.673502 13.530502 17.578995 13.736816 17.416872 13.850647 c
|
||||
17.307278 13.954897 17.109980 14.050033 16.908789 14.050033 c
|
||||
9.433671 14.050033 l
|
||||
11.030708 21.783056 l
|
||||
11.136768 22.213842 10.809155 22.518576 10.519746 22.615046 c
|
||||
10.515335 22.601814 l
|
||||
10.495173 22.615263 10.479261 22.621460 10.479255 22.621460 c
|
||||
10.394504 22.621460 l
|
||||
h
|
||||
8.522223 18.133879 m
|
||||
8.967960 18.822746 l
|
||||
8.731994 18.077103 l
|
||||
7.495819 12.514318 l
|
||||
15.035453 12.514318 l
|
||||
15.499902 12.607208 l
|
||||
15.100374 12.207681 l
|
||||
9.094956 4.401608 l
|
||||
8.752862 3.854258 l
|
||||
8.971348 4.544376 l
|
||||
9.830936 8.498480 l
|
||||
10.207473 10.192890 l
|
||||
2.755587 10.192890 l
|
||||
2.144394 10.091025 l
|
||||
2.600486 10.496440 l
|
||||
8.522223 18.133879 l
|
||||
h
|
||||
f*
|
||||
n
|
||||
Q
|
||||
|
||||
endstream
|
||||
endobj
|
||||
|
||||
3 0 obj
|
||||
1520
|
||||
endobj
|
||||
|
||||
4 0 obj
|
||||
<< /Annots []
|
||||
/Type /Page
|
||||
/MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
|
||||
/Resources 1 0 R
|
||||
/Contents 2 0 R
|
||||
/Parent 5 0 R
|
||||
>>
|
||||
endobj
|
||||
|
||||
5 0 obj
|
||||
<< /Kids [ 4 0 R ]
|
||||
/Count 1
|
||||
/Type /Pages
|
||||
>>
|
||||
endobj
|
||||
|
||||
6 0 obj
|
||||
<< /Pages 5 0 R
|
||||
/Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
|
||||
xref
|
||||
0 7
|
||||
0000000000 65535 f
|
||||
0000000010 00000 n
|
||||
0000000034 00000 n
|
||||
0000001610 00000 n
|
||||
0000001633 00000 n
|
||||
0000001806 00000 n
|
||||
0000001880 00000 n
|
||||
trailer
|
||||
<< /ID [ (some) (id) ]
|
||||
/Root 6 0 R
|
||||
/Size 7
|
||||
>>
|
||||
startxref
|
||||
1939
|
||||
%%EOF
|
||||
15
Signal/Images.xcassets/media-composer-lock-outline-24.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "media-composer-lock-outline-24.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
121
Signal/Images.xcassets/media-composer-lock-outline-24.imageset/media-composer-lock-outline-24.pdf
vendored
Normal file
@ -0,0 +1,121 @@
|
||||
%PDF-1.7
|
||||
|
||||
1 0 obj
|
||||
<< >>
|
||||
endobj
|
||||
|
||||
2 0 obj
|
||||
<< /Length 3 0 R >>
|
||||
stream
|
||||
/DeviceRGB CS
|
||||
/DeviceRGB cs
|
||||
q
|
||||
1.000000 0.000000 -0.000000 1.000000 4.000000 2.000000 cm
|
||||
0.105882 0.105882 0.105882 scn
|
||||
13.000000 13.000000 m
|
||||
13.000000 16.000000 l
|
||||
13.000000 17.326082 12.473216 18.597853 11.535534 19.535534 c
|
||||
10.597852 20.473215 9.326082 21.000000 8.000000 21.000000 c
|
||||
6.673918 21.000000 5.402148 20.473215 4.464466 19.535534 c
|
||||
3.526784 18.597853 3.000000 17.326082 3.000000 16.000000 c
|
||||
3.000000 13.000000 l
|
||||
2.204350 13.000000 1.441289 12.683929 0.878680 12.121321 c
|
||||
0.316071 11.558712 0.000000 10.795650 0.000000 10.000000 c
|
||||
0.000000 3.000000 l
|
||||
0.000000 2.204351 0.316071 1.441288 0.878680 0.878679 c
|
||||
1.441289 0.316071 2.204350 0.000000 3.000000 0.000000 c
|
||||
13.000000 0.000000 l
|
||||
13.795650 0.000000 14.558712 0.316071 15.121321 0.878679 c
|
||||
15.683929 1.441288 16.000000 2.204351 16.000000 3.000000 c
|
||||
16.000000 10.000000 l
|
||||
16.000000 10.795650 15.683929 11.558712 15.121321 12.121321 c
|
||||
14.558712 12.683929 13.795650 13.000000 13.000000 13.000000 c
|
||||
h
|
||||
4.500000 16.000000 m
|
||||
4.500000 16.928257 4.868749 17.818497 5.525126 18.474874 c
|
||||
6.181504 19.131250 7.071742 19.500000 8.000000 19.500000 c
|
||||
8.928258 19.500000 9.818497 19.131250 10.474874 18.474874 c
|
||||
11.131250 17.818497 11.500000 16.928257 11.500000 16.000000 c
|
||||
11.500000 13.000000 l
|
||||
4.500000 13.000000 l
|
||||
4.500000 16.000000 l
|
||||
h
|
||||
14.500000 3.000000 m
|
||||
14.500000 2.602175 14.341964 2.220646 14.060659 1.939341 c
|
||||
13.779355 1.658035 13.397824 1.500000 13.000000 1.500000 c
|
||||
3.000000 1.500000 l
|
||||
2.602175 1.500000 2.220644 1.658035 1.939340 1.939341 c
|
||||
1.658035 2.220646 1.500000 2.602175 1.500000 3.000000 c
|
||||
1.500000 10.000000 l
|
||||
1.500000 10.397824 1.658035 10.779356 1.939340 11.060660 c
|
||||
2.220644 11.341965 2.602175 11.500000 3.000000 11.500000 c
|
||||
13.000000 11.500000 l
|
||||
13.397824 11.500000 13.779355 11.341965 14.060659 11.060660 c
|
||||
14.341964 10.779356 14.500000 10.397824 14.500000 10.000000 c
|
||||
14.500000 3.000000 l
|
||||
h
|
||||
8.750000 6.208000 m
|
||||
8.750000 4.000000 l
|
||||
7.250000 4.000000 l
|
||||
7.250000 6.208000 l
|
||||
6.964037 6.373101 6.740543 6.627947 6.614181 6.933013 c
|
||||
6.487818 7.238079 6.465649 7.576317 6.551111 7.895267 c
|
||||
6.636574 8.214216 6.824892 8.496055 7.086858 8.697068 c
|
||||
7.348824 8.898082 7.669799 9.007038 8.000000 9.007038 c
|
||||
8.330201 9.007038 8.651175 8.898082 8.913142 8.697068 c
|
||||
9.175109 8.496055 9.363426 8.214216 9.448889 7.895267 c
|
||||
9.534351 7.576317 9.512182 7.238079 9.385819 6.933013 c
|
||||
9.259457 6.627947 9.035963 6.373101 8.750000 6.208000 c
|
||||
8.750000 6.208000 l
|
||||
h
|
||||
f
|
||||
n
|
||||
Q
|
||||
|
||||
endstream
|
||||
endobj
|
||||
|
||||
3 0 obj
|
||||
2423
|
||||
endobj
|
||||
|
||||
4 0 obj
|
||||
<< /Annots []
|
||||
/Type /Page
|
||||
/MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
|
||||
/Resources 1 0 R
|
||||
/Contents 2 0 R
|
||||
/Parent 5 0 R
|
||||
>>
|
||||
endobj
|
||||
|
||||
5 0 obj
|
||||
<< /Kids [ 4 0 R ]
|
||||
/Count 1
|
||||
/Type /Pages
|
||||
>>
|
||||
endobj
|
||||
|
||||
6 0 obj
|
||||
<< /Pages 5 0 R
|
||||
/Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
|
||||
xref
|
||||
0 7
|
||||
0000000000 65535 f
|
||||
0000000010 00000 n
|
||||
0000000034 00000 n
|
||||
0000002513 00000 n
|
||||
0000002536 00000 n
|
||||
0000002709 00000 n
|
||||
0000002783 00000 n
|
||||
trailer
|
||||
<< /ID [ (some) (id) ]
|
||||
/Root 6 0 R
|
||||
/Size 7
|
||||
>>
|
||||
startxref
|
||||
2842
|
||||
%%EOF
|
||||
15
Signal/Images.xcassets/media-composer-navbar-chevron.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "media-composer-navbar-chevron.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
73
Signal/Images.xcassets/media-composer-navbar-chevron.imageset/media-composer-navbar-chevron.pdf
vendored
Normal file
@ -0,0 +1,73 @@
|
||||
%PDF-1.7
|
||||
|
||||
1 0 obj
|
||||
<< >>
|
||||
endobj
|
||||
|
||||
2 0 obj
|
||||
<< /Length 3 0 R >>
|
||||
stream
|
||||
/DeviceRGB CS
|
||||
/DeviceRGB cs
|
||||
q
|
||||
0.000000 -1.000000 1.000000 0.000000 4.995239 8.406494 cm
|
||||
0.000000 0.000000 0.000000 scn
|
||||
5.508584 1.675859 m
|
||||
0.000000 6.963662 l
|
||||
1.385004 8.406494 l
|
||||
8.406458 1.666456 l
|
||||
1.375829 -4.995262 l
|
||||
0.000217 -3.543472 l
|
||||
5.508584 1.675859 l
|
||||
h
|
||||
f
|
||||
n
|
||||
Q
|
||||
|
||||
endstream
|
||||
endobj
|
||||
|
||||
3 0 obj
|
||||
269
|
||||
endobj
|
||||
|
||||
4 0 obj
|
||||
<< /Annots []
|
||||
/Type /Page
|
||||
/MediaBox [ 0.000000 0.000000 13.401733 8.406494 ]
|
||||
/Resources 1 0 R
|
||||
/Contents 2 0 R
|
||||
/Parent 5 0 R
|
||||
>>
|
||||
endobj
|
||||
|
||||
5 0 obj
|
||||
<< /Kids [ 4 0 R ]
|
||||
/Count 1
|
||||
/Type /Pages
|
||||
>>
|
||||
endobj
|
||||
|
||||
6 0 obj
|
||||
<< /Pages 5 0 R
|
||||
/Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
|
||||
xref
|
||||
0 7
|
||||
0000000000 65535 f
|
||||
0000000010 00000 n
|
||||
0000000034 00000 n
|
||||
0000000359 00000 n
|
||||
0000000381 00000 n
|
||||
0000000553 00000 n
|
||||
0000000627 00000 n
|
||||
trailer
|
||||
<< /ID [ (some) (id) ]
|
||||
/Root 6 0 R
|
||||
/Size 7
|
||||
>>
|
||||
startxref
|
||||
686
|
||||
%%EOF
|
||||
15
Signal/Images.xcassets/media-composer-switch-camera-24.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "switch-camera-24.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
108
Signal/Images.xcassets/media-composer-switch-camera-24.imageset/switch-camera-24.pdf
vendored
Normal file
@ -0,0 +1,108 @@
|
||||
%PDF-1.7
|
||||
|
||||
1 0 obj
|
||||
<< >>
|
||||
endobj
|
||||
|
||||
2 0 obj
|
||||
<< /Length 3 0 R >>
|
||||
stream
|
||||
/DeviceRGB CS
|
||||
/DeviceRGB cs
|
||||
q
|
||||
1.000000 0.000000 -0.000000 1.000000 0.209961 2.659668 cm
|
||||
0.000000 0.000000 0.000000 scn
|
||||
18.150002 15.700432 m
|
||||
17.090000 14.640432 l
|
||||
16.396545 15.340194 15.570852 15.895062 14.660963 16.272751 c
|
||||
13.751074 16.650442 12.775159 16.843410 11.790000 16.840431 c
|
||||
9.800877 16.840431 7.893222 16.050255 6.486699 14.643732 c
|
||||
5.080176 13.237209 4.290000 11.329556 4.290000 9.340431 c
|
||||
4.290000 8.640431 l
|
||||
5.000000 9.860432 l
|
||||
6.000000 10.860432 l
|
||||
7.070000 9.790431 l
|
||||
3.540000 6.280431 l
|
||||
0.000000 9.810431 l
|
||||
1.070000 10.880432 l
|
||||
2.070000 9.880431 l
|
||||
2.790000 8.640431 l
|
||||
2.790000 9.340431 l
|
||||
2.791565 11.119312 3.320253 12.857834 4.309292 14.336420 c
|
||||
5.298331 15.815008 6.703359 16.967339 8.346918 17.647875 c
|
||||
9.990478 18.328411 11.798848 18.506628 13.543634 18.160015 c
|
||||
15.288421 17.813402 16.891361 16.957508 18.150002 15.700432 c
|
||||
18.150002 15.700432 l
|
||||
h
|
||||
20.040001 12.400433 m
|
||||
16.500000 8.870431 l
|
||||
17.570002 7.800431 l
|
||||
18.570002 8.800431 l
|
||||
19.280001 10.020432 l
|
||||
19.280001 9.340431 l
|
||||
19.280003 7.353039 18.491199 5.446898 17.086838 4.040663 c
|
||||
15.682477 2.634427 13.777390 1.843081 11.790000 1.840431 c
|
||||
10.804841 1.837452 9.828926 2.030422 8.919037 2.408112 c
|
||||
8.009148 2.785801 7.183455 3.340670 6.490000 4.040432 c
|
||||
5.430000 2.980431 l
|
||||
6.688641 1.723354 8.291579 0.867460 10.036366 0.520847 c
|
||||
11.781152 0.174234 13.589522 0.352451 15.233082 1.032988 c
|
||||
16.876642 1.713524 18.281670 2.865855 19.270708 4.344442 c
|
||||
20.259747 5.823030 20.788437 7.561551 20.790001 9.340431 c
|
||||
20.790001 10.040431 l
|
||||
21.500000 8.820431 l
|
||||
22.500000 7.820431 l
|
||||
23.570002 8.890431 l
|
||||
20.040001 12.400433 l
|
||||
h
|
||||
f
|
||||
n
|
||||
Q
|
||||
|
||||
endstream
|
||||
endobj
|
||||
|
||||
3 0 obj
|
||||
1599
|
||||
endobj
|
||||
|
||||
4 0 obj
|
||||
<< /Annots []
|
||||
/Type /Page
|
||||
/MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
|
||||
/Resources 1 0 R
|
||||
/Contents 2 0 R
|
||||
/Parent 5 0 R
|
||||
>>
|
||||
endobj
|
||||
|
||||
5 0 obj
|
||||
<< /Kids [ 4 0 R ]
|
||||
/Count 1
|
||||
/Type /Pages
|
||||
>>
|
||||
endobj
|
||||
|
||||
6 0 obj
|
||||
<< /Pages 5 0 R
|
||||
/Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
|
||||
xref
|
||||
0 7
|
||||
0000000000 65535 f
|
||||
0000000010 00000 n
|
||||
0000000034 00000 n
|
||||
0000001689 00000 n
|
||||
0000001712 00000 n
|
||||
0000001885 00000 n
|
||||
0000001959 00000 n
|
||||
trailer
|
||||
<< /ID [ (some) (id) ]
|
||||
/Root 6 0 R
|
||||
/Size 7
|
||||
>>
|
||||
startxref
|
||||
2018
|
||||
%%EOF
|
||||
@ -1,23 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "navbar_disclosure_down_small@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "navbar_disclosure_down_small@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "navbar_disclosure_down_small@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
@ -1,34 +1,42 @@
|
||||
//
|
||||
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
|
||||
// Copyright (c) 2022 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Photos
|
||||
import UIKit
|
||||
|
||||
protocol ImagePickerGridControllerDelegate: AnyObject {
|
||||
func imagePickerDidCompleteSelection(_ imagePicker: ImagePickerGridController)
|
||||
func imagePickerDidRequestSendMedia(_ imagePicker: ImagePickerGridController)
|
||||
func imagePickerDidCancel(_ imagePicker: ImagePickerGridController)
|
||||
|
||||
func imagePicker(_ imagePicker: ImagePickerGridController, isAssetSelected asset: PHAsset) -> Bool
|
||||
func imagePicker(_ imagePicker: ImagePickerGridController, didSelectAsset asset: PHAsset, attachmentPromise: Promise<SignalAttachment>)
|
||||
func imagePicker(_ imagePicker: ImagePickerGridController, didDeselectAsset asset: PHAsset)
|
||||
|
||||
var isInBatchSelectMode: Bool { get }
|
||||
func imagePickerCanSelectMoreItems(_ imagePicker: ImagePickerGridController) -> Bool
|
||||
func imagePickerDidTryToSelectTooMany(_ imagePicker: ImagePickerGridController)
|
||||
}
|
||||
|
||||
protocol ImagePickerGridControllerDataSource: AnyObject {
|
||||
func imagePicker(_ imagePicker: ImagePickerGridController, isAssetSelected asset: PHAsset) -> Bool
|
||||
func imagePickerCanSelectMoreItems(_ imagePicker: ImagePickerGridController) -> Bool
|
||||
var numberOfMediaItems: Int { get }
|
||||
}
|
||||
|
||||
class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegate, PhotoCollectionPickerDelegate {
|
||||
|
||||
weak var delegate: ImagePickerGridControllerDelegate?
|
||||
weak var dataSource: ImagePickerGridControllerDataSource?
|
||||
|
||||
private let library: PhotoLibrary = PhotoLibrary()
|
||||
private var photoCollection: PhotoCollection
|
||||
private var photoCollectionContents: PhotoCollectionContents
|
||||
private let photoMediaSize = PhotoMediaSize()
|
||||
|
||||
var collectionViewFlowLayout: UICollectionViewFlowLayout
|
||||
var titleView: TitleView!
|
||||
private var collectionViewFlowLayout: UICollectionViewFlowLayout
|
||||
private var titleView: TitleView!
|
||||
|
||||
private var bottomBar: UIView!
|
||||
private var doneButton: MediaDoneButton!
|
||||
|
||||
init() {
|
||||
collectionViewFlowLayout = type(of: self).buildLayout()
|
||||
@ -52,48 +60,49 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
|
||||
return true
|
||||
}
|
||||
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||
return .lightContent
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
view.backgroundColor = Theme.darkThemeBackgroundColor
|
||||
|
||||
library.add(delegate: self)
|
||||
|
||||
guard let collectionView = collectionView else {
|
||||
owsFailDebug("collectionView was unexpectedly nil")
|
||||
return
|
||||
}
|
||||
collectionView.allowsMultipleSelection = true
|
||||
collectionView.backgroundColor = Theme.darkThemeBackgroundColor
|
||||
collectionView.register(PhotoGridViewCell.self, forCellWithReuseIdentifier: PhotoGridViewCell.reuseIdentifier)
|
||||
|
||||
// ensure images at the end of the list can be scrolled above the bottom buttons
|
||||
let bottomButtonInset = -1 * SendMediaNavigationController.bottomButtonsCenterOffset + SendMediaNavigationController.bottomButtonWidth / 2
|
||||
collectionView.contentInset.bottom = bottomButtonInset + 8
|
||||
view.backgroundColor = .ows_gray95
|
||||
|
||||
// The PhotoCaptureVC needs a shadow behind it's cancel button, so we use a custom icon.
|
||||
// This VC has a visible navbar so doesn't need the shadow, but because the user can
|
||||
// quickly toggle between the Capture and the Picker VC's, we use the same custom "X"
|
||||
// icon here rather than the system "stop" icon so that the spacing matches exactly.
|
||||
// Otherwise there's a noticable shift in the icon placement.
|
||||
if UIDevice.current.isIPad {
|
||||
let cancelButton = OWSButton.shadowedCancelButton { [weak self] in
|
||||
self?.didPressCancel()
|
||||
}
|
||||
navigationItem.leftBarButtonItem = UIBarButtonItem(customView: cancelButton)
|
||||
} else {
|
||||
let cancelImage = UIImage(imageLiteralResourceName: "ic_x_with_shadow")
|
||||
let cancelButton = UIBarButtonItem(image: cancelImage, style: .plain, target: self, action: #selector(didPressCancel))
|
||||
|
||||
cancelButton.tintColor = .ows_gray05
|
||||
navigationItem.leftBarButtonItem = cancelButton
|
||||
}
|
||||
let cancelButton = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(didPressCancel))
|
||||
cancelButton.tintColor = .ows_gray05
|
||||
navigationItem.leftBarButtonItem = cancelButton
|
||||
|
||||
let titleView = TitleView()
|
||||
titleView.delegate = self
|
||||
titleView.tintColor = .ows_gray05
|
||||
titleView.text = photoCollection.localizedTitle()
|
||||
|
||||
navigationItem.titleView = titleView
|
||||
self.titleView = titleView
|
||||
|
||||
collectionView.backgroundColor = .ows_gray95
|
||||
bottomBar = UIView()
|
||||
view.addSubview(bottomBar)
|
||||
bottomBar.preservesSuperviewLayoutMargins = true
|
||||
bottomBar.autoPinBottomToSuperviewMargin(withInset: UIDevice.current.hasIPhoneXNotch ? 0 : 8)
|
||||
bottomBar.autoPinHorizontalEdges(toEdgesOf: view)
|
||||
|
||||
doneButton = MediaDoneButton()
|
||||
doneButton.userInterfaceStyleOverride = .light
|
||||
doneButton.addTarget(self, action: #selector(didTapDoneButton), for: .touchUpInside)
|
||||
bottomBar.addSubview(doneButton)
|
||||
doneButton.autoPinTopToSuperviewMargin()
|
||||
doneButton.autoPinBottomToSuperviewMargin()
|
||||
doneButton.autoPinTrailingToSuperviewMargin()
|
||||
|
||||
let selectionPanGesture = DirectionalPanGestureRecognizer(direction: [.horizontal], target: self, action: #selector(didPanSelection))
|
||||
selectionPanGesture.delegate = self
|
||||
@ -101,31 +110,28 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
|
||||
collectionView.addGestureRecognizer(selectionPanGesture)
|
||||
}
|
||||
|
||||
var selectionPanGesture: UIPanGestureRecognizer?
|
||||
enum BatchSelectionGestureMode {
|
||||
private var selectionPanGesture: UIPanGestureRecognizer?
|
||||
private enum BatchSelectionGestureMode {
|
||||
case select, deselect
|
||||
}
|
||||
var selectionPanGestureMode: BatchSelectionGestureMode = .select
|
||||
private var selectionPanGestureMode: BatchSelectionGestureMode = .select
|
||||
|
||||
@objc
|
||||
func didPanSelection(_ selectionPanGesture: UIPanGestureRecognizer) {
|
||||
private func didPanSelection(_ selectionPanGesture: UIPanGestureRecognizer) {
|
||||
guard let collectionView = collectionView else {
|
||||
owsFailDebug("collectionView was unexpectedly nil")
|
||||
return
|
||||
}
|
||||
|
||||
guard let delegate = delegate else {
|
||||
owsFailDebug("delegate was unexpectedly nil")
|
||||
return
|
||||
}
|
||||
|
||||
guard delegate.isInBatchSelectMode else {
|
||||
guard let dataSource = dataSource else {
|
||||
owsFailDebug("dataSource was unexpectedly nil")
|
||||
return
|
||||
}
|
||||
|
||||
switch selectionPanGesture.state {
|
||||
case .possible:
|
||||
break
|
||||
|
||||
case .began:
|
||||
collectionView.isUserInteractionEnabled = false
|
||||
collectionView.isScrollEnabled = false
|
||||
@ -135,11 +141,12 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
|
||||
return
|
||||
}
|
||||
let asset = photoCollectionContents.asset(at: indexPath.item)
|
||||
if delegate.imagePicker(self, isAssetSelected: asset) {
|
||||
if dataSource.imagePicker(self, isAssetSelected: asset) {
|
||||
selectionPanGestureMode = .deselect
|
||||
} else {
|
||||
selectionPanGestureMode = .select
|
||||
}
|
||||
|
||||
case .changed:
|
||||
let velocity = selectionPanGesture.velocity(in: view)
|
||||
|
||||
@ -160,27 +167,24 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
|
||||
return
|
||||
}
|
||||
tryToToggleBatchSelect(at: indexPath)
|
||||
|
||||
case .cancelled, .ended, .failed:
|
||||
collectionView.isUserInteractionEnabled = true
|
||||
collectionView.isScrollEnabled = true
|
||||
|
||||
@unknown default:
|
||||
owsFailDebug("unexpected selectionPanGesture.state: \(selectionPanGesture.state)")
|
||||
}
|
||||
}
|
||||
|
||||
func tryToToggleBatchSelect(at indexPath: IndexPath) {
|
||||
private func tryToToggleBatchSelect(at indexPath: IndexPath) {
|
||||
guard let collectionView = collectionView else {
|
||||
owsFailDebug("collectionView was unexpectedly nil")
|
||||
return
|
||||
}
|
||||
|
||||
guard let delegate = delegate else {
|
||||
owsFailDebug("delegate was unexpectedly nil")
|
||||
return
|
||||
}
|
||||
|
||||
guard delegate.isInBatchSelectMode else {
|
||||
owsFailDebug("isInBatchSelectMode was unexpectedly false")
|
||||
guard let delegate = delegate, let dataSource = dataSource else {
|
||||
owsFailDebug("delegate or dataSource was unexpectedly nil")
|
||||
return
|
||||
}
|
||||
|
||||
@ -191,7 +195,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
|
||||
return
|
||||
}
|
||||
|
||||
guard delegate.imagePickerCanSelectMoreItems(self) else {
|
||||
guard dataSource.imagePickerCanSelectMoreItems(self) else {
|
||||
delegate.imagePickerDidTryToSelectTooMany(self)
|
||||
return
|
||||
}
|
||||
@ -214,7 +218,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
|
||||
updateLayout()
|
||||
}
|
||||
|
||||
var hasEverAppeared: Bool = false
|
||||
private var hasEverAppeared: Bool = false
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
@ -227,6 +231,8 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
|
||||
if !hasEverAppeared {
|
||||
scrollToBottom(animated: false)
|
||||
}
|
||||
|
||||
updateDoneButtonAppearance()
|
||||
}
|
||||
|
||||
override func viewSafeAreaInsetsDidChange() {
|
||||
@ -257,14 +263,21 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
|
||||
}
|
||||
}
|
||||
|
||||
// MARK:
|
||||
private func updateDoneButtonAppearance() {
|
||||
guard let dataSource = dataSource else { return }
|
||||
|
||||
var lastPageYOffset: CGFloat {
|
||||
doneButton.badgeNumber = dataSource.numberOfMediaItems
|
||||
doneButton.isHidden = doneButton.badgeNumber == 0
|
||||
}
|
||||
|
||||
// MARK: - Scrolling
|
||||
|
||||
private var lastPageYOffset: CGFloat {
|
||||
let yOffset = collectionView.contentSize.height - collectionView.frame.height + collectionView.contentInset.bottom + view.safeAreaInsets.bottom
|
||||
return yOffset
|
||||
}
|
||||
|
||||
func scrollToBottom(animated: Bool) {
|
||||
private func scrollToBottom(animated: Bool) {
|
||||
self.view.layoutIfNeeded()
|
||||
|
||||
guard let collectionView = collectionView else {
|
||||
@ -296,7 +309,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
|
||||
}
|
||||
}
|
||||
|
||||
public func reloadData() {
|
||||
private func reloadData() {
|
||||
guard let collectionView = collectionView else {
|
||||
owsFailDebug("Missing collectionView.")
|
||||
return
|
||||
@ -309,13 +322,18 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
|
||||
// MARK: - Actions
|
||||
|
||||
@objc
|
||||
func didPressCancel() {
|
||||
private func didPressCancel() {
|
||||
self.delegate?.imagePickerDidCancel(self)
|
||||
}
|
||||
|
||||
@objc
|
||||
private func didTapDoneButton() {
|
||||
delegate?.imagePickerDidRequestSendMedia(self)
|
||||
}
|
||||
|
||||
// MARK: - Layout
|
||||
|
||||
static let kInterItemSpacing: CGFloat = 2
|
||||
private static let kInterItemSpacing: CGFloat = 2
|
||||
private class func buildLayout() -> UICollectionViewFlowLayout {
|
||||
let layout = UICollectionViewFlowLayout()
|
||||
|
||||
@ -327,7 +345,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
|
||||
return layout
|
||||
}
|
||||
|
||||
func updateLayout() {
|
||||
private func updateLayout() {
|
||||
let containerWidth = self.view.safeAreaLayoutGuide.layoutFrame.size.width
|
||||
|
||||
let minItemWidth: CGFloat = 100
|
||||
@ -351,7 +369,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
|
||||
|
||||
// MARK: - Batch Selection
|
||||
|
||||
func isSelected(indexPath: IndexPath) -> Bool {
|
||||
private func isSelected(indexPath: IndexPath) -> Bool {
|
||||
guard let selectedIndexPaths = collectionView.indexPathsForSelectedItems else {
|
||||
return false
|
||||
}
|
||||
@ -359,24 +377,6 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
|
||||
return selectedIndexPaths.contains(indexPath)
|
||||
}
|
||||
|
||||
func batchSelectModeDidChange() {
|
||||
applyBatchSelectMode()
|
||||
}
|
||||
|
||||
func applyBatchSelectMode() {
|
||||
guard let delegate = delegate else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let collectionView = collectionView else {
|
||||
owsFailDebug("collectionView was unexpectedly nil")
|
||||
return
|
||||
}
|
||||
|
||||
collectionView.allowsMultipleSelection = delegate.isInBatchSelectMode
|
||||
updateVisibleCells()
|
||||
}
|
||||
|
||||
// MARK: - PhotoLibraryDelegate
|
||||
|
||||
func photoLibraryDidChange(_ photoLibrary: PhotoLibrary) {
|
||||
@ -386,16 +386,14 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
|
||||
|
||||
// MARK: - PhotoCollectionPicker Presentation
|
||||
|
||||
var isShowingCollectionPickerController: Bool = false
|
||||
private var isShowingCollectionPickerController: Bool = false
|
||||
|
||||
lazy var collectionPickerController: PhotoCollectionPickerController = {
|
||||
private lazy var collectionPickerController: PhotoCollectionPickerController = {
|
||||
return PhotoCollectionPickerController(library: library,
|
||||
collectionDelegate: self)
|
||||
}()
|
||||
|
||||
func showCollectionPicker() {
|
||||
Logger.debug("")
|
||||
|
||||
private func showCollectionPicker() {
|
||||
guard let collectionPickerView = collectionPickerController.view else {
|
||||
owsFailDebug("collectionView was unexpectedly nil")
|
||||
return
|
||||
@ -419,9 +417,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
|
||||
}
|
||||
}
|
||||
|
||||
func hideCollectionPicker() {
|
||||
Logger.debug("")
|
||||
|
||||
private func hideCollectionPicker() {
|
||||
assert(isShowingCollectionPickerController)
|
||||
isShowingCollectionPickerController = false
|
||||
|
||||
@ -462,14 +458,14 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
|
||||
// MARK: - UICollectionView
|
||||
|
||||
override func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
|
||||
guard let delegate = delegate else { return false }
|
||||
guard let delegate = delegate, let dataSource = dataSource else { return false }
|
||||
|
||||
if delegate.imagePickerCanSelectMoreItems(self) {
|
||||
if dataSource.imagePickerCanSelectMoreItems(self) {
|
||||
return true
|
||||
} else {
|
||||
delegate.imagePickerDidTryToSelectTooMany(self)
|
||||
return false
|
||||
}
|
||||
|
||||
delegate.imagePickerDidTryToSelectTooMany(self)
|
||||
return false
|
||||
}
|
||||
|
||||
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
@ -481,16 +477,10 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
|
||||
let asset: PHAsset = photoCollectionContents.asset(at: indexPath.item)
|
||||
let attachmentPromise: Promise<SignalAttachment> = photoCollectionContents.outgoingAttachment(for: asset)
|
||||
delegate.imagePicker(self, didSelectAsset: asset, attachmentPromise: attachmentPromise)
|
||||
|
||||
if !delegate.isInBatchSelectMode {
|
||||
// Don't show "selected" badge unless we're in batch mode
|
||||
collectionView.deselectItem(at: indexPath, animated: false)
|
||||
delegate.imagePickerDidCompleteSelection(self)
|
||||
}
|
||||
updateDoneButtonAppearance()
|
||||
}
|
||||
|
||||
public override func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
|
||||
Logger.debug("")
|
||||
override func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
|
||||
guard let delegate = delegate else {
|
||||
owsFailDebug("delegate was unexpectedly nil")
|
||||
return
|
||||
@ -498,6 +488,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
|
||||
|
||||
let asset = photoCollectionContents.asset(at: indexPath.item)
|
||||
delegate.imagePicker(self, didDeselectAsset: asset)
|
||||
updateDoneButtonAppearance()
|
||||
}
|
||||
|
||||
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
@ -516,13 +507,13 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
|
||||
}
|
||||
|
||||
override func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
|
||||
guard let delegate = delegate else { return }
|
||||
guard let dataSource = dataSource else { return }
|
||||
guard let photoGridViewCell = cell as? PhotoGridViewCell else {
|
||||
owsFailDebug("unexpected cell: \(cell)")
|
||||
return
|
||||
}
|
||||
let assetItem = photoCollectionContents.assetItem(at: indexPath.item, photoMediaSize: photoMediaSize)
|
||||
let isSelected = delegate.imagePicker(self, isAssetSelected: assetItem.asset)
|
||||
let isSelected = dataSource.imagePicker(self, isAssetSelected: assetItem.asset)
|
||||
if isSelected {
|
||||
collectionView.selectItem(at: indexPath, animated: false, scrollPosition: [])
|
||||
} else {
|
||||
@ -532,8 +523,9 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
|
||||
photoGridViewCell.allowsMultipleSelection = collectionView.allowsMultipleSelection
|
||||
}
|
||||
|
||||
func updateVisibleCells() {
|
||||
guard let delegate = delegate else { return }
|
||||
private func updateVisibleCells() {
|
||||
guard let dataSource = dataSource else { return }
|
||||
|
||||
for cell in collectionView.visibleCells {
|
||||
guard let photoGridViewCell = cell as? PhotoGridViewCell else {
|
||||
owsFailDebug("unexpected cell: \(cell)")
|
||||
@ -545,13 +537,14 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
|
||||
continue
|
||||
}
|
||||
|
||||
photoGridViewCell.isSelected = delegate.imagePicker(self, isAssetSelected: assetItem.asset)
|
||||
photoGridViewCell.isSelected = dataSource.imagePicker(self, isAssetSelected: assetItem.asset)
|
||||
photoGridViewCell.allowsMultipleSelection = collectionView.allowsMultipleSelection
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ImagePickerGridController: UIGestureRecognizerDelegate {
|
||||
|
||||
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
guard gestureRecognizer == selectionPanGesture else {
|
||||
return true
|
||||
@ -561,52 +554,75 @@ extension ImagePickerGridController: UIGestureRecognizerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
protocol TitleViewDelegate: AnyObject {
|
||||
private protocol TitleViewDelegate: AnyObject {
|
||||
func titleViewWasTapped(_ titleView: TitleView)
|
||||
}
|
||||
|
||||
class TitleView: UIView {
|
||||
|
||||
// MARK: - Private
|
||||
private class TitleView: UIView {
|
||||
|
||||
private let label = UILabel()
|
||||
private let iconView = UIImageView()
|
||||
private let stackView: UIStackView
|
||||
|
||||
// Returns same font as UIBarButtonItem uses.
|
||||
private func titleLabelFont() -> UIFont {
|
||||
let fontDescriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body)
|
||||
return UIFont(descriptor: fontDescriptor, size: fontDescriptor.pointSize.clamp(17, 21))
|
||||
}
|
||||
|
||||
// MARK: - Initializers
|
||||
|
||||
override init(frame: CGRect) {
|
||||
let stackView = UIStackView(arrangedSubviews: [label, iconView])
|
||||
stackView = UIStackView(arrangedSubviews: [label, iconView])
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
stackView.axis = .horizontal
|
||||
stackView.alignment = .center
|
||||
stackView.spacing = 5
|
||||
stackView.isUserInteractionEnabled = true
|
||||
|
||||
self.stackView = stackView
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
addSubview(stackView)
|
||||
stackView.autoPinEdgesToSuperviewEdges()
|
||||
|
||||
label.textColor = .ows_gray05
|
||||
label.font = UIFont.ows_dynamicTypeBody.ows_semibold
|
||||
label.textColor = tintColor
|
||||
label.font = titleLabelFont()
|
||||
|
||||
iconView.tintColor = .ows_gray05
|
||||
iconView.image = UIImage(named: "navbar_disclosure_down")?.withRenderingMode(.alwaysTemplate)
|
||||
iconView.tintColor = tintColor
|
||||
if #available(iOS 13, *) {
|
||||
iconView.image = UIImage(systemName: "chevron.down", withConfiguration: UIImage.SymbolConfiguration(font: label.font))
|
||||
} else {
|
||||
iconView.image = UIImage(named: "media-composer-navbar-chevron")
|
||||
}
|
||||
|
||||
addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(titleTapped)))
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
@available(*, unavailable, message: "Use init(frame:) instead")
|
||||
required init?(coder: NSCoder) {
|
||||
notImplemented()
|
||||
}
|
||||
|
||||
override func tintColorDidChange() {
|
||||
super.tintColorDidChange()
|
||||
label.textColor = tintColor
|
||||
iconView.tintColor = tintColor
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
if traitCollection.preferredContentSizeCategory != previousTraitCollection?.preferredContentSizeCategory {
|
||||
label.font = titleLabelFont()
|
||||
if #available(iOS 13, *) {
|
||||
iconView.image = UIImage(systemName: "chevron.down", withConfiguration: UIImage.SymbolConfiguration(font: label.font))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
weak var delegate: TitleViewDelegate?
|
||||
|
||||
public var text: String? {
|
||||
var text: String? {
|
||||
get {
|
||||
return label.text
|
||||
}
|
||||
@ -615,11 +631,11 @@ class TitleView: UIView {
|
||||
}
|
||||
}
|
||||
|
||||
public enum TitleViewRotationDirection {
|
||||
enum TitleViewRotationDirection {
|
||||
case up, down
|
||||
}
|
||||
|
||||
public func rotateIcon(_ direction: TitleViewRotationDirection) {
|
||||
func rotateIcon(_ direction: TitleViewRotationDirection) {
|
||||
switch direction {
|
||||
case .up:
|
||||
// *slightly* more than `pi` to ensure the chevron animates counter-clockwise
|
||||
@ -639,7 +655,7 @@ class TitleView: UIView {
|
||||
}
|
||||
|
||||
extension ImagePickerGridController: TitleViewDelegate {
|
||||
func titleViewWasTapped(_ titleView: TitleView) {
|
||||
fileprivate func titleViewWasTapped(_ titleView: TitleView) {
|
||||
if isShowingCollectionPickerController {
|
||||
hideCollectionPicker()
|
||||
} else {
|
||||
|
||||
801
Signal/src/ViewControllers/Photos/MediaControls.swift
Normal file
@ -0,0 +1,801 @@
|
||||
//
|
||||
// Copyright (c) 2022 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SignalUI
|
||||
|
||||
protocol CameraCaptureControlDelegate: AnyObject {
|
||||
// MARK: Photo
|
||||
func cameraCaptureControlDidRequestCapturePhoto(_ control: CameraCaptureControl)
|
||||
|
||||
// MARK: Video
|
||||
func cameraCaptureControlDidRequestStartVideoRecording(_ control: CameraCaptureControl)
|
||||
func cameraCaptureControlDidRequestFinishVideoRecording(_ control: CameraCaptureControl)
|
||||
func cameraCaptureControlDidRequestCancelVideoRecording(_ control: CameraCaptureControl)
|
||||
|
||||
// MARK: Zoom
|
||||
var zoomScaleReferenceDistance: CGFloat? { get }
|
||||
func cameraCaptureControl(_ control: CameraCaptureControl, didUpdateZoomLevel zoomLevel: CGFloat)
|
||||
}
|
||||
|
||||
class CameraCaptureControl: UIView {
|
||||
|
||||
var axis: NSLayoutConstraint.Axis = .horizontal {
|
||||
didSet {
|
||||
if oldValue != axis {
|
||||
reactivateConstraintsForCurrentAxis()
|
||||
invalidateIntrinsicContentSize()
|
||||
}
|
||||
}
|
||||
}
|
||||
private var horizontalAxisConstraints = [NSLayoutConstraint]()
|
||||
private var verticalAxisConstraints = [NSLayoutConstraint]()
|
||||
|
||||
let shutterButtonLayoutGuide = UILayoutGuide() // allows view controller to align to shutter button.
|
||||
private let shutterButtonOuterCircle = CircleBlurView(effect: UIBlurEffect(style: .light))
|
||||
private let shutterButtonInnerCircle = CircleView()
|
||||
|
||||
fileprivate static let recordingLockControlSize: CGFloat = 36 // Stop button, swipe tracking circle, lock icon
|
||||
private static let shutterButtonDefaultSize: CGFloat = 72
|
||||
private static let shutterButtonRecordingSize: CGFloat = 122
|
||||
|
||||
private let outerCircleSizeConstraint: NSLayoutConstraint
|
||||
private let innerCircleSizeConstraint: NSLayoutConstraint
|
||||
private var slidingCircleHPositionConstraint: NSLayoutConstraint?
|
||||
private var slidingCircleVPositionConstraint: NSLayoutConstraint?
|
||||
|
||||
private lazy var slidingCircleView: CircleView = {
|
||||
let view = CircleView(diameter: CameraCaptureControl.recordingLockControlSize)
|
||||
view.backgroundColor = .ows_white
|
||||
view.isHidden = true
|
||||
return view
|
||||
}()
|
||||
private lazy var lockIconView = LockView(frame: CGRect(origin: .zero, size: .square(CameraCaptureControl.recordingLockControlSize)))
|
||||
private lazy var stopButton: UIButton = {
|
||||
let button = OWSButton { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.didTapStopButton()
|
||||
}
|
||||
button.backgroundColor = .white
|
||||
button.dimsWhenHighlighted = true
|
||||
button.layer.masksToBounds = true
|
||||
button.layer.cornerRadius = 4
|
||||
button.isHidden = true
|
||||
return button
|
||||
}()
|
||||
|
||||
weak var delegate: CameraCaptureControlDelegate?
|
||||
|
||||
required init(axis: NSLayoutConstraint.Axis) {
|
||||
innerCircleSizeConstraint = shutterButtonInnerCircle.autoSetDimension(.width, toSize: CameraCaptureControl.shutterButtonDefaultSize)
|
||||
outerCircleSizeConstraint = shutterButtonOuterCircle.autoSetDimension(.width, toSize: CameraCaptureControl.shutterButtonDefaultSize)
|
||||
|
||||
super.init(frame: CGRect(origin: .zero, size: CameraCaptureControl.intrinsicContentSize(forAxis: axis)))
|
||||
|
||||
self.axis = axis
|
||||
|
||||
// Round Shutter Button
|
||||
addLayoutGuide(shutterButtonLayoutGuide)
|
||||
shutterButtonLayoutGuide.widthAnchor.constraint(equalToConstant: CameraCaptureControl.shutterButtonDefaultSize).isActive = true
|
||||
shutterButtonLayoutGuide.heightAnchor.constraint(equalToConstant: CameraCaptureControl.shutterButtonDefaultSize).isActive = true
|
||||
horizontalAxisConstraints.append(contentsOf: [
|
||||
shutterButtonLayoutGuide.centerXAnchor.constraint(equalTo: leadingAnchor, constant: 0.5*CameraCaptureControl.shutterButtonDefaultSize),
|
||||
shutterButtonLayoutGuide.topAnchor.constraint(equalTo: topAnchor),
|
||||
shutterButtonLayoutGuide.bottomAnchor.constraint(equalTo: bottomAnchor)
|
||||
|
||||
])
|
||||
verticalAxisConstraints.append(contentsOf: [
|
||||
shutterButtonLayoutGuide.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
shutterButtonLayoutGuide.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
shutterButtonLayoutGuide.centerYAnchor.constraint(equalTo: topAnchor, constant: 0.5*CameraCaptureControl.shutterButtonDefaultSize)
|
||||
])
|
||||
|
||||
addSubview(shutterButtonOuterCircle)
|
||||
shutterButtonOuterCircle.centerXAnchor.constraint(equalTo: shutterButtonLayoutGuide.centerXAnchor).isActive = true
|
||||
shutterButtonOuterCircle.centerYAnchor.constraint(equalTo: shutterButtonLayoutGuide.centerYAnchor).isActive = true
|
||||
shutterButtonOuterCircle.autoPin(toAspectRatio: 1)
|
||||
|
||||
addSubview(shutterButtonInnerCircle)
|
||||
shutterButtonInnerCircle.autoPin(toAspectRatio: 1)
|
||||
shutterButtonInnerCircle.isUserInteractionEnabled = false
|
||||
shutterButtonInnerCircle.backgroundColor = .clear
|
||||
shutterButtonInnerCircle.layer.borderColor = UIColor.ows_white.cgColor
|
||||
shutterButtonInnerCircle.layer.borderWidth = 5
|
||||
shutterButtonInnerCircle.centerXAnchor.constraint(equalTo: shutterButtonOuterCircle.centerXAnchor).isActive = true
|
||||
shutterButtonInnerCircle.centerYAnchor.constraint(equalTo: shutterButtonOuterCircle.centerYAnchor).isActive = true
|
||||
|
||||
// The long press handles both the tap and the hold interaction, as well as the animation
|
||||
// the presents as the user begins to hold (and the button begins to grow prior to recording)
|
||||
let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress))
|
||||
longPressGesture.minimumPressDuration = 0
|
||||
shutterButtonOuterCircle.addGestureRecognizer(longPressGesture)
|
||||
|
||||
reactivateConstraintsForCurrentAxis()
|
||||
}
|
||||
|
||||
@available(*, unavailable, message: "Use init(axis:) instead")
|
||||
required init?(coder: NSCoder) {
|
||||
notImplemented()
|
||||
}
|
||||
|
||||
// MARK: - UI State
|
||||
|
||||
enum State {
|
||||
case initial
|
||||
case recording
|
||||
case recordingLocked
|
||||
}
|
||||
|
||||
private var _internalState: State = .initial
|
||||
var state: State {
|
||||
get {
|
||||
_internalState
|
||||
}
|
||||
set {
|
||||
setState(newValue)
|
||||
}
|
||||
}
|
||||
|
||||
private var sliderTrackingProgress: CGFloat = 0 {
|
||||
didSet {
|
||||
if sliderTrackingProgress == 1 && state == .recording {
|
||||
setState(.recordingLocked) // this will call updateUIForCurrentState()
|
||||
} else {
|
||||
updateUIForCurrentState()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setState(_ state: State, isRecordingWithLongPress: Bool = false, animationDuration: TimeInterval = 0) {
|
||||
guard _internalState != state else { return }
|
||||
|
||||
Logger.debug("New state: \(_internalState) -> \(state)")
|
||||
|
||||
_internalState = state
|
||||
self.isRecordingWithLongPress = isRecordingWithLongPress
|
||||
if animationDuration > 0 {
|
||||
UIView.animate(withDuration: animationDuration,
|
||||
delay: 0,
|
||||
options: [ .beginFromCurrentState ],
|
||||
animations: {
|
||||
self.updateUIForCurrentState()
|
||||
})
|
||||
} else {
|
||||
updateUIForCurrentState()
|
||||
}
|
||||
}
|
||||
|
||||
private func updateUIForCurrentState() {
|
||||
switch state {
|
||||
case .initial:
|
||||
// element visibility
|
||||
if slidingCircleHPositionConstraint != nil {
|
||||
stopButton.isHidden = true
|
||||
slidingCircleView.isHidden = true
|
||||
lockIconView.isHidden = true
|
||||
lockIconView.state = .unlocked
|
||||
}
|
||||
shutterButtonInnerCircle.alpha = 1
|
||||
shutterButtonInnerCircle.backgroundColor = .clear
|
||||
// element sizes
|
||||
outerCircleSizeConstraint.constant = CameraCaptureControl.shutterButtonDefaultSize
|
||||
innerCircleSizeConstraint.constant = CameraCaptureControl.shutterButtonDefaultSize
|
||||
|
||||
case .recording:
|
||||
prepareRecordingControlsIfNecessary()
|
||||
let sliderProgress = isRecordingWithLongPress ? sliderTrackingProgress : 0
|
||||
// element visibility
|
||||
stopButton.isHidden = sliderProgress == 0
|
||||
slidingCircleView.isHidden = sliderProgress == 0
|
||||
lockIconView.isHidden = !isRecordingWithLongPress
|
||||
lockIconView.setState(sliderProgress > 0.5 ? .locking : .unlocked, animated: true)
|
||||
shutterButtonInnerCircle.backgroundColor = .ows_white
|
||||
// element sizes
|
||||
outerCircleSizeConstraint.constant = CameraCaptureControl.shutterButtonRecordingSize
|
||||
// Inner (white) circle gets smaller as user drags the slider and reveals stop button when the slider is halfway to the lock icon.
|
||||
let circleSizeOffset = 2 * sliderProgress * (CameraCaptureControl.shutterButtonDefaultSize - CameraCaptureControl.recordingLockControlSize)
|
||||
innerCircleSizeConstraint.constant = CameraCaptureControl.shutterButtonDefaultSize - circleSizeOffset
|
||||
|
||||
case .recordingLocked:
|
||||
prepareRecordingControlsIfNecessary()
|
||||
// element visibility
|
||||
stopButton.isHidden = false
|
||||
slidingCircleView.isHidden = false
|
||||
lockIconView.isHidden = false
|
||||
lockIconView.setState(.locked, animated: true)
|
||||
shutterButtonInnerCircle.alpha = 0
|
||||
shutterButtonInnerCircle.backgroundColor = .ows_white
|
||||
// element sizes
|
||||
outerCircleSizeConstraint.constant = CameraCaptureControl.shutterButtonRecordingSize
|
||||
innerCircleSizeConstraint.constant = CameraCaptureControl.recordingLockControlSize
|
||||
}
|
||||
|
||||
setNeedsLayout()
|
||||
layoutIfNeeded()
|
||||
}
|
||||
|
||||
private func prepareRecordingControlsIfNecessary() {
|
||||
guard slidingCircleHPositionConstraint == nil else { return }
|
||||
|
||||
// 1. Stop button.
|
||||
addSubview(stopButton)
|
||||
stopButton.autoPin(toAspectRatio: 1)
|
||||
stopButton.autoSetDimension(.width, toSize: CameraCaptureControl.recordingLockControlSize)
|
||||
stopButton.centerXAnchor.constraint(equalTo: shutterButtonLayoutGuide.centerXAnchor).isActive = true
|
||||
stopButton.centerYAnchor.constraint(equalTo: shutterButtonLayoutGuide.centerYAnchor).isActive = true
|
||||
|
||||
// 2. Slider.
|
||||
insertSubview(slidingCircleView, belowSubview: shutterButtonInnerCircle)
|
||||
slidingCircleView.translatesAutoresizingMaskIntoConstraints = false
|
||||
let circleHPositionConstraint = slidingCircleView.centerXAnchor.constraint(equalTo: shutterButtonLayoutGuide.centerXAnchor)
|
||||
var horizontalConstraints: [NSLayoutConstraint] = [ circleHPositionConstraint ]
|
||||
horizontalConstraints.append(slidingCircleView.centerYAnchor.constraint(equalTo: shutterButtonLayoutGuide.centerYAnchor))
|
||||
slidingCircleHPositionConstraint = circleHPositionConstraint
|
||||
|
||||
let circleVPositionConstraint = slidingCircleView.centerYAnchor.constraint(equalTo: shutterButtonLayoutGuide.centerYAnchor)
|
||||
var verticalConstraints: [NSLayoutConstraint] = [ circleVPositionConstraint ]
|
||||
verticalConstraints.append(slidingCircleView.centerXAnchor.constraint(equalTo: shutterButtonLayoutGuide.centerXAnchor))
|
||||
slidingCircleVPositionConstraint = circleVPositionConstraint
|
||||
|
||||
// 3. Lock Icon
|
||||
addSubview(lockIconView)
|
||||
lockIconView.translatesAutoresizingMaskIntoConstraints = false
|
||||
// Centered vertically, pinned to trailing edge.
|
||||
horizontalConstraints.append(contentsOf: [ lockIconView.centerYAnchor.constraint(equalTo: shutterButtonLayoutGuide.centerYAnchor),
|
||||
lockIconView.trailingAnchor.constraint(equalTo: trailingAnchor) ])
|
||||
// Centered horizontally, pinned to bottom edge.
|
||||
verticalConstraints.append(contentsOf: [ lockIconView.centerXAnchor.constraint(equalTo: shutterButtonLayoutGuide.centerXAnchor),
|
||||
lockIconView.bottomAnchor.constraint(equalTo: bottomAnchor) ])
|
||||
|
||||
// 4. Activate current constraints.
|
||||
horizontalAxisConstraints.append(contentsOf: horizontalConstraints)
|
||||
if axis == .horizontal {
|
||||
addConstraints(horizontalConstraints)
|
||||
}
|
||||
|
||||
verticalAxisConstraints.append(contentsOf: verticalConstraints)
|
||||
if axis == .vertical {
|
||||
addConstraints(verticalConstraints)
|
||||
}
|
||||
|
||||
setNeedsLayout()
|
||||
UIView.performWithoutAnimation {
|
||||
self.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
private func reactivateConstraintsForCurrentAxis() {
|
||||
switch axis {
|
||||
case .horizontal:
|
||||
removeConstraints(verticalAxisConstraints)
|
||||
addConstraints(horizontalAxisConstraints)
|
||||
|
||||
case .vertical:
|
||||
removeConstraints(horizontalAxisConstraints)
|
||||
addConstraints(verticalAxisConstraints)
|
||||
|
||||
@unknown default:
|
||||
owsFailDebug("Unsupported `axis` value: \(axis.rawValue)")
|
||||
}
|
||||
}
|
||||
|
||||
override var intrinsicContentSize: CGSize {
|
||||
return Self.intrinsicContentSize(forAxis: axis)
|
||||
}
|
||||
|
||||
private static func intrinsicContentSize(forAxis axis: NSLayoutConstraint.Axis) -> CGSize {
|
||||
switch axis {
|
||||
case .horizontal:
|
||||
return CGSize(width: CameraCaptureControl.shutterButtonDefaultSize + 64 + CameraCaptureControl.recordingLockControlSize,
|
||||
height: CameraCaptureControl.shutterButtonDefaultSize)
|
||||
|
||||
case .vertical:
|
||||
return CGSize(width: CameraCaptureControl.shutterButtonDefaultSize,
|
||||
height: CameraCaptureControl.shutterButtonDefaultSize + 64 + CameraCaptureControl.recordingLockControlSize)
|
||||
|
||||
@unknown default:
|
||||
owsFailDebug("Unsupported `axis` value: \(axis.rawValue)")
|
||||
return CGSize(square: UIView.noIntrinsicMetric)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Gestures
|
||||
|
||||
private var isRecordingWithLongPress = false
|
||||
private static let longPressDurationThreshold = 0.5
|
||||
private static let minDistanceBeforeActivatingLockSlider: CGFloat = 30
|
||||
private var initialTouchLocation: CGPoint?
|
||||
private var initialZoomPosition: CGFloat?
|
||||
private var touchTimer: Timer?
|
||||
|
||||
@objc
|
||||
private func handleLongPress(gesture: UILongPressGestureRecognizer) {
|
||||
guard let gestureView = gesture.view else {
|
||||
owsFailDebug("gestureView was unexpectedly nil")
|
||||
return
|
||||
}
|
||||
|
||||
switch gesture.state {
|
||||
case .possible:
|
||||
break
|
||||
|
||||
case .began:
|
||||
guard state == .initial else { break }
|
||||
|
||||
initialTouchLocation = gesture.location(in: gesture.view)
|
||||
initialZoomPosition = nil
|
||||
|
||||
touchTimer?.invalidate()
|
||||
touchTimer = WeakTimer.scheduledTimer(
|
||||
timeInterval: CameraCaptureControl.longPressDurationThreshold,
|
||||
target: self,
|
||||
userInfo: nil,
|
||||
repeats: false
|
||||
) { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
|
||||
self.setState(.recording, isRecordingWithLongPress: true, animationDuration: 0.4)
|
||||
|
||||
self.delegate?.cameraCaptureControlDidRequestStartVideoRecording(self)
|
||||
}
|
||||
|
||||
case .changed:
|
||||
guard state == .recording else { break }
|
||||
|
||||
guard let referenceDistance = delegate?.zoomScaleReferenceDistance else {
|
||||
owsFailDebug("referenceHeight was unexpectedly nil")
|
||||
return
|
||||
}
|
||||
|
||||
guard referenceDistance > 0 else {
|
||||
owsFailDebug("referenceHeight was unexpectedly <= 0")
|
||||
return
|
||||
}
|
||||
|
||||
guard let initialTouchLocation = initialTouchLocation else {
|
||||
owsFailDebug("initialTouchLocation was unexpectedly nil")
|
||||
return
|
||||
}
|
||||
|
||||
let currentLocation = gesture.location(in: gestureView)
|
||||
|
||||
// Zoom - only use if slide to lock hasn't been activated.
|
||||
var zoomLevel: CGFloat = 0
|
||||
if sliderTrackingProgress == 0 {
|
||||
let minDistanceBeforeActivatingZoom: CGFloat = 30
|
||||
let currentDistance: CGFloat = {
|
||||
switch axis {
|
||||
case .horizontal:
|
||||
if initialZoomPosition == nil {
|
||||
initialZoomPosition = currentLocation.y
|
||||
}
|
||||
return initialZoomPosition! - currentLocation.y - minDistanceBeforeActivatingZoom
|
||||
|
||||
case .vertical:
|
||||
if initialZoomPosition == nil {
|
||||
initialZoomPosition = currentLocation.x
|
||||
}
|
||||
return initialZoomPosition! - currentLocation.x - minDistanceBeforeActivatingZoom
|
||||
|
||||
@unknown default:
|
||||
owsFailDebug("Unsupported `axis` value: \(axis.rawValue)")
|
||||
return 0
|
||||
}
|
||||
}()
|
||||
|
||||
let distanceForFullZoom = referenceDistance / 4
|
||||
let ratio = currentDistance / distanceForFullZoom
|
||||
zoomLevel = ratio.clamp(0, 1)
|
||||
delegate?.cameraCaptureControl(self, didUpdateZoomLevel: zoomLevel)
|
||||
} else {
|
||||
initialZoomPosition = nil
|
||||
}
|
||||
|
||||
// Video Recording Lock - only works if zoom level == 0
|
||||
if zoomLevel == 0 {
|
||||
switch axis {
|
||||
case .horizontal:
|
||||
let xOffset = currentLocation.x - initialTouchLocation.x
|
||||
updateHorizontalTracking(xOffset: xOffset)
|
||||
|
||||
case .vertical:
|
||||
let yOffset = currentLocation.y - initialTouchLocation.y
|
||||
updateVerticalTracking(yOffset: yOffset)
|
||||
|
||||
@unknown default:
|
||||
owsFailDebug("Unsupported `axis` value: \(axis.rawValue)")
|
||||
}
|
||||
}
|
||||
|
||||
case .ended:
|
||||
touchTimer?.invalidate()
|
||||
touchTimer = nil
|
||||
|
||||
guard state != .recordingLocked else { return }
|
||||
|
||||
if state == .recording {
|
||||
setState(.initial, animationDuration: 0.2)
|
||||
|
||||
delegate?.cameraCaptureControlDidRequestFinishVideoRecording(self)
|
||||
} else {
|
||||
delegate?.cameraCaptureControlDidRequestCapturePhoto(self)
|
||||
}
|
||||
|
||||
case .cancelled, .failed:
|
||||
if state == .recording {
|
||||
setState(.initial, animationDuration: 0.2)
|
||||
|
||||
delegate?.cameraCaptureControlDidRequestCancelVideoRecording(self)
|
||||
}
|
||||
|
||||
touchTimer?.invalidate()
|
||||
touchTimer = nil
|
||||
|
||||
@unknown default:
|
||||
owsFailDebug("unexpected gesture state: \(gesture.state.rawValue)")
|
||||
}
|
||||
}
|
||||
|
||||
private func updateHorizontalTracking(xOffset: CGFloat) {
|
||||
let effectiveDistance = xOffset - Self.minDistanceBeforeActivatingLockSlider
|
||||
let distanceToLock = abs(lockIconView.center.x - stopButton.center.x)
|
||||
if let positionConstraint = slidingCircleHPositionConstraint {
|
||||
let trackingPosition = effectiveDistance.clamp(0, distanceToLock)
|
||||
positionConstraint.constant = trackingPosition
|
||||
}
|
||||
sliderTrackingProgress = (effectiveDistance / distanceToLock).clamp(0, 1)
|
||||
|
||||
Logger.debug("xOffset: \(xOffset), effectiveDistance: \(effectiveDistance), distanceToLock: \(distanceToLock), progress: \(sliderTrackingProgress)")
|
||||
}
|
||||
|
||||
private func updateVerticalTracking(yOffset: CGFloat) {
|
||||
let effectiveDistance = yOffset - Self.minDistanceBeforeActivatingLockSlider
|
||||
let distanceToLock = abs(lockIconView.center.y - stopButton.center.y)
|
||||
if let positionConstraint = slidingCircleVPositionConstraint {
|
||||
let trackingPosition = effectiveDistance.clamp(0, distanceToLock)
|
||||
positionConstraint.constant = trackingPosition
|
||||
}
|
||||
sliderTrackingProgress = (effectiveDistance / distanceToLock).clamp(0, 1)
|
||||
|
||||
Logger.debug("yOffset: \(yOffset), effectiveDistance: \(effectiveDistance), distanceToLock: \(distanceToLock), progress: \(sliderTrackingProgress)")
|
||||
}
|
||||
|
||||
// MARK: - Button Actions
|
||||
|
||||
private func didTapStopButton() {
|
||||
delegate?.cameraCaptureControlDidRequestFinishVideoRecording(self)
|
||||
}
|
||||
}
|
||||
|
||||
private class LockView: UIView {
|
||||
|
||||
private let imageViewLock = UIImageView(image: UIImage(named: "media-composer-lock-outline-24"))
|
||||
private let blurBackgroundView = CircleBlurView(effect: UIBlurEffect(style: .dark))
|
||||
private let whiteBackgroundView = CircleView()
|
||||
private let whiteCircleView = CircleView()
|
||||
|
||||
enum State {
|
||||
case unlocked
|
||||
case locking
|
||||
case locked
|
||||
}
|
||||
private var _internalState: State = .unlocked
|
||||
var state: State {
|
||||
get {
|
||||
_internalState
|
||||
}
|
||||
set {
|
||||
guard _internalState != newValue else { return }
|
||||
setState(newValue)
|
||||
}
|
||||
}
|
||||
|
||||
func setState(_ state: State, animated: Bool = false) {
|
||||
_internalState = state
|
||||
if animated {
|
||||
UIView.animate(withDuration: 0.25,
|
||||
delay: 0,
|
||||
options: [ .beginFromCurrentState ]) {
|
||||
self.updateAppearance()
|
||||
}
|
||||
} else {
|
||||
updateAppearance()
|
||||
}
|
||||
}
|
||||
|
||||
private func updateAppearance() {
|
||||
switch state {
|
||||
case .unlocked:
|
||||
blurBackgroundView.alpha = 1
|
||||
whiteCircleView.alpha = 0
|
||||
whiteBackgroundView.alpha = 0
|
||||
imageViewLock.alpha = 1
|
||||
imageViewLock.tintColor = .ows_white
|
||||
|
||||
case .locking:
|
||||
blurBackgroundView.alpha = 1
|
||||
whiteCircleView.alpha = 1
|
||||
whiteBackgroundView.alpha = 0
|
||||
imageViewLock.alpha = 0
|
||||
|
||||
case .locked:
|
||||
blurBackgroundView.alpha = 0
|
||||
whiteCircleView.alpha = 0
|
||||
whiteBackgroundView.alpha = 1
|
||||
imageViewLock.alpha = 1
|
||||
imageViewLock.tintColor = .ows_black
|
||||
}
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
isUserInteractionEnabled = false
|
||||
|
||||
addSubview(blurBackgroundView)
|
||||
blurBackgroundView.autoPinEdgesToSuperviewEdges()
|
||||
|
||||
addSubview(whiteCircleView)
|
||||
whiteCircleView.backgroundColor = .clear
|
||||
whiteCircleView.layer.borderColor = UIColor.ows_white.cgColor
|
||||
whiteCircleView.layer.borderWidth = 3
|
||||
whiteCircleView.autoPinEdgesToSuperviewEdges()
|
||||
|
||||
addSubview(whiteBackgroundView)
|
||||
whiteBackgroundView.backgroundColor = .ows_white
|
||||
whiteBackgroundView.autoPinEdgesToSuperviewEdges()
|
||||
|
||||
addSubview(imageViewLock)
|
||||
imageViewLock.tintColor = .ows_white
|
||||
imageViewLock.autoCenterInSuperview()
|
||||
|
||||
updateAppearance()
|
||||
}
|
||||
|
||||
@available(*, unavailable, message: "Use init(frame:) instead")
|
||||
required init?(coder: NSCoder) {
|
||||
notImplemented()
|
||||
}
|
||||
|
||||
override var intrinsicContentSize: CGSize {
|
||||
CGSize(width: CameraCaptureControl.recordingLockControlSize, height: CameraCaptureControl.recordingLockControlSize)
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS, deprecated: 13.0, message: "Use `overrideUserInterfaceStyle` instead.")
|
||||
private protocol UserInterfaceStyleOverride {
|
||||
|
||||
var userInterfaceStyleOverride: UIUserInterfaceStyle { get set }
|
||||
|
||||
var effectiveUserInterfaceStyle: UIUserInterfaceStyle { get }
|
||||
|
||||
}
|
||||
|
||||
private extension UserInterfaceStyleOverride {
|
||||
|
||||
var effectiveUserInterfaceStyle: UIUserInterfaceStyle {
|
||||
if userInterfaceStyleOverride != .unspecified {
|
||||
return userInterfaceStyleOverride
|
||||
}
|
||||
if let uiView = self as? UIView {
|
||||
return uiView.traitCollection.userInterfaceStyle
|
||||
}
|
||||
return .unspecified
|
||||
}
|
||||
|
||||
static func blurEffectStyle(for userInterfaceStyle: UIUserInterfaceStyle) -> UIBlurEffect.Style {
|
||||
switch userInterfaceStyle {
|
||||
case .dark:
|
||||
return .dark
|
||||
case .light:
|
||||
return .extraLight
|
||||
default:
|
||||
fatalError("It is an error to pass UIUserInterfaceStyleUnspecified.")
|
||||
}
|
||||
}
|
||||
|
||||
static func tintColor(for userInterfaceStyle: UIUserInterfaceStyle) -> UIColor {
|
||||
switch userInterfaceStyle {
|
||||
case .dark:
|
||||
return Theme.darkThemePrimaryColor
|
||||
case .light:
|
||||
return .ows_gray60
|
||||
default:
|
||||
fatalError("It is an error to pass UIUserInterfaceStyleUnspecified.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CameraOverlayButton: UIButton, UserInterfaceStyleOverride {
|
||||
|
||||
fileprivate var userInterfaceStyleOverride: UIUserInterfaceStyle = .unspecified {
|
||||
didSet {
|
||||
if oldValue != userInterfaceStyleOverride {
|
||||
updateStyle()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let backgroundView: CircleBlurView = {
|
||||
let view = CircleBlurView(effect: UIBlurEffect(style: .regular))
|
||||
view.isUserInteractionEnabled = false
|
||||
return view
|
||||
}()
|
||||
|
||||
private static let visibleButtonSize: CGFloat = 36 // both height and width
|
||||
private static let defaultInset: CGFloat = 4
|
||||
|
||||
var contentInsets: UIEdgeInsets = UIEdgeInsets(margin: CameraOverlayButton.defaultInset) {
|
||||
didSet {
|
||||
layoutMargins = contentInsets
|
||||
}
|
||||
}
|
||||
|
||||
required init(image: UIImage?, userInterfaceStyleOverride: UIUserInterfaceStyle = .unspecified) {
|
||||
super.init(frame: CGRect(origin: .zero, size: .square(Self.visibleButtonSize + 2*Self.defaultInset)))
|
||||
|
||||
self.userInterfaceStyleOverride = userInterfaceStyleOverride
|
||||
|
||||
layoutMargins = contentInsets
|
||||
|
||||
addSubview(backgroundView)
|
||||
backgroundView.autoPinEdgesToSuperviewMargins()
|
||||
|
||||
setImage(image, for: .normal)
|
||||
updateStyle()
|
||||
}
|
||||
|
||||
@available(*, unavailable, message: "Use init(image:userInterfaceStyleOverride:) instead")
|
||||
override init(frame: CGRect) {
|
||||
notImplemented()
|
||||
}
|
||||
|
||||
@available(*, unavailable, message: "Use init(image:userInterfaceStyleOverride:) instead")
|
||||
required init?(coder: NSCoder) {
|
||||
notImplemented()
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
sendSubviewToBack(backgroundView)
|
||||
}
|
||||
|
||||
override var intrinsicContentSize: CGSize {
|
||||
return CGSize(width: Self.visibleButtonSize + layoutMargins.leading + layoutMargins.trailing,
|
||||
height: Self.visibleButtonSize + layoutMargins.top + layoutMargins.bottom)
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
if previousTraitCollection?.userInterfaceStyle != traitCollection.userInterfaceStyle, userInterfaceStyleOverride == .unspecified {
|
||||
updateStyle()
|
||||
}
|
||||
}
|
||||
|
||||
private func updateStyle() {
|
||||
backgroundView.effect = UIBlurEffect(style: CameraOverlayButton.blurEffectStyle(for: effectiveUserInterfaceStyle))
|
||||
tintColor = CameraOverlayButton.tintColor(for: effectiveUserInterfaceStyle)
|
||||
}
|
||||
}
|
||||
|
||||
class MediaDoneButton: UIButton, UserInterfaceStyleOverride {
|
||||
|
||||
var badgeNumber: Int = 0 {
|
||||
didSet {
|
||||
textLabel.text = numberFormatter.string(for: badgeNumber)
|
||||
invalidateIntrinsicContentSize()
|
||||
}
|
||||
}
|
||||
|
||||
var userInterfaceStyleOverride: UIUserInterfaceStyle = .unspecified {
|
||||
didSet {
|
||||
if oldValue != userInterfaceStyleOverride {
|
||||
updateStyle()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static var font: UIFont {
|
||||
return UIFont.ows_dynamicTypeSubheadline.ows_monospaced
|
||||
}
|
||||
|
||||
private let numberFormatter: NumberFormatter = {
|
||||
let numberFormatter = NumberFormatter()
|
||||
numberFormatter.numberStyle = .decimal
|
||||
return numberFormatter
|
||||
}()
|
||||
|
||||
private let textLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = .ows_white
|
||||
label.textAlignment = .center
|
||||
label.font = MediaDoneButton.font
|
||||
return label
|
||||
}()
|
||||
private let pillView: PillView = {
|
||||
let pillView = PillView(frame: .zero)
|
||||
pillView.isUserInteractionEnabled = false
|
||||
pillView.layoutMargins = UIEdgeInsets(hMargin: 8, vMargin: 7)
|
||||
return pillView
|
||||
}()
|
||||
private let blurBackgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .regular))
|
||||
private let chevronImageView: UIImageView = {
|
||||
let image: UIImage?
|
||||
if #available(iOS 13, *) {
|
||||
image = CurrentAppContext().isRTL ? UIImage(systemName: "chevron.backward") : UIImage(systemName: "chevron.right")
|
||||
} else {
|
||||
image = CurrentAppContext().isRTL ? UIImage(named: "chevron-left-20") : UIImage(named: "chevron-right-20")
|
||||
}
|
||||
let chevronImageView = UIImageView(image: image!.withRenderingMode(.alwaysTemplate))
|
||||
chevronImageView.contentMode = .center
|
||||
if #available(iOS 13, *) {
|
||||
chevronImageView.preferredSymbolConfiguration = UIImage.SymbolConfiguration(pointSize: MediaDoneButton.font.pointSize)
|
||||
}
|
||||
return chevronImageView
|
||||
}()
|
||||
private var dimmerView: UIView?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
addSubview(pillView)
|
||||
pillView.autoPinEdgesToSuperviewEdges()
|
||||
|
||||
pillView.addSubview(blurBackgroundView)
|
||||
blurBackgroundView.autoPinEdgesToSuperviewEdges()
|
||||
|
||||
let blueBadgeView = PillView(frame: bounds)
|
||||
blueBadgeView.backgroundColor = .ows_accentBlue
|
||||
blueBadgeView.layoutMargins = UIEdgeInsets(margin: 4)
|
||||
blueBadgeView.addSubview(textLabel)
|
||||
textLabel.autoPinEdgesToSuperviewMargins()
|
||||
|
||||
let hStack = UIStackView(arrangedSubviews: [blueBadgeView, chevronImageView])
|
||||
hStack.spacing = 6
|
||||
pillView.addSubview(hStack)
|
||||
hStack.autoPinEdgesToSuperviewMargins()
|
||||
|
||||
updateStyle()
|
||||
}
|
||||
|
||||
@available(*, unavailable, message: "Use init(frame:) instead")
|
||||
required init?(coder: NSCoder) {
|
||||
notImplemented()
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
if traitCollection.preferredContentSizeCategory != previousTraitCollection?.preferredContentSizeCategory {
|
||||
textLabel.font = .ows_dynamicTypeSubheadline.ows_monospaced
|
||||
if #available(iOS 13, *) {
|
||||
chevronImageView.preferredSymbolConfiguration = UIImage.SymbolConfiguration(pointSize: textLabel.font.pointSize)
|
||||
}
|
||||
}
|
||||
if traitCollection.userInterfaceStyle != previousTraitCollection?.userInterfaceStyle {
|
||||
updateStyle()
|
||||
}
|
||||
}
|
||||
|
||||
override var isHighlighted: Bool {
|
||||
didSet {
|
||||
if isHighlighted {
|
||||
if dimmerView == nil {
|
||||
let dimmerView = UIView(frame: bounds)
|
||||
dimmerView.isUserInteractionEnabled = false
|
||||
dimmerView.backgroundColor = .ows_black
|
||||
pillView.addSubview(dimmerView)
|
||||
dimmerView.autoPinEdgesToSuperviewEdges()
|
||||
self.dimmerView = dimmerView
|
||||
}
|
||||
dimmerView?.alpha = 0.5
|
||||
} else if let dimmerView = dimmerView {
|
||||
dimmerView.alpha = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func updateStyle() {
|
||||
blurBackgroundView.effect = UIBlurEffect(style: MediaDoneButton.blurEffectStyle(for: effectiveUserInterfaceStyle))
|
||||
chevronImageView.tintColor = MediaDoneButton.tintColor(for: effectiveUserInterfaceStyle)
|
||||
}
|
||||
}
|
||||
@ -1,35 +1,36 @@
|
||||
//
|
||||
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
|
||||
// Copyright (c) 2022 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreServices
|
||||
import Foundation
|
||||
|
||||
protocol PhotoCaptureDelegate: AnyObject {
|
||||
|
||||
// MARK: Still Photo
|
||||
|
||||
func photoCaptureDidStartPhotoCapture(_ photoCapture: PhotoCapture)
|
||||
func photoCapture(_ photoCapture: PhotoCapture, didFinishProcessingAttachment attachment: SignalAttachment)
|
||||
func photoCapture(_ photoCapture: PhotoCapture, processingDidError error: Error)
|
||||
func photoCaptureDidStart(_ photoCapture: PhotoCapture)
|
||||
func photoCapture(_ photoCapture: PhotoCapture, didFinishProcessing attachment: SignalAttachment)
|
||||
func photoCapture(_ photoCapture: PhotoCapture, didFailProcessing error: Error)
|
||||
|
||||
// MARK: Movie
|
||||
// MARK: Video
|
||||
|
||||
func photoCaptureDidBeginMovie(_ photoCapture: PhotoCapture)
|
||||
func photoCaptureDidCompleteMovie(_ photoCapture: PhotoCapture)
|
||||
func photoCaptureDidCancelMovie(_ photoCapture: PhotoCapture)
|
||||
func photoCaptureWillBeginRecording(_ photoCapture: PhotoCapture)
|
||||
func photoCaptureDidBeginRecording(_ photoCapture: PhotoCapture)
|
||||
func photoCaptureDidFinishRecording(_ photoCapture: PhotoCapture)
|
||||
func photoCaptureDidCancelRecording(_ photoCapture: PhotoCapture)
|
||||
|
||||
// MARK: Utility
|
||||
|
||||
func photoCapture(_ photoCapture: PhotoCapture, didChangeOrientation: AVCaptureVideoOrientation)
|
||||
func photoCaptureCanCaptureMoreItems(_ photoCapture: PhotoCapture) -> Bool
|
||||
func photoCaptureDidTryToCaptureTooMany(_ photoCapture: PhotoCapture)
|
||||
var zoomScaleReferenceHeight: CGFloat? { get }
|
||||
var zoomScaleReferenceDistance: CGFloat? { get }
|
||||
|
||||
func beginCaptureButtonAnimation(_ duration: TimeInterval)
|
||||
func endCaptureButtonAnimation(_ duration: TimeInterval)
|
||||
|
||||
func photoCapture(_ photoCapture: PhotoCapture, didCompleteFocusingAtPoint focusPoint: CGPoint)
|
||||
func photoCapture(_ photoCapture: PhotoCapture, didCompleteFocusing focusPoint: CGPoint)
|
||||
|
||||
}
|
||||
|
||||
@ -82,7 +83,7 @@ class PhotoCapture: NSObject {
|
||||
let focusPoint = currentCaptureInput.device.focusPointOfInterest
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.photoCapture(self, didCompleteFocusingAtPoint: focusPoint)
|
||||
self.delegate?.photoCapture(self, didCompleteFocusing: focusPoint)
|
||||
}
|
||||
}
|
||||
|
||||
@ -292,20 +293,20 @@ class PhotoCapture: NSObject {
|
||||
focusObservation.invalidate()
|
||||
}
|
||||
self.focusObservation = newInput.observe(\.device.isAdjustingFocus,
|
||||
options: [.old, .new]) { [weak self] _, change in
|
||||
guard let self = self else { return }
|
||||
options: [.old, .new]) { [weak self] _, change in
|
||||
guard let self = self else { return }
|
||||
|
||||
guard let oldValue = change.oldValue else {
|
||||
return
|
||||
}
|
||||
guard let oldValue = change.oldValue else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let newValue = change.newValue else {
|
||||
return
|
||||
}
|
||||
guard let newValue = change.newValue else {
|
||||
return
|
||||
}
|
||||
|
||||
if oldValue == true && newValue == false {
|
||||
self.didCompleteFocusing()
|
||||
}
|
||||
if oldValue == true && newValue == false {
|
||||
self.didCompleteFocusing()
|
||||
}
|
||||
}
|
||||
|
||||
currentCaptureInput = newInput
|
||||
@ -333,9 +334,9 @@ class PhotoCapture: NSObject {
|
||||
}
|
||||
|
||||
public func focus(with focusMode: AVCaptureDevice.FocusMode,
|
||||
exposureMode: AVCaptureDevice.ExposureMode,
|
||||
at devicePoint: CGPoint,
|
||||
monitorSubjectAreaChange: Bool) {
|
||||
exposureMode: AVCaptureDevice.ExposureMode,
|
||||
at devicePoint: CGPoint,
|
||||
monitorSubjectAreaChange: Bool) {
|
||||
sessionQueue.async {
|
||||
Logger.debug("focusMode: \(focusMode), exposureMode: \(exposureMode), devicePoint: \(devicePoint), monitorSubjectAreaChange:\(monitorSubjectAreaChange)")
|
||||
guard let device = self.captureDevice else {
|
||||
@ -460,7 +461,7 @@ class PhotoCapture: NSObject {
|
||||
}
|
||||
|
||||
let captureRect = captureOutputPhotoRect
|
||||
delegate.photoCaptureDidStartPhotoCapture(self)
|
||||
delegate.photoCaptureDidStart(self)
|
||||
sessionQueue.async {
|
||||
self.captureOutput.takePhoto(delegate: self, captureRect: captureRect)
|
||||
}
|
||||
@ -516,11 +517,13 @@ class PhotoCapture: NSObject {
|
||||
throw PhotoCaptureError.invalidVideo
|
||||
}
|
||||
|
||||
self.delegate?.photoCaptureDidBeginMovie(self)
|
||||
self.delegate?.photoCaptureDidBeginRecording(self)
|
||||
}
|
||||
}.catch { error in
|
||||
self.handleMovieCaptureError(error)
|
||||
}
|
||||
|
||||
delegate.photoCaptureWillBeginRecording(self)
|
||||
}
|
||||
|
||||
private func completeMovieCapture() {
|
||||
@ -555,7 +558,7 @@ class PhotoCapture: NSObject {
|
||||
|
||||
// Inform UI that capture is stopping.
|
||||
self.lastMovieRecordingEndDate = movieRecordingStartDate
|
||||
self.delegate?.photoCaptureDidCompleteMovie(self)
|
||||
self.delegate?.photoCaptureDidFinishRecording(self)
|
||||
}.catch { error in
|
||||
self.handleMovieCaptureError(error)
|
||||
}
|
||||
@ -573,7 +576,7 @@ class PhotoCapture: NSObject {
|
||||
self.shouldHaveAudioCapture = false
|
||||
}
|
||||
self.lastMovieRecordingEndDate = Date()
|
||||
self.delegate?.photoCapture(self, processingDidError: error)
|
||||
self.delegate?.photoCapture(self, didFailProcessing: error)
|
||||
}
|
||||
|
||||
private func cancelMovieCapture() {
|
||||
@ -592,7 +595,7 @@ class PhotoCapture: NSObject {
|
||||
|
||||
// Inform UI that capture is stopping.
|
||||
self.lastMovieRecordingEndDate = Date()
|
||||
self.delegate?.photoCaptureDidCancelMovie(self)
|
||||
self.delegate?.photoCaptureDidCancelRecording(self)
|
||||
}.catch { error in
|
||||
self.handleMovieCaptureError(error)
|
||||
}
|
||||
@ -668,33 +671,34 @@ extension PhotoCapture: VolumeButtonObserver {
|
||||
|
||||
// MARK: -
|
||||
|
||||
extension PhotoCapture: CaptureButtonDelegate {
|
||||
func didTapCaptureButton(_ captureButton: CaptureButton) {
|
||||
extension PhotoCapture: CameraCaptureControlDelegate {
|
||||
|
||||
func cameraCaptureControlDidRequestCapturePhoto(_ control: CameraCaptureControl) {
|
||||
takePhoto()
|
||||
}
|
||||
|
||||
func didBeginLongPressCaptureButton(_ captureButton: CaptureButton) {
|
||||
func cameraCaptureControlDidRequestStartVideoRecording(_ control: CameraCaptureControl) {
|
||||
beginMovieCapture()
|
||||
}
|
||||
|
||||
func didCompleteLongPressCaptureButton(_ captureButton: CaptureButton) {
|
||||
func cameraCaptureControlDidRequestFinishVideoRecording(_ control: CameraCaptureControl) {
|
||||
completeMovieCapture()
|
||||
}
|
||||
|
||||
func didCancelLongPressCaptureButton(_ captureButton: CaptureButton) {
|
||||
func cameraCaptureControlDidRequestCancelVideoRecording(_ control: CameraCaptureControl) {
|
||||
cancelMovieCapture()
|
||||
}
|
||||
|
||||
func didPressStopCaptureButton(_ captureButton: CaptureButton) {
|
||||
func didPressStopCaptureButton(_ control: CameraCaptureControl) {
|
||||
completeMovieCapture()
|
||||
}
|
||||
|
||||
var zoomScaleReferenceHeight: CGFloat? {
|
||||
return delegate?.zoomScaleReferenceHeight
|
||||
var zoomScaleReferenceDistance: CGFloat? {
|
||||
return delegate?.zoomScaleReferenceDistance
|
||||
}
|
||||
|
||||
func longPressCaptureButton(_ captureButton: CaptureButton, didUpdateZoomAlpha zoomAlpha: CGFloat) {
|
||||
updateZoom(alpha: zoomAlpha)
|
||||
func cameraCaptureControl(_ control: CameraCaptureControl, didUpdateZoomLevel zoomLevel: CGFloat) {
|
||||
updateZoom(alpha: zoomLevel)
|
||||
}
|
||||
}
|
||||
|
||||
@ -730,12 +734,12 @@ extension PhotoCapture: CaptureOutputDelegate {
|
||||
|
||||
switch photoData {
|
||||
case .failure(let error):
|
||||
delegate.photoCapture(self, processingDidError: error)
|
||||
delegate.photoCapture(self, didFailProcessing: error)
|
||||
case .success(let photoData):
|
||||
let dataSource = DataSourceValue.dataSource(with: photoData, utiType: kUTTypeJPEG as String)
|
||||
|
||||
let attachment = SignalAttachment.attachment(dataSource: dataSource, dataUTI: kUTTypeJPEG as String)
|
||||
delegate.photoCapture(self, didFinishProcessingAttachment: attachment)
|
||||
delegate.photoCapture(self, didFinishProcessing: attachment)
|
||||
}
|
||||
}
|
||||
|
||||
@ -761,7 +765,7 @@ extension PhotoCapture: CaptureOutputDelegate {
|
||||
let attachment = SignalAttachment.attachment(dataSource: dataSource, dataUTI: kUTTypeMPEG4 as String)
|
||||
|
||||
BenchEventComplete(eventId: "Movie Processing")
|
||||
delegate.photoCapture(self, didFinishProcessingAttachment: attachment)
|
||||
delegate.photoCapture(self, didFinishProcessing: attachment)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1165,7 +1169,7 @@ class PhotoCaptureOutputAdaptee: NSObject, ImageCaptureOutput {
|
||||
photoSettings.isHighResolutionPhotoEnabled = true
|
||||
|
||||
photoSettings.isAutoStillImageStabilizationEnabled =
|
||||
photoOutput.isStillImageStabilizationSupported
|
||||
photoOutput.isStillImageStabilizationSupported
|
||||
|
||||
return photoSettings
|
||||
}
|
||||
|
||||
@ -62,7 +62,11 @@ class PhotoCollectionPickerController: OWSTableViewController, PhotoLibraryDeleg
|
||||
self.contents = contents
|
||||
}
|
||||
|
||||
private let numberFormatter: NumberFormatter = NumberFormatter()
|
||||
private lazy var numberFormatter: NumberFormatter = {
|
||||
let formatter = NumberFormatter()
|
||||
formatter.numberStyle = .decimal
|
||||
return formatter
|
||||
}()
|
||||
|
||||
private func buildTableCell(collection: PhotoCollection) -> UITableViewCell {
|
||||
let cell = OWSTableItem.newCell()
|
||||
|
||||
@ -1,89 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2020 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class SendMediaBottomButton: UIView {
|
||||
let button: OWSButton
|
||||
let blurView: UIVisualEffectView
|
||||
|
||||
init(imageName: String, tintColor: UIColor, diameter: CGFloat, block: @escaping () -> Void) {
|
||||
self.button = OWSButton(imageName: imageName, tintColor: tintColor, block: block)
|
||||
button.imageEdgeInsets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
|
||||
|
||||
let blurEffect = UIBlurEffect(style: .dark)
|
||||
self.blurView = UIVisualEffectView(effect: blurEffect)
|
||||
|
||||
super.init(frame: .zero)
|
||||
|
||||
layer.cornerRadius = diameter / 2
|
||||
blurView.layer.cornerRadius = diameter / 2
|
||||
blurView.clipsToBounds = true
|
||||
|
||||
addSubview(blurView)
|
||||
blurView.autoPinEdgesToSuperviewEdges()
|
||||
|
||||
let vibrancyEffect = UIVibrancyEffect(blurEffect: blurEffect)
|
||||
let vibrancyView = UIVisualEffectView(effect: vibrancyEffect)
|
||||
blurView.contentView.addSubview(vibrancyView)
|
||||
vibrancyView.autoPinEdgesToSuperviewEdges()
|
||||
|
||||
addSubview(button)
|
||||
button.autoSetDimensions(to: CGSize(square: diameter))
|
||||
button.autoPinEdgesToSuperviewEdges()
|
||||
updateViewState()
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
var isSelected: Bool = false {
|
||||
didSet {
|
||||
updateViewState()
|
||||
}
|
||||
}
|
||||
|
||||
var isBeingPresentedOverPhotoCapture: Bool = false {
|
||||
didSet {
|
||||
updateViewState()
|
||||
}
|
||||
}
|
||||
|
||||
private enum Mode {
|
||||
case selected, unselectedOverMediaLibrary, unselectedOverPhotoCapture
|
||||
}
|
||||
|
||||
private var mode: Mode {
|
||||
if isSelected {
|
||||
return .selected
|
||||
}
|
||||
|
||||
if isBeingPresentedOverPhotoCapture {
|
||||
return .unselectedOverPhotoCapture
|
||||
}
|
||||
|
||||
return .unselectedOverMediaLibrary
|
||||
}
|
||||
|
||||
func updateViewState() {
|
||||
switch mode {
|
||||
case .selected:
|
||||
button.tintColor = .ows_black
|
||||
blurView.isHidden = true
|
||||
backgroundColor = .ows_white
|
||||
setShadow()
|
||||
case .unselectedOverMediaLibrary:
|
||||
button.tintColor = .ows_white
|
||||
blurView.isHidden = false
|
||||
backgroundColor = .clear
|
||||
layer.shadowRadius = 0
|
||||
case .unselectedOverPhotoCapture:
|
||||
button.tintColor = .ows_white
|
||||
blurView.isHidden = true
|
||||
backgroundColor = .clear
|
||||
setShadow()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,9 +1,10 @@
|
||||
//
|
||||
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
|
||||
// Copyright (c) 2022 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Photos
|
||||
import SignalUI
|
||||
|
||||
@objc
|
||||
protocol SendMediaNavDelegate: AnyObject {
|
||||
@ -38,23 +39,8 @@ class CameraFirstCaptureNavigationController: SendMediaNavigationController {
|
||||
}
|
||||
}
|
||||
|
||||
public let fixedBottomSafeAreaInset: CGFloat = 20
|
||||
public let fixedHorizontalMargin: CGFloat = 16
|
||||
|
||||
@objc
|
||||
class SendMediaNavigationController: OWSNavigationController {
|
||||
static var bottomButtonsCenterOffset: CGFloat {
|
||||
if UIDevice.current.hasIPhoneXNotch {
|
||||
// we pin to a constant rather than margin, because on notched devices the
|
||||
// safeAreaInsets/margins change as the device rotates *EVEN THOUGH* the interface
|
||||
// is locked to portrait.
|
||||
return -1 * (CaptureButton.recordingDiameter / 2 + 4) - fixedBottomSafeAreaInset
|
||||
} else {
|
||||
return -1 * (CaptureButton.recordingDiameter / 2 + 4)
|
||||
}
|
||||
}
|
||||
|
||||
static var trailingButtonsOffset: CGFloat = -28
|
||||
|
||||
var attachmentCount: Int {
|
||||
return attachmentDraftCollection.count
|
||||
@ -66,69 +52,26 @@ class SendMediaNavigationController: OWSNavigationController {
|
||||
guard !CurrentAppContext().hasActiveCall else {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||
if topViewController is ConversationPickerViewController {
|
||||
return .default
|
||||
}
|
||||
return .lightContent
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
self.delegate = self
|
||||
|
||||
let bottomButtonsCenterOffset = SendMediaNavigationController.bottomButtonsCenterOffset
|
||||
|
||||
view.addSubview(batchModeButton)
|
||||
batchModeButton.setCompressionResistanceHigh()
|
||||
|
||||
view.addSubview(doneButton)
|
||||
doneButton.setCompressionResistanceHigh()
|
||||
|
||||
view.addSubview(cameraModeButton)
|
||||
cameraModeButton.setCompressionResistanceHigh()
|
||||
|
||||
view.addSubview(mediaLibraryModeButton)
|
||||
mediaLibraryModeButton.setCompressionResistanceHigh()
|
||||
|
||||
if UIDevice.current.isIPad {
|
||||
let buttonSpacing: CGFloat = 28
|
||||
// `doneButton` is our widest button, so we position it relative to the superview
|
||||
// margin, and position other buttons relative to `doneButton`. This ensures
|
||||
// `donebutton` has a good distance from the edge *and* that all the buttons in the
|
||||
// cluster are centered WRT eachother.
|
||||
doneButton.autoPinEdge(toSuperviewMargin: .trailing)
|
||||
doneButton.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor, constant: -buttonSpacing).isActive = true
|
||||
|
||||
batchModeButton.autoAlignAxis(.vertical, toSameAxisOf: doneButton)
|
||||
batchModeButton.autoAlignAxis(.horizontal, toSameAxisOf: doneButton)
|
||||
|
||||
cameraModeButton.autoAlignAxis(.vertical, toSameAxisOf: doneButton)
|
||||
cameraModeButton.autoPinEdge(.bottom, to: .top, of: doneButton, withOffset: -buttonSpacing)
|
||||
|
||||
mediaLibraryModeButton.autoAlignAxis(.vertical, toSameAxisOf: cameraModeButton)
|
||||
mediaLibraryModeButton.autoAlignAxis(.horizontal, toSameAxisOf: cameraModeButton)
|
||||
} else {
|
||||
// we pin to edges rather than margin, because on notched devices the safeAreaInsets/margins change
|
||||
// as the device rotates *EVEN THOUGH* the interface is locked to portrait.
|
||||
|
||||
batchModeButton.centerYAnchor.constraint(equalTo: view.bottomAnchor, constant: bottomButtonsCenterOffset).isActive = true
|
||||
batchModeButton.autoPinEdge(toSuperviewEdge: .trailing, withInset: 16)
|
||||
|
||||
doneButton.centerYAnchor.constraint(equalTo: view.bottomAnchor, constant: bottomButtonsCenterOffset).isActive = true
|
||||
doneButton.autoPinEdge(toSuperviewEdge: .trailing, withInset: 16)
|
||||
|
||||
cameraModeButton.centerYAnchor.constraint(equalTo: view.bottomAnchor, constant: bottomButtonsCenterOffset).isActive = true
|
||||
cameraModeButton.autoPinEdge(toSuperviewEdge: .leading, withInset: 16)
|
||||
|
||||
mediaLibraryModeButton.centerYAnchor.constraint(equalTo: view.bottomAnchor, constant: bottomButtonsCenterOffset).isActive = true
|
||||
mediaLibraryModeButton.autoPinEdge(toSuperviewEdge: .leading, withInset: 16)
|
||||
}
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
DispatchQueue.main.async {
|
||||
// pre-layout views for snappier response should the user
|
||||
// decide to switch
|
||||
// Pre-layout views for snappier response should the user decide to switch.
|
||||
|
||||
if PHPhotoLibrary.authorizationStatus() == .authorized {
|
||||
self.mediaLibraryViewController.view.layoutIfNeeded()
|
||||
@ -184,96 +127,6 @@ class SendMediaNavigationController: OWSNavigationController {
|
||||
return navController
|
||||
}
|
||||
|
||||
private var isForcingBatchSelectInMediaLibrary = true
|
||||
|
||||
private var isShowingMediaLibrary = false
|
||||
private var isRecordingMovie = false
|
||||
|
||||
var isInBatchSelectMode: Bool {
|
||||
get {
|
||||
if isForcingBatchSelectInMediaLibrary && isShowingMediaLibrary {
|
||||
return true
|
||||
}
|
||||
return self.batchModeButton.isSelected
|
||||
}
|
||||
|
||||
set {
|
||||
let didChange = newValue != isInBatchSelectMode
|
||||
self.batchModeButton.isSelected = newValue
|
||||
|
||||
if didChange {
|
||||
mediaLibraryViewController.batchSelectModeDidChange()
|
||||
guard let topViewController = viewControllers.last else {
|
||||
return
|
||||
}
|
||||
updateViewState(topViewController: topViewController, animated: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateViewState(topViewController: UIViewController, animated: Bool) {
|
||||
let changes: () -> Void
|
||||
switch topViewController {
|
||||
case is AttachmentApprovalViewController:
|
||||
changes = {
|
||||
self.isShowingMediaLibrary = false
|
||||
self.batchModeButton.alpha = 0
|
||||
self.doneButton.alpha = 0
|
||||
self.cameraModeButton.alpha = 0
|
||||
self.mediaLibraryModeButton.alpha = 0
|
||||
}
|
||||
case let mediaLibraryView as ImagePickerGridController:
|
||||
changes = {
|
||||
self.isShowingMediaLibrary = true
|
||||
let showDoneButton = self.isInBatchSelectMode && self.attachmentCount > 0
|
||||
self.doneButton.alpha = showDoneButton ? 1 : 0
|
||||
|
||||
self.batchModeButton.alpha = showDoneButton || self.isForcingBatchSelectInMediaLibrary ? 0 : 1
|
||||
self.batchModeButton.isBeingPresentedOverPhotoCapture = false
|
||||
|
||||
self.cameraModeButton.alpha = 1
|
||||
self.cameraModeButton.isBeingPresentedOverPhotoCapture = false
|
||||
|
||||
self.mediaLibraryModeButton.alpha = 0
|
||||
self.mediaLibraryModeButton.isBeingPresentedOverPhotoCapture = false
|
||||
|
||||
mediaLibraryView.applyBatchSelectMode()
|
||||
}
|
||||
case is PhotoCaptureViewController:
|
||||
changes = {
|
||||
self.isShowingMediaLibrary = false
|
||||
let showDoneButton = self.isInBatchSelectMode && self.attachmentCount > 0
|
||||
self.doneButton.alpha = !showDoneButton || self.isRecordingMovie ? 0 : 1
|
||||
|
||||
self.batchModeButton.alpha = showDoneButton || self.isRecordingMovie ? 0 : 1
|
||||
self.batchModeButton.isBeingPresentedOverPhotoCapture = true
|
||||
|
||||
self.cameraModeButton.alpha = 0
|
||||
self.cameraModeButton.isBeingPresentedOverPhotoCapture = true
|
||||
|
||||
self.mediaLibraryModeButton.alpha = self.isRecordingMovie ? 0 : 1
|
||||
self.mediaLibraryModeButton.isBeingPresentedOverPhotoCapture = true
|
||||
}
|
||||
case is ConversationPickerViewController:
|
||||
changes = {
|
||||
self.doneButton.alpha = 0
|
||||
self.batchModeButton.alpha = 0
|
||||
self.cameraModeButton.alpha = 0
|
||||
self.mediaLibraryModeButton.alpha = 0
|
||||
}
|
||||
default:
|
||||
owsFailDebug("unexpected topViewController: \(topViewController)")
|
||||
changes = { }
|
||||
}
|
||||
|
||||
if animated {
|
||||
UIView.animate(withDuration: 0.3, animations: changes)
|
||||
} else {
|
||||
changes()
|
||||
}
|
||||
doneButton.updateCount()
|
||||
}
|
||||
|
||||
func fadeTo(viewControllers: [UIViewController], duration: CFTimeInterval) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
@ -284,63 +137,6 @@ class SendMediaNavigationController: OWSNavigationController {
|
||||
setViewControllers(viewControllers, animated: false)
|
||||
}
|
||||
|
||||
// MARK: - Events
|
||||
|
||||
private func didTapBatchModeButton() {
|
||||
isInBatchSelectMode = !isInBatchSelectMode
|
||||
owsAssertDebug(isInBatchSelectMode || attachmentCount <= 1)
|
||||
}
|
||||
|
||||
private func didTapCameraModeButton() {
|
||||
self.ows_askForCameraPermissions { isGranted in
|
||||
guard isGranted else { return }
|
||||
|
||||
BenchEventStart(title: "Show-Camera", eventId: "Show-Camera")
|
||||
self.fadeTo(viewControllers: [self.captureViewController], duration: 0.08)
|
||||
}
|
||||
}
|
||||
|
||||
private func didTapMediaLibraryModeButton() {
|
||||
self.ows_askForMediaLibraryPermissions { isGranted in
|
||||
guard isGranted else { return }
|
||||
|
||||
BenchEventStart(title: "Show-Media-Library", eventId: "Show-Media-Library")
|
||||
self.fadeTo(viewControllers: [self.mediaLibraryViewController], duration: 0.08)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Views
|
||||
public static let bottomButtonWidth: CGFloat = 44
|
||||
|
||||
private lazy var doneButton: DoneButton = {
|
||||
let button = DoneButton()
|
||||
button.delegate = self
|
||||
button.setShadow()
|
||||
|
||||
return button
|
||||
}()
|
||||
|
||||
private lazy var batchModeButton: SendMediaBottomButton = {
|
||||
return SendMediaBottomButton(imageName: "create-album-filled-28",
|
||||
tintColor: .ows_white,
|
||||
diameter: type(of: self).bottomButtonWidth,
|
||||
block: { [weak self] in self?.didTapBatchModeButton() })
|
||||
}()
|
||||
|
||||
private lazy var cameraModeButton: SendMediaBottomButton = {
|
||||
return SendMediaBottomButton(imageName: "camera-outline-28",
|
||||
tintColor: .ows_white,
|
||||
diameter: type(of: self).bottomButtonWidth,
|
||||
block: { [weak self] in self?.didTapCameraModeButton() })
|
||||
}()
|
||||
|
||||
private lazy var mediaLibraryModeButton: SendMediaBottomButton = {
|
||||
return SendMediaBottomButton(imageName: "photo-outline-28",
|
||||
tintColor: .ows_white,
|
||||
diameter: type(of: self).bottomButtonWidth,
|
||||
block: { [weak self] in self?.didTapMediaLibraryModeButton() })
|
||||
}()
|
||||
|
||||
// MARK: State
|
||||
|
||||
private var attachmentDraftCollection: AttachmentDraftCollection = .empty
|
||||
@ -352,24 +148,22 @@ class SendMediaNavigationController: OWSNavigationController {
|
||||
// MARK: Child VC's
|
||||
|
||||
fileprivate lazy var captureViewController: PhotoCaptureViewController = {
|
||||
let vc = PhotoCaptureViewController()
|
||||
vc.delegate = self
|
||||
|
||||
return vc
|
||||
let viewController = PhotoCaptureViewController()
|
||||
viewController.delegate = self
|
||||
viewController.dataSource = self
|
||||
return viewController
|
||||
}()
|
||||
|
||||
private lazy var mediaLibraryViewController: ImagePickerGridController = {
|
||||
let vc = ImagePickerGridController()
|
||||
vc.delegate = self
|
||||
|
||||
return vc
|
||||
let viewController = ImagePickerGridController()
|
||||
viewController.delegate = self
|
||||
viewController.dataSource = self
|
||||
return viewController
|
||||
}()
|
||||
|
||||
private func pushApprovalViewController(
|
||||
attachmentApprovalItems: [AttachmentApprovalItem],
|
||||
options: AttachmentApprovalViewControllerOptions = .canAddMore,
|
||||
animated: Bool
|
||||
) {
|
||||
private func pushApprovalViewController(attachmentApprovalItems: [AttachmentApprovalItem],
|
||||
options: AttachmentApprovalViewControllerOptions = .canAddMore,
|
||||
animated: Bool) {
|
||||
guard let sendMediaNavDelegate = self.sendMediaNavDelegate else {
|
||||
owsFailDebug("sendMediaNavDelegate was unexpectedly nil")
|
||||
return
|
||||
@ -394,51 +188,35 @@ class SendMediaNavigationController: OWSNavigationController {
|
||||
} else {
|
||||
let alert = ActionSheetController(title: nil, message: nil)
|
||||
|
||||
let confirmAbandonText = NSLocalizedString("SEND_MEDIA_CONFIRM_ABANDON_ALBUM", comment: "alert action, confirming the user wants to exit the media flow and abandon any photos they've taken")
|
||||
let confirmAbandonText = NSLocalizedString("SEND_MEDIA_CONFIRM_ABANDON_ALBUM",
|
||||
comment: "alert action, confirming the user wants to exit the media flow and abandon any photos they've taken")
|
||||
let confirmAbandonAction = ActionSheetAction(title: confirmAbandonText,
|
||||
style: .destructive,
|
||||
handler: { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
self.sendMediaNavDelegate?.sendMediaNavDidCancel(self)
|
||||
style: .destructive,
|
||||
handler: { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
self.sendMediaNavDelegate?.sendMediaNavDidCancel(self)
|
||||
})
|
||||
alert.addAction(confirmAbandonAction)
|
||||
let dontAbandonAction = ActionSheetAction(title: dontAbandonText,
|
||||
style: .default,
|
||||
handler: { _ in })
|
||||
style: .default,
|
||||
handler: { _ in })
|
||||
alert.addAction(dontAbandonAction)
|
||||
|
||||
self.presentActionSheet(alert)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension SendMediaNavigationController: UINavigationControllerDelegate {
|
||||
|
||||
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
|
||||
updateNavbarTheme(for: viewController, animated: animated)
|
||||
|
||||
switch viewController {
|
||||
case is PhotoCaptureViewController:
|
||||
if attachmentDraftCollection.count == 1 && !isInBatchSelectMode {
|
||||
// User is navigating "back" to the previous view, indicating
|
||||
// they want to discard the previously captured item
|
||||
discardDraft()
|
||||
}
|
||||
case is ImagePickerGridController:
|
||||
if attachmentDraftCollection.count == 1 && !isInBatchSelectMode {
|
||||
isInBatchSelectMode = true
|
||||
mediaLibraryViewController.reloadData()
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
updateViewState(topViewController: viewController, animated: false)
|
||||
}
|
||||
|
||||
// In case back navigation was canceled, we re-apply whatever is showing.
|
||||
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
|
||||
updateNavbarTheme(for: viewController, animated: animated)
|
||||
updateViewState(topViewController: viewController, animated: false)
|
||||
}
|
||||
|
||||
func navigationControllerSupportedInterfaceOrientations(_ navigationController: UINavigationController) -> UIInterfaceOrientationMask {
|
||||
@ -467,7 +245,7 @@ extension SendMediaNavigationController: UINavigationControllerDelegate {
|
||||
case is AttachmentApprovalViewController:
|
||||
showNavbar(.alwaysDarkAndClear)
|
||||
case is ImagePickerGridController:
|
||||
showNavbar(.alwaysDark)
|
||||
showNavbar(.alwaysDarkAndClear)
|
||||
case is ConversationPickerViewController:
|
||||
showNavbar(.default)
|
||||
default:
|
||||
@ -479,34 +257,23 @@ extension SendMediaNavigationController: UINavigationControllerDelegate {
|
||||
// MARK: - Too Many
|
||||
|
||||
func showTooManySelectedToast() {
|
||||
Logger.info("")
|
||||
|
||||
let toastFormat = NSLocalizedString("IMAGE_PICKER_CAN_SELECT_NO_MORE_TOAST_FORMAT",
|
||||
comment: "Momentarily shown to the user when attempting to select more images than is allowed. Embeds {{max number of items}} that can be shared.")
|
||||
|
||||
let toastText = String(format: toastFormat, NSNumber(value: SignalAttachment.maxAttachmentsAllowed))
|
||||
|
||||
let toastController = ToastController(text: toastText)
|
||||
|
||||
let kToastInset: CGFloat = 10
|
||||
let bottomInset = kToastInset + view.layoutMargins.bottom
|
||||
|
||||
toastController.presentToastView(fromBottomOfView: view, inset: bottomInset)
|
||||
toastController.presentToastView(fromBottomOfView: view, inset: view.layoutMargins.bottom + 10)
|
||||
}
|
||||
}
|
||||
|
||||
extension SendMediaNavigationController: PhotoCaptureViewControllerDelegate {
|
||||
|
||||
func photoCaptureViewController(_ photoCaptureViewController: PhotoCaptureViewController, didFinishProcessingAttachment attachment: SignalAttachment) {
|
||||
guard let sendMediaNavDelegate = self.sendMediaNavDelegate else { return }
|
||||
let cameraCaptureAttachment = CameraCaptureAttachment(signalAttachment: attachment, canSave: sendMediaNavDelegate.sendMediaNavCanSaveAttachments)
|
||||
attachmentDraftCollection.append(.camera(attachment: cameraCaptureAttachment))
|
||||
if isInBatchSelectMode {
|
||||
updateViewState(topViewController: photoCaptureViewController, animated: false)
|
||||
} else {
|
||||
pushApprovalViewController(attachmentApprovalItems: [cameraCaptureAttachment.attachmentApprovalItem],
|
||||
animated: true)
|
||||
func photoCaptureViewControllerDidFinish(_ photoCaptureViewController: PhotoCaptureViewController) {
|
||||
guard attachmentDraftCollection.count > 0 else {
|
||||
owsFailDebug("No camera attachments found")
|
||||
return
|
||||
}
|
||||
showApprovalAfterProcessingAnyMediaLibrarySelections()
|
||||
}
|
||||
|
||||
func photoCaptureViewControllerDidCancel(_ photoCaptureViewController: PhotoCaptureViewController) {
|
||||
@ -514,6 +281,13 @@ extension SendMediaNavigationController: PhotoCaptureViewControllerDelegate {
|
||||
didRequestExit(dontAbandonText: dontAbandonText)
|
||||
}
|
||||
|
||||
func photoCaptureViewControllerViewWillAppear(_ photoCaptureViewController: PhotoCaptureViewController) {
|
||||
if !photoCaptureViewController.isInBatchMode, attachmentCount == 1, case .camera(_) = attachmentDraftCollection.attachmentDrafts.last {
|
||||
// User is navigating back to the camera screen, indicating they want to discard the previously captured item.
|
||||
discardDraft()
|
||||
}
|
||||
}
|
||||
|
||||
func photoCaptureViewControllerDidTryToCaptureTooMany(_ photoCaptureViewController: PhotoCaptureViewController) {
|
||||
showTooManySelectedToast()
|
||||
}
|
||||
@ -530,19 +304,62 @@ extension SendMediaNavigationController: PhotoCaptureViewControllerDelegate {
|
||||
owsAssertDebug(attachmentDraftCollection.attachmentDrafts.count == 0)
|
||||
}
|
||||
|
||||
func photoCaptureViewController(_ photoCaptureViewController: PhotoCaptureViewController, isRecordingMovie: Bool) {
|
||||
self.isRecordingMovie = isRecordingMovie
|
||||
updateViewState(topViewController: photoCaptureViewController, animated: true)
|
||||
func photoCaptureViewControllerDidRequestPresentPhotoLibrary(_ photoCaptureViewController: PhotoCaptureViewController) {
|
||||
self.ows_askForMediaLibraryPermissions { isGranted in
|
||||
guard isGranted else { return }
|
||||
|
||||
BenchEventStart(title: "Show-Media-Library", eventId: "Show-Media-Library")
|
||||
let presentedViewController = OWSNavigationController(rootViewController: self.mediaLibraryViewController)
|
||||
if let owsNavBar = presentedViewController.navigationBar as? OWSNavigationBar {
|
||||
owsNavBar.switchToStyle(.alwaysDarkAndClear)
|
||||
}
|
||||
self.presentFullScreen(presentedViewController, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
func photoCaptureViewController(_ photoCaptureViewController: PhotoCaptureViewController, didRequestSwitchBatchMode batchMode: Bool) -> Bool {
|
||||
if batchMode {
|
||||
// Always can be enabled
|
||||
return true
|
||||
}
|
||||
// Can only be disabled if there's no media attachments yet.
|
||||
return attachmentCount > 0
|
||||
}
|
||||
}
|
||||
|
||||
extension SendMediaNavigationController: PhotoCaptureViewControllerDataSource {
|
||||
|
||||
var numberOfMediaItems: Int {
|
||||
attachmentCount
|
||||
}
|
||||
|
||||
func addMedia(attachment: SignalAttachment) {
|
||||
guard let sendMediaNavDelegate = self.sendMediaNavDelegate else { return }
|
||||
let cameraCaptureAttachment = CameraCaptureAttachment(signalAttachment: attachment, canSave: sendMediaNavDelegate.sendMediaNavCanSaveAttachments)
|
||||
attachmentDraftCollection.append(.camera(attachment: cameraCaptureAttachment))
|
||||
}
|
||||
}
|
||||
|
||||
extension SendMediaNavigationController: ImagePickerGridControllerDelegate {
|
||||
|
||||
func imagePickerDidCompleteSelection(_ imagePicker: ImagePickerGridController) {
|
||||
func imagePickerDidRequestSendMedia(_ imagePicker: ImagePickerGridController) {
|
||||
if let navigationController = presentedViewController as? OWSNavigationController,
|
||||
navigationController.viewControllers.contains(imagePicker) {
|
||||
dismiss(animated: true) {
|
||||
self.showApprovalAfterProcessingAnyMediaLibrarySelections()
|
||||
}
|
||||
return
|
||||
}
|
||||
showApprovalAfterProcessingAnyMediaLibrarySelections()
|
||||
}
|
||||
|
||||
func imagePickerDidCancel(_ imagePicker: ImagePickerGridController) {
|
||||
if let navigationController = presentedViewController as? OWSNavigationController,
|
||||
navigationController.viewControllers.contains(imagePicker) {
|
||||
dismiss(animated: true)
|
||||
return
|
||||
}
|
||||
|
||||
let dontAbandonText = NSLocalizedString("SEND_MEDIA_RETURN_TO_MEDIA_LIBRARY", comment: "alert action when the user decides not to cancel the media flow after all.")
|
||||
didRequestExit(dontAbandonText: dontAbandonText)
|
||||
}
|
||||
@ -582,16 +399,9 @@ extension SendMediaNavigationController: ImagePickerGridControllerDelegate {
|
||||
backgroundBlock: backgroundBlock)
|
||||
}
|
||||
|
||||
func imagePicker(_ imagePicker: ImagePickerGridController, isAssetSelected asset: PHAsset) -> Bool {
|
||||
return attachmentDraftCollection.hasPickerAttachment(forAsset: asset)
|
||||
}
|
||||
|
||||
func imagePicker(_ imagePicker: ImagePickerGridController, didSelectAsset asset: PHAsset, attachmentPromise: Promise<SignalAttachment>) {
|
||||
guard let sendMediaNavDelegate = sendMediaNavDelegate else { return }
|
||||
|
||||
guard !attachmentDraftCollection.hasPickerAttachment(forAsset: asset) else {
|
||||
return
|
||||
}
|
||||
guard !attachmentDraftCollection.hasPickerAttachment(forAsset: asset) else { return }
|
||||
|
||||
let attachmentApprovalItemPromise = attachmentPromise.map { attachment in
|
||||
AttachmentApprovalItem(attachment: attachment,
|
||||
@ -600,8 +410,6 @@ extension SendMediaNavigationController: ImagePickerGridControllerDelegate {
|
||||
|
||||
let libraryMedia = MediaLibraryAttachment(asset: asset, attachmentApprovalItemPromise: attachmentApprovalItemPromise)
|
||||
attachmentDraftCollection.append(.picker(attachment: libraryMedia))
|
||||
|
||||
updateViewState(topViewController: imagePicker, animated: false)
|
||||
}
|
||||
|
||||
func imagePicker(_ imagePicker: ImagePickerGridController, didDeselectAsset asset: PHAsset) {
|
||||
@ -609,12 +417,6 @@ extension SendMediaNavigationController: ImagePickerGridControllerDelegate {
|
||||
return
|
||||
}
|
||||
attachmentDraftCollection.remove(.picker(attachment: draft))
|
||||
|
||||
updateViewState(topViewController: imagePicker, animated: false)
|
||||
}
|
||||
|
||||
func imagePickerCanSelectMoreItems(_ imagePicker: ImagePickerGridController) -> Bool {
|
||||
return attachmentCount < SignalAttachment.maxAttachmentsAllowed
|
||||
}
|
||||
|
||||
func imagePickerDidTryToSelectTooMany(_ imagePicker: ImagePickerGridController) {
|
||||
@ -622,10 +424,21 @@ extension SendMediaNavigationController: ImagePickerGridControllerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
extension SendMediaNavigationController: ImagePickerGridControllerDataSource {
|
||||
|
||||
func imagePicker(_ imagePicker: ImagePickerGridController, isAssetSelected asset: PHAsset) -> Bool {
|
||||
return attachmentDraftCollection.hasPickerAttachment(forAsset: asset)
|
||||
}
|
||||
|
||||
func imagePickerCanSelectMoreItems(_ imagePicker: ImagePickerGridController) -> Bool {
|
||||
return attachmentCount < SignalAttachment.maxAttachmentsAllowed
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension SendMediaNavigationController: AttachmentApprovalViewControllerDelegate {
|
||||
|
||||
func attachmentApprovalDidAppear(_ attachmentApproval: AttachmentApprovalViewController) {
|
||||
updateViewState(topViewController: attachmentApproval, animated: true)
|
||||
}
|
||||
|
||||
func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didChangeMessageBody newMessageBody: MessageBody?) {
|
||||
@ -653,8 +466,9 @@ extension SendMediaNavigationController: AttachmentApprovalViewControllerDelegat
|
||||
// Current design dicates we'll go "back" to the single thing before us.
|
||||
owsAssertDebug(viewControllers.count == 2)
|
||||
|
||||
// regardless of which VC we're going "back" to, we're in "batch" mode at this point.
|
||||
isInBatchSelectMode = true
|
||||
if let cameraViewController = viewControllers.first as? PhotoCaptureViewController {
|
||||
cameraViewController.switchToBatchMode()
|
||||
}
|
||||
|
||||
popViewController(animated: true)
|
||||
}
|
||||
@ -779,7 +593,7 @@ private struct CameraCaptureAttachment: Hashable, Equatable {
|
||||
hasher.combine(signalAttachment)
|
||||
}
|
||||
|
||||
static func ==(lhs: CameraCaptureAttachment, rhs: CameraCaptureAttachment) -> Bool {
|
||||
static func == (lhs: CameraCaptureAttachment, rhs: CameraCaptureAttachment) -> Bool {
|
||||
return lhs.signalAttachment == rhs.signalAttachment
|
||||
}
|
||||
}
|
||||
@ -792,102 +606,7 @@ private struct MediaLibraryAttachment: Hashable, Equatable {
|
||||
hasher.combine(asset)
|
||||
}
|
||||
|
||||
static func ==(lhs: MediaLibraryAttachment, rhs: MediaLibraryAttachment) -> Bool {
|
||||
static func == (lhs: MediaLibraryAttachment, rhs: MediaLibraryAttachment) -> Bool {
|
||||
return lhs.asset == rhs.asset
|
||||
}
|
||||
}
|
||||
|
||||
extension SendMediaNavigationController: DoneButtonDelegate {
|
||||
var doneButtonCount: Int {
|
||||
return attachmentCount
|
||||
}
|
||||
|
||||
fileprivate func doneButtonWasTapped(_ doneButton: DoneButton) {
|
||||
owsAssertDebug(attachmentDraftCollection.count > 0)
|
||||
showApprovalAfterProcessingAnyMediaLibrarySelections()
|
||||
}
|
||||
}
|
||||
|
||||
private protocol DoneButtonDelegate: AnyObject {
|
||||
func doneButtonWasTapped(_ doneButton: DoneButton)
|
||||
var doneButtonCount: Int { get }
|
||||
}
|
||||
|
||||
private class DoneButton: UIView {
|
||||
weak var delegate: DoneButtonDelegate?
|
||||
|
||||
init() {
|
||||
super.init(frame: .zero)
|
||||
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTap(tapGesture:)))
|
||||
addGestureRecognizer(tapGesture)
|
||||
|
||||
let container = PillView()
|
||||
container.backgroundColor = .ows_white
|
||||
container.layoutMargins = UIEdgeInsets(top: 7, leading: 8, bottom: 7, trailing: 8)
|
||||
|
||||
addSubview(container)
|
||||
container.autoPinEdgesToSuperviewMargins()
|
||||
|
||||
let stackView = UIStackView(arrangedSubviews: [badge, chevron])
|
||||
stackView.axis = .horizontal
|
||||
stackView.alignment = .center
|
||||
stackView.spacing = 9
|
||||
|
||||
container.addSubview(stackView)
|
||||
stackView.autoPinEdgesToSuperviewMargins()
|
||||
}
|
||||
|
||||
let numberFormatter: NumberFormatter = NumberFormatter()
|
||||
|
||||
func updateCount() {
|
||||
guard let delegate = delegate else {
|
||||
return
|
||||
}
|
||||
|
||||
badgeLabel.text = numberFormatter.string(for: delegate.doneButtonCount)
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
// MARK: - Subviews
|
||||
|
||||
private lazy var badge: UIView = {
|
||||
let badge = PillView()
|
||||
badge.layoutMargins = UIEdgeInsets(top: 4, leading: 4, bottom: 4, trailing: 4)
|
||||
badge.backgroundColor = .ows_accentBlue
|
||||
badge.addSubview(badgeLabel)
|
||||
badgeLabel.autoPinEdgesToSuperviewMargins()
|
||||
|
||||
return badge
|
||||
}()
|
||||
|
||||
private lazy var badgeLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = .ows_white
|
||||
label.font = UIFont.ows_dynamicTypeSubheadline.ows_monospaced
|
||||
label.textAlignment = .center
|
||||
return label
|
||||
}()
|
||||
|
||||
private lazy var chevron: UIView = {
|
||||
let image: UIImage
|
||||
if CurrentAppContext().isRTL {
|
||||
image = #imageLiteral(resourceName: "small_chevron_left")
|
||||
} else {
|
||||
image = #imageLiteral(resourceName: "small_chevron_right")
|
||||
}
|
||||
let chevron = UIImageView(image: image.withRenderingMode(.alwaysTemplate))
|
||||
chevron.contentMode = .scaleAspectFit
|
||||
chevron.tintColor = .ows_gray60
|
||||
chevron.autoSetDimensions(to: CGSize(width: 10, height: 18))
|
||||
|
||||
return chevron
|
||||
}()
|
||||
|
||||
@objc
|
||||
func didTap(tapGesture: UITapGestureRecognizer) {
|
||||
delegate?.doneButtonWasTapped(self)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
//
|
||||
// Copyright (c) 2020 Open Whisper Systems. All rights reserved.
|
||||
// Copyright (c) 2022 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import SignalUI
|
||||
import UIKit
|
||||
|
||||
public enum PhotoGridItemType {
|
||||
case photo, animated, video
|
||||
}
|
||||
@ -18,8 +21,8 @@ public class PhotoGridViewCell: UICollectionViewCell {
|
||||
public let imageView: UIImageView
|
||||
|
||||
private let contentTypeBadgeView: UIImageView
|
||||
private let unselectedBadgeView: UIView
|
||||
private let selectedBadgeView: UIImageView
|
||||
private let outlineBadgeView: UIView
|
||||
private let selectedBadgeView: UIView
|
||||
|
||||
private let highlightedMaskView: UIView
|
||||
private let selectedMaskView: UIView
|
||||
@ -28,7 +31,7 @@ public class PhotoGridViewCell: UICollectionViewCell {
|
||||
|
||||
private static let videoBadgeImage = #imageLiteral(resourceName: "ic_gallery_badge_video")
|
||||
private static let animatedBadgeImage = #imageLiteral(resourceName: "ic_gallery_badge_gif")
|
||||
private static let selectedBadgeImage = #imageLiteral(resourceName: "image_editor_checkmark_full").withRenderingMode(.alwaysTemplate)
|
||||
private static let selectedBadgeImage = UIImage(named: "media-composer-checkmark")
|
||||
public var loadingColor = Theme.washColor
|
||||
|
||||
override public var isSelected: Bool {
|
||||
@ -45,15 +48,15 @@ public class PhotoGridViewCell: UICollectionViewCell {
|
||||
|
||||
func updateSelectionState() {
|
||||
if isSelected {
|
||||
unselectedBadgeView.isHidden = true
|
||||
outlineBadgeView.isHidden = false
|
||||
selectedBadgeView.isHidden = false
|
||||
selectedMaskView.isHidden = false
|
||||
} else if allowsMultipleSelection {
|
||||
unselectedBadgeView.isHidden = false
|
||||
outlineBadgeView.isHidden = false
|
||||
selectedBadgeView.isHidden = true
|
||||
selectedMaskView.isHidden = true
|
||||
} else {
|
||||
unselectedBadgeView.isHidden = true
|
||||
outlineBadgeView.isHidden = true
|
||||
selectedBadgeView.isHidden = true
|
||||
selectedMaskView.isHidden = true
|
||||
}
|
||||
@ -66,43 +69,49 @@ public class PhotoGridViewCell: UICollectionViewCell {
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.imageView = UIImageView()
|
||||
let selectionBadgeSize: CGFloat = 22
|
||||
let contentTypeBadgeSize: CGFloat = 12
|
||||
|
||||
imageView = UIImageView()
|
||||
imageView.contentMode = .scaleAspectFill
|
||||
|
||||
self.contentTypeBadgeView = UIImageView()
|
||||
contentTypeBadgeView = UIImageView()
|
||||
contentTypeBadgeView.isHidden = true
|
||||
|
||||
self.selectedBadgeView = UIImageView()
|
||||
selectedBadgeView.image = PhotoGridViewCell.selectedBadgeImage
|
||||
selectedBadgeView = CircleView(diameter: selectionBadgeSize)
|
||||
selectedBadgeView.backgroundColor = .ows_accentBlue
|
||||
selectedBadgeView.isHidden = true
|
||||
selectedBadgeView.tintColor = .white
|
||||
let checkmarkImageView = UIImageView(image: PhotoGridViewCell.selectedBadgeImage)
|
||||
checkmarkImageView.tintColor = .white
|
||||
selectedBadgeView.addSubview(checkmarkImageView)
|
||||
checkmarkImageView.autoCenterInSuperview()
|
||||
|
||||
self.unselectedBadgeView = CircleView()
|
||||
unselectedBadgeView.backgroundColor = .clear
|
||||
unselectedBadgeView.layer.borderWidth = 0.5
|
||||
unselectedBadgeView.layer.borderColor = UIColor.white.cgColor
|
||||
outlineBadgeView = CircleView()
|
||||
outlineBadgeView.backgroundColor = .clear
|
||||
outlineBadgeView.layer.borderWidth = 1.5
|
||||
outlineBadgeView.layer.borderColor = UIColor.ows_white.cgColor
|
||||
selectedBadgeView.isHidden = true
|
||||
|
||||
self.highlightedMaskView = UIView()
|
||||
highlightedMaskView = UIView()
|
||||
highlightedMaskView.alpha = 0.2
|
||||
highlightedMaskView.backgroundColor = Theme.darkThemePrimaryColor
|
||||
highlightedMaskView.isHidden = true
|
||||
|
||||
self.selectedMaskView = UIView()
|
||||
selectedMaskView = UIView()
|
||||
selectedMaskView.alpha = 0.3
|
||||
selectedMaskView.backgroundColor = Theme.darkThemeBackgroundColor
|
||||
selectedMaskView.isHidden = true
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.clipsToBounds = true
|
||||
clipsToBounds = true
|
||||
|
||||
self.contentView.addSubview(imageView)
|
||||
self.contentView.addSubview(contentTypeBadgeView)
|
||||
self.contentView.addSubview(highlightedMaskView)
|
||||
self.contentView.addSubview(selectedMaskView)
|
||||
self.contentView.addSubview(unselectedBadgeView)
|
||||
self.contentView.addSubview(selectedBadgeView)
|
||||
contentView.addSubview(imageView)
|
||||
contentView.addSubview(contentTypeBadgeView)
|
||||
contentView.addSubview(highlightedMaskView)
|
||||
contentView.addSubview(selectedMaskView)
|
||||
contentView.addSubview(selectedBadgeView)
|
||||
contentView.addSubview(outlineBadgeView)
|
||||
|
||||
imageView.autoPinEdgesToSuperviewEdges()
|
||||
highlightedMaskView.autoPinEdgesToSuperviewEdges()
|
||||
@ -110,20 +119,17 @@ public class PhotoGridViewCell: UICollectionViewCell {
|
||||
|
||||
// Note assets were rendered to match exactly. We don't want to re-size with
|
||||
// content mode lest they become less legible.
|
||||
let kContentTypeBadgeSize = CGSize(square: 12)
|
||||
contentTypeBadgeView.autoSetDimensions(to: CGSize(square: contentTypeBadgeSize))
|
||||
contentTypeBadgeView.autoPinEdge(toSuperviewEdge: .leading, withInset: 3)
|
||||
contentTypeBadgeView.autoPinEdge(toSuperviewEdge: .bottom, withInset: 3)
|
||||
contentTypeBadgeView.autoSetDimensions(to: kContentTypeBadgeSize)
|
||||
|
||||
let kUnselectedBadgeSize = CGSize(square: 22)
|
||||
unselectedBadgeView.autoPinEdge(toSuperviewEdge: .trailing, withInset: 4)
|
||||
unselectedBadgeView.autoPinEdge(toSuperviewEdge: .top, withInset: 4)
|
||||
unselectedBadgeView.autoSetDimensions(to: kUnselectedBadgeSize)
|
||||
outlineBadgeView.autoSetDimensions(to: CGSize(square: selectionBadgeSize))
|
||||
outlineBadgeView.autoPinEdge(toSuperviewEdge: .trailing, withInset: 6)
|
||||
outlineBadgeView.autoPinEdge(toSuperviewEdge: .top, withInset: 6)
|
||||
|
||||
let kSelectedBadgeSize = CGSize(square: 22)
|
||||
selectedBadgeView.autoSetDimensions(to: kSelectedBadgeSize)
|
||||
selectedBadgeView.autoAlignAxis(.vertical, toSameAxisOf: unselectedBadgeView)
|
||||
selectedBadgeView.autoAlignAxis(.horizontal, toSameAxisOf: unselectedBadgeView)
|
||||
selectedBadgeView.autoSetDimensions(to: CGSize(square: selectionBadgeSize))
|
||||
selectedBadgeView.autoAlignAxis(.vertical, toSameAxisOf: outlineBadgeView)
|
||||
selectedBadgeView.autoAlignAxis(.horizontal, toSameAxisOf: outlineBadgeView)
|
||||
}
|
||||
|
||||
@available(*, unavailable, message: "Unimplemented")
|
||||
@ -191,6 +197,6 @@ public class PhotoGridViewCell: UICollectionViewCell {
|
||||
highlightedMaskView.isHidden = true
|
||||
selectedMaskView.isHidden = true
|
||||
selectedBadgeView.isHidden = true
|
||||
unselectedBadgeView.isHidden = true
|
||||
outlineBadgeView.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
@ -237,6 +237,13 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
|
||||
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapTouchInterceptorView(gesture:)))
|
||||
touchInterceptorView.addGestureRecognizer(tapGesture)
|
||||
|
||||
let bottomToolViewWidth = view.bounds.width
|
||||
let bottomToolViewHeight = bottomToolView.systemLayoutSizeFitting(view.bounds.size, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel).height
|
||||
bottomToolView.frame = CGRect(x: 0, y: view.bounds.maxY - bottomToolViewHeight, width: bottomToolViewWidth, height: bottomToolViewHeight)
|
||||
UIView.performWithoutAnimation {
|
||||
bottomToolView.setNeedsLayout()
|
||||
bottomToolView.layoutIfNeeded()
|
||||
}
|
||||
view.addSubview(bottomToolView)
|
||||
bottomToolView.autoPinWidthToSuperview()
|
||||
bottomToolViewBottomConstraint = bottomToolView.autoPinEdge(toSuperviewEdge: .bottom)
|
||||
|
||||
@ -37,7 +37,13 @@ public class AttachmentPrepViewController: OWSViewController {
|
||||
private(set) var contentContainer: UIView!
|
||||
|
||||
private var imageEditorView: ImageEditorView?
|
||||
private var imageEditorViewConstraintsPortrait: [NSLayoutConstraint]?
|
||||
private var imageEditorViewConstraintsLandscape: [NSLayoutConstraint]?
|
||||
|
||||
private var videoEditorView: VideoEditorView?
|
||||
private var videoEditorViewConstraintsPortrait: [NSLayoutConstraint]?
|
||||
private var videoEditorViewConstraintsLandscape: [NSLayoutConstraint]?
|
||||
|
||||
private var mediaMessageView: MediaMessageView?
|
||||
|
||||
public var shouldHideControls: Bool {
|
||||
@ -61,20 +67,20 @@ public class AttachmentPrepViewController: OWSViewController {
|
||||
// MARK: - View Lifecycle
|
||||
|
||||
override public func loadView() {
|
||||
self.view = UIView()
|
||||
view = UIView(frame: UIScreen.main.bounds)
|
||||
view.backgroundColor = .ows_black
|
||||
|
||||
// Anything that should be shrunk when user pops keyboard lives in the contentContainer.
|
||||
let contentContainer = UIView()
|
||||
self.contentContainer = contentContainer
|
||||
contentContainer = UIView(frame: view.bounds)
|
||||
view.addSubview(contentContainer)
|
||||
contentContainer.autoPinEdgesToSuperviewEdges()
|
||||
|
||||
// Scroll View - used to zoom/pan on images and video
|
||||
scrollView = UIScrollView()
|
||||
contentContainer.addSubview(scrollView)
|
||||
scrollView = UIScrollView(frame: contentContainer.bounds)
|
||||
scrollView.delegate = self
|
||||
scrollView.showsHorizontalScrollIndicator = false
|
||||
scrollView.showsVerticalScrollIndicator = false
|
||||
contentContainer.addSubview(scrollView)
|
||||
|
||||
// Panning should stop pretty soon after the user stops scrolling
|
||||
scrollView.decelerationRate = UIScrollView.DecelerationRate.fast
|
||||
@ -85,26 +91,38 @@ public class AttachmentPrepViewController: OWSViewController {
|
||||
|
||||
scrollView.autoPinEdgesToSuperviewEdges()
|
||||
|
||||
let backgroundColor = UIColor.black
|
||||
self.view.backgroundColor = backgroundColor
|
||||
|
||||
// Create full screen container view so the scrollView
|
||||
// can compute an appropriate content size in which to center
|
||||
// our media view.
|
||||
let containerView = UIView.container()
|
||||
containerView.frame = view.bounds
|
||||
scrollView.addSubview(containerView)
|
||||
containerView.autoPinEdgesToSuperviewEdges()
|
||||
containerView.autoMatch(.height, to: .height, of: self.view)
|
||||
containerView.autoMatch(.width, to: .width, of: self.view)
|
||||
containerView.autoMatch(.height, to: .height, of: view)
|
||||
containerView.autoMatch(.width, to: .width, of: view)
|
||||
|
||||
let contentMarginTop = UIDevice.current.hasIPhoneXNotch ? CurrentAppContext().statusBarHeight : 0
|
||||
|
||||
if let imageEditorModel = attachmentApprovalItem.imageEditorModel {
|
||||
|
||||
let imageEditorView = ImageEditorView(model: imageEditorModel, delegate: self)
|
||||
imageEditorView.frame = view.bounds
|
||||
imageEditorView.configureSubviews()
|
||||
self.imageEditorView = imageEditorView
|
||||
|
||||
imageEditorView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(imageEditorView)
|
||||
imageEditorView.autoPinEdgesToSuperviewEdges()
|
||||
|
||||
imageEditorViewConstraintsPortrait = [
|
||||
imageEditorView.heightAnchor.constraint(equalTo: imageEditorView.widthAnchor, multiplier: 16/9),
|
||||
imageEditorView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
imageEditorView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
imageEditorView.topAnchor.constraint(equalTo: view.topAnchor, constant: contentMarginTop) ]
|
||||
imageEditorViewConstraintsLandscape = [
|
||||
imageEditorView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
imageEditorView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
imageEditorView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
imageEditorView.bottomAnchor.constraint(equalTo: view.bottomAnchor) ]
|
||||
|
||||
self.imageEditorView = imageEditorView
|
||||
|
||||
imageEditorUpdateNavigationBar()
|
||||
} else if let videoEditorModel = attachmentApprovalItem.videoEditorModel {
|
||||
@ -112,11 +130,23 @@ public class AttachmentPrepViewController: OWSViewController {
|
||||
let videoEditorView = VideoEditorView(model: videoEditorModel,
|
||||
attachmentApprovalItem: attachmentApprovalItem,
|
||||
delegate: self)
|
||||
videoEditorView.frame = view.bounds
|
||||
videoEditorView.configureSubviews()
|
||||
self.videoEditorView = videoEditorView
|
||||
|
||||
videoEditorView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(videoEditorView)
|
||||
videoEditorView.autoPinEdgesToSuperviewEdges()
|
||||
|
||||
videoEditorViewConstraintsPortrait = [
|
||||
videoEditorView.heightAnchor.constraint(equalTo: videoEditorView.widthAnchor, multiplier: 16/9),
|
||||
videoEditorView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
videoEditorView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
videoEditorView.topAnchor.constraint(equalTo: view.topAnchor, constant: contentMarginTop) ]
|
||||
videoEditorViewConstraintsLandscape = [
|
||||
videoEditorView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
videoEditorView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
videoEditorView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
videoEditorView.bottomAnchor.constraint(equalTo: view.bottomAnchor) ]
|
||||
|
||||
self.videoEditorView = videoEditorView
|
||||
|
||||
videoEditorUpdateNavigationBar()
|
||||
} else {
|
||||
@ -127,6 +157,12 @@ public class AttachmentPrepViewController: OWSViewController {
|
||||
}
|
||||
}
|
||||
|
||||
public override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
updateLayoutConstraints()
|
||||
}
|
||||
|
||||
override public func viewWillAppear(_ animated: Bool) {
|
||||
Logger.debug("")
|
||||
|
||||
@ -159,6 +195,14 @@ public class AttachmentPrepViewController: OWSViewController {
|
||||
positionBlurTooltip()
|
||||
}
|
||||
|
||||
public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
|
||||
if traitCollection.horizontalSizeClass != previousTraitCollection?.horizontalSizeClass {
|
||||
updateLayoutConstraints()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Navigation Bar
|
||||
|
||||
public func navigationBarItems() -> [UIView] {
|
||||
@ -236,6 +280,37 @@ public class AttachmentPrepViewController: OWSViewController {
|
||||
}
|
||||
}
|
||||
|
||||
private func updateLayoutConstraints() {
|
||||
let isPortraitLayout = traitCollection.horizontalSizeClass == .compact
|
||||
|
||||
var constraintsToRemove: [NSLayoutConstraint]?
|
||||
var constraintsToAdd: [NSLayoutConstraint]?
|
||||
|
||||
if imageEditorView != nil {
|
||||
if isPortraitLayout {
|
||||
constraintsToRemove = imageEditorViewConstraintsLandscape
|
||||
constraintsToAdd = imageEditorViewConstraintsPortrait
|
||||
} else {
|
||||
constraintsToRemove = imageEditorViewConstraintsPortrait
|
||||
constraintsToAdd = imageEditorViewConstraintsLandscape
|
||||
}
|
||||
} else if videoEditorView != nil {
|
||||
if isPortraitLayout {
|
||||
constraintsToRemove = videoEditorViewConstraintsLandscape
|
||||
constraintsToAdd = videoEditorViewConstraintsPortrait
|
||||
} else {
|
||||
constraintsToRemove = videoEditorViewConstraintsPortrait
|
||||
constraintsToAdd = videoEditorViewConstraintsLandscape
|
||||
}
|
||||
}
|
||||
if let constraintsToRemove = constraintsToRemove {
|
||||
view.removeConstraints(constraintsToRemove)
|
||||
}
|
||||
if let constraintsToAdd = constraintsToAdd {
|
||||
view.addConstraints(constraintsToAdd)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Tooltip
|
||||
|
||||
private var shouldShowBlurTooltip: Bool {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
//
|
||||
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
|
||||
// Copyright (c) 2022 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
//
|
||||
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
|
||||
// Copyright (c) 2022 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSNavigationController.h"
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
//
|
||||
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
|
||||
// Copyright (c) 2022 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
@ -43,12 +43,56 @@ public class CircleView: UIView {
|
||||
}
|
||||
}
|
||||
|
||||
@objc (OWSCircleBlurView)
|
||||
public class CircleBlurView: UIVisualEffectView {
|
||||
|
||||
@available(*, unavailable, message: "use other constructor instead.")
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
notImplemented()
|
||||
}
|
||||
|
||||
@objc
|
||||
public override init(effect: UIVisualEffect?) {
|
||||
super.init(effect: effect)
|
||||
frame = .zero
|
||||
}
|
||||
|
||||
@objc
|
||||
public required init(effect: UIVisualEffect, diameter: CGFloat) {
|
||||
super.init(effect: effect)
|
||||
frame = .zero
|
||||
|
||||
autoSetDimensions(to: CGSize(square: diameter))
|
||||
}
|
||||
|
||||
@objc
|
||||
override public var frame: CGRect {
|
||||
didSet {
|
||||
updateRadius()
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
override public var bounds: CGRect {
|
||||
didSet {
|
||||
updateRadius()
|
||||
}
|
||||
}
|
||||
|
||||
private func updateRadius() {
|
||||
layer.cornerRadius = bounds.size.height / 2
|
||||
clipsToBounds = true
|
||||
}
|
||||
}
|
||||
|
||||
@objc (OWSPillView)
|
||||
public class PillView: UIView {
|
||||
open class PillView: UIView {
|
||||
|
||||
public override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
layer.masksToBounds = true
|
||||
|
||||
// Constrain to be a pill that is at least a circle, and maybe wider.
|
||||
autoPin(toAspectRatio: 1.0, relation: .greaterThanOrEqual)
|
||||
|
||||
@ -59,7 +103,7 @@ public class PillView: UIView {
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
|
||||