Merge branch 'igor/media-updates' into release/5.31.0

This commit is contained in:
Igor Solomennikov 2022-03-02 18:01:26 -08:00
commit 88a5bbbd66
58 changed files with 3092 additions and 1526 deletions

View File

@ -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 */,

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 524 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 995 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 493 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 860 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 399 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 750 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 425 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 808 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 243 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 398 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 573 B

View File

@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "media-composer-checkmark.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

View File

@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "media_composer_close.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

View 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

View File

@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "create-album-outline-24.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

View 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

View File

@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "create-album-solid-24.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

View 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"
}
}

View 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

View 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"
}
}

View File

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

View 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"
}
}

View 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

View 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"
}
}

View 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

View File

@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "media-composer-navbar-chevron.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

View 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

View File

@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "switch-camera-24.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

View 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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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