Only show ACI Safety Numbers
This commit is contained in:
parent
0e5424f090
commit
e179fac4e1
@ -807,7 +807,6 @@
|
||||
662C440B2A156DF7001F83E2 /* SecureValueRecovery2Impl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 662C440A2A156DF7001F83E2 /* SecureValueRecovery2Impl.swift */; };
|
||||
662C44172A1D21D7001F83E2 /* SecureValueRecovery2Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 662C44152A1D2101001F83E2 /* SecureValueRecovery2Tests.swift */; };
|
||||
663BA3182A4B8595004B9A43 /* SpoilerRenderState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 663BA3172A4B8595004B9A43 /* SpoilerRenderState.swift */; };
|
||||
663BA31C2A4C9997004B9A43 /* safety-numbers.json in Resources */ = {isa = PBXBuildFile; fileRef = 663BA31B2A4C9997004B9A43 /* safety-numbers.json */; };
|
||||
663BA3202A4CF96B004B9A43 /* MessageBodyDisplayConfigurations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 663BA31F2A4CF96B004B9A43 /* MessageBodyDisplayConfigurations.swift */; };
|
||||
663D6A7C292319BC00CABC49 /* ConversationPickerFailedRecipientsSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 663D6A7B292319BC00CABC49 /* ConversationPickerFailedRecipientsSheet.swift */; };
|
||||
6640639E294D20A900997E0B /* OutgoingCallEventSyncMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6640639D294D20A900997E0B /* OutgoingCallEventSyncMessage.swift */; };
|
||||
@ -3398,7 +3397,6 @@
|
||||
662C440A2A156DF7001F83E2 /* SecureValueRecovery2Impl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureValueRecovery2Impl.swift; sourceTree = "<group>"; };
|
||||
662C44152A1D2101001F83E2 /* SecureValueRecovery2Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureValueRecovery2Tests.swift; sourceTree = "<group>"; };
|
||||
663BA3172A4B8595004B9A43 /* SpoilerRenderState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpoilerRenderState.swift; sourceTree = "<group>"; };
|
||||
663BA31B2A4C9997004B9A43 /* safety-numbers.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "safety-numbers.json"; sourceTree = "<group>"; };
|
||||
663BA31F2A4CF96B004B9A43 /* MessageBodyDisplayConfigurations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageBodyDisplayConfigurations.swift; sourceTree = "<group>"; };
|
||||
663D6A7B292319BC00CABC49 /* ConversationPickerFailedRecipientsSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationPickerFailedRecipientsSheet.swift; sourceTree = "<group>"; };
|
||||
6640639D294D20A900997E0B /* OutgoingCallEventSyncMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutgoingCallEventSyncMessage.swift; sourceTree = "<group>"; };
|
||||
@ -7720,7 +7718,6 @@
|
||||
880C0FF6233D3F7C00386FB8 /* playPauseButton.json */,
|
||||
346EFC3825FFDC6900F493C7 /* restore-dark.json */,
|
||||
346EFC3725FFDC6900F493C7 /* restore.json */,
|
||||
663BA31B2A4C9997004B9A43 /* safety-numbers.json */,
|
||||
66586D3529005A1B00DDA9B9 /* story_viewer_onboarding_1.json */,
|
||||
66586D3429005A1B00DDA9B9 /* story_viewer_onboarding_2.json */,
|
||||
66586D3629005A1B00DDA9B9 /* story_viewer_onboarding_3.json */,
|
||||
@ -10802,7 +10799,6 @@
|
||||
346EFC3C25FFDC6A00F493C7 /* restore-dark.json in Resources */,
|
||||
346EFC3B25FFDC6A00F493C7 /* restore.json in Resources */,
|
||||
34CF0788203E6B78005C4D61 /* ringback_tone_ansi.caf in Resources */,
|
||||
663BA31C2A4C9997004B9A43 /* safety-numbers.json in Resources */,
|
||||
882F8DE6251AB23600AA4359 /* Settings.bundle in Resources */,
|
||||
4C63CC00210A620B003AE45C /* SignalTSan.supp in Resources */,
|
||||
4C6F527C20FFE8400097DEEE /* SignalUBSan.supp in Resources */,
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "safety-numbers.svg",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@ -1,42 +0,0 @@
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="29.3486" y="6" width="20" height="32" rx="4" transform="rotate(25 29.3486 6)" fill="#86A1E3" stroke="#3D62BC" stroke-width="1.75"/>
|
||||
<g clip-path="url(#clip0_8_3611)">
|
||||
<path d="M36.7111 19.4259C36.9746 19.2614 37.0549 18.9144 36.8904 18.6509C36.7259 18.3873 36.3789 18.3071 36.1153 18.4716L32.9916 20.4217L32.6913 19.1078C32.622 18.805 32.3204 18.6156 32.0176 18.6848C31.7147 18.754 31.5253 19.0557 31.5946 19.3585L32.0709 21.4426C32.1115 21.62 32.2353 21.7668 32.4033 21.8367C32.5714 21.9065 32.7628 21.8908 32.9172 21.7944L36.7111 19.4259Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M30.6389 24.3772C30.9879 25.0957 31.7929 25.4711 32.5677 25.2766L34.8402 24.706C36.1636 24.3738 37.2969 23.5214 37.9831 22.3421L39.3586 19.9783C39.7524 19.3014 39.6289 18.4426 39.0602 17.9042L36.9889 15.943C36.4292 15.4131 35.7127 15.0789 34.9469 14.9908L32.1132 14.6647C31.3353 14.5752 30.5979 15.0326 30.3326 15.7694L29.4059 18.3425C28.9436 19.6262 29.0191 21.0422 29.6152 22.2696L30.6389 24.3772ZM32.2938 24.1855C32.0355 24.2503 31.7672 24.1252 31.6508 23.8857L30.6272 21.7781C30.1635 20.8235 30.1048 19.7221 30.4644 18.7236L31.3911 16.1505C31.4795 15.905 31.7253 15.7525 31.9846 15.7823L34.8183 16.1084C35.3422 16.1687 35.8325 16.3973 36.2155 16.76L38.2868 18.7211C38.4763 18.9005 38.5175 19.1868 38.3862 19.4124L37.0107 21.7763C36.477 22.6935 35.5956 23.3565 34.5663 23.6149L32.2938 24.1855Z" fill="white"/>
|
||||
<path d="M36.7111 19.4259C36.9746 19.2614 37.0549 18.9144 36.8904 18.6509C36.7259 18.3873 36.3789 18.3071 36.1153 18.4716L32.9916 20.4217L32.6913 19.1078C32.622 18.805 32.3204 18.6156 32.0176 18.6848C31.7147 18.754 31.5253 19.0557 31.5946 19.3585L32.0709 21.4426C32.1115 21.62 32.2353 21.7668 32.4033 21.8367C32.5714 21.9065 32.7628 21.8908 32.9172 21.7944L36.7111 19.4259Z" stroke="white" stroke-width="0.25"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M30.6389 24.3772C30.9879 25.0957 31.7929 25.4711 32.5677 25.2766L34.8402 24.706C36.1636 24.3738 37.2969 23.5214 37.9831 22.3421L39.3586 19.9783C39.7524 19.3014 39.6289 18.4426 39.0602 17.9042L36.9889 15.943C36.4292 15.4131 35.7127 15.0789 34.9469 14.9908L32.1132 14.6647C31.3353 14.5752 30.5979 15.0326 30.3326 15.7694L29.4059 18.3425C28.9436 19.6262 29.0191 21.0422 29.6152 22.2696L30.6389 24.3772ZM32.2938 24.1855C32.0355 24.2503 31.7672 24.1252 31.6508 23.8857L30.6272 21.7781C30.1635 20.8235 30.1048 19.7221 30.4644 18.7236L31.3911 16.1505C31.4795 15.905 31.7253 15.7525 31.9846 15.7823L34.8183 16.1084C35.3422 16.1687 35.8325 16.3973 36.2155 16.76L38.2868 18.7211C38.4763 18.9005 38.5175 19.1868 38.3862 19.4124L37.0107 21.7763C36.477 22.6935 35.5956 23.3565 34.5663 23.6149L32.2938 24.1855Z" stroke="white" stroke-width="0.25"/>
|
||||
</g>
|
||||
<path d="M28.6304 28.8359L30.443 29.6812" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M24.0991 26.7229L25.9117 27.5681" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M33.1621 30.9492L34.9747 31.7945" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M27.3628 31.5549L29.1754 32.4002" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M22.8311 29.4419L24.6437 30.2871" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M31.894 33.668L33.7067 34.5132" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M26.0947 34.2739L27.9073 35.1192" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M21.563 32.1609L23.3756 33.0061" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M30.6265 36.387L32.4391 37.2322" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<rect y="14.4524" width="20" height="32" rx="4" transform="rotate(-25 0 14.4524)" fill="#EBEAE8" stroke="#837D72" stroke-width="1.75"/>
|
||||
<g clip-path="url(#clip1_8_3611)">
|
||||
<path d="M15.0172 17.4425C15.0605 17.1348 14.8463 16.8503 14.5387 16.807C14.2311 16.7636 13.9465 16.9778 13.9032 17.2854L13.3892 20.9319L12.1896 20.3174C11.9131 20.1758 11.5742 20.2851 11.4325 20.5616C11.2909 20.8381 11.4002 21.177 11.6767 21.3187L13.5794 22.2934C13.7414 22.3763 13.9334 22.3758 14.095 22.292C14.2565 22.2082 14.3675 22.0514 14.3929 21.8712L15.0172 17.4425Z" fill="#837D72"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.9069 25.2767C15.6817 25.4712 16.4868 25.0958 16.8358 24.3772L17.8594 22.2696C18.4556 21.0423 18.531 19.6263 18.0687 18.3425L17.1421 15.7694C16.8767 15.0327 16.1394 14.5752 15.3615 14.6648L12.5277 14.9909C11.762 15.079 11.0455 15.4131 10.4857 15.9431L8.41443 17.9042C7.84579 18.4426 7.72225 19.3015 8.11609 19.9783L9.49156 22.3421C10.1778 23.5215 11.311 24.3739 12.6344 24.7061L14.9069 25.2767ZM15.8238 23.8857C15.7075 24.1252 15.4391 24.2504 15.1809 24.1855L12.9084 23.615C11.8791 23.3566 10.9977 22.6936 10.4639 21.7763L9.08846 19.4125C8.95717 19.1869 8.99835 18.9006 9.1879 18.7211L11.2592 16.76C11.6422 16.3974 12.1324 16.1688 12.6564 16.1085L15.4901 15.7824C15.7494 15.7525 15.9952 15.905 16.0836 16.1506L17.0103 18.7237C17.3698 19.7222 17.3111 20.8235 16.8475 21.7781L15.8238 23.8857Z" fill="#837D72"/>
|
||||
<path d="M15.0172 17.4425C15.0605 17.1348 14.8463 16.8503 14.5387 16.807C14.2311 16.7636 13.9465 16.9778 13.9032 17.2854L13.3892 20.9319L12.1896 20.3174C11.9131 20.1758 11.5742 20.2851 11.4325 20.5616C11.2909 20.8381 11.4002 21.177 11.6767 21.3187L13.5794 22.2934C13.7414 22.3763 13.9334 22.3758 14.095 22.292C14.2565 22.2082 14.3675 22.0514 14.3929 21.8712L15.0172 17.4425Z" stroke="#837D72" stroke-width="0.25"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.9069 25.2767C15.6817 25.4712 16.4868 25.0958 16.8358 24.3772L17.8594 22.2696C18.4556 21.0423 18.531 19.6263 18.0687 18.3425L17.1421 15.7694C16.8767 15.0327 16.1394 14.5752 15.3615 14.6648L12.5277 14.9909C11.762 15.079 11.0455 15.4131 10.4857 15.9431L8.41443 17.9042C7.84579 18.4426 7.72225 19.3015 8.11609 19.9783L9.49156 22.3421C10.1778 23.5215 11.311 24.3739 12.6344 24.7061L14.9069 25.2767ZM15.8238 23.8857C15.7075 24.1252 15.4391 24.2504 15.1809 24.1855L12.9084 23.615C11.8791 23.3566 10.9977 22.6936 10.4639 21.7763L9.08846 19.4125C8.95717 19.1869 8.99835 18.9006 9.1879 18.7211L11.2592 16.76C11.6422 16.3974 12.1324 16.1688 12.6564 16.1085L15.4901 15.7824C15.7494 15.7525 15.9952 15.905 16.0836 16.1506L17.0103 18.7237C17.3698 19.7222 17.3111 20.8235 16.8475 21.7781L15.8238 23.8857Z" stroke="#837D72" stroke-width="0.25"/>
|
||||
</g>
|
||||
<path d="M17.0317 29.6814L18.8444 28.8362" stroke="#837D72" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M12.5 31.7944L14.3126 30.9492" stroke="#837D72" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M21.5635 27.5681L23.3761 26.7229" stroke="#837D72" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M18.2998 32.4001L20.1124 31.5549" stroke="#837D72" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M13.7681 34.5134L15.5807 33.6682" stroke="#837D72" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M22.8311 30.2871L24.6437 29.4419" stroke="#837D72" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M19.5674 35.1191L21.38 34.2739" stroke="#837D72" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M15.0361 37.2322L16.8487 36.3869" stroke="#837D72" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M24.0991 33.0061L25.9117 32.1609" stroke="#837D72" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<defs>
|
||||
<clipPath id="clip0_8_3611">
|
||||
<rect width="12" height="12" fill="white" transform="translate(30.8608 12.2219) rotate(25)"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip1_8_3611">
|
||||
<rect width="12" height="12" fill="white" transform="translate(5.73828 17.2935) rotate(-25)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 7.5 KiB |
File diff suppressed because one or more lines are too long
@ -488,7 +488,7 @@ extension ConversationViewController: CVComponentDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
FingerprintViewController.present(from: self, address: address)
|
||||
FingerprintViewController.present(for: address.aci, from: self)
|
||||
}
|
||||
|
||||
public func didTapUnverifiedIdentityChange(_ address: SignalServiceAddress) {
|
||||
|
||||
@ -405,8 +405,7 @@ class ConversationSettingsViewController: OWSTableViewController2, BadgeCollecti
|
||||
return
|
||||
}
|
||||
let contactAddress = contactThread.contactAddress
|
||||
assert(contactAddress.isValid)
|
||||
FingerprintViewController.present(from: self, address: contactAddress)
|
||||
FingerprintViewController.present(for: contactAddress.aci, from: self)
|
||||
}
|
||||
|
||||
func showColorAndWallpaperSettingsView() {
|
||||
|
||||
@ -241,7 +241,7 @@ class MemberActionSheet: OWSTableSheetViewController {
|
||||
actionBlock: { [weak self] in
|
||||
guard let self = self, let fromViewController = self.fromViewController else { return }
|
||||
self.dismiss(animated: true) {
|
||||
FingerprintViewController.present(from: fromViewController, address: self.address)
|
||||
FingerprintViewController.present(for: self.address.aci, from: fromViewController)
|
||||
}
|
||||
}
|
||||
))
|
||||
|
||||
@ -850,12 +850,6 @@
|
||||
/* VoiceOver description of current camera zoom level. */
|
||||
"CAMERA_VO_ZOOM_LEVEL" = "%@times";
|
||||
|
||||
/* Message for alert explaining that a user cannot be verified. */
|
||||
"CANT_VERIFY_IDENTITY_ALERT_MESSAGE" = "This user can't be verified until you've exchanged messages with them.";
|
||||
|
||||
/* Title for alert explaining that a user cannot be verified. */
|
||||
"CANT_VERIFY_IDENTITY_ALERT_TITLE" = "Error";
|
||||
|
||||
/* Alert shown when the user needs to exchange messages to see the safety number. */
|
||||
"CANT_VERIFY_IDENTITY_EXCHANGE_MESSAGES" = "A safety number will be created with this person after you exchange messages with them.";
|
||||
|
||||
@ -5572,21 +5566,6 @@
|
||||
/* Snippet to share {{safety number}} with a friend. sent e.g. via SMS */
|
||||
"SAFETY_NUMBER_SHARE_FORMAT" = "Our Signal Safety Number:\n%@";
|
||||
|
||||
/* Header informing the user about the transition from phone number to user identifier based. */
|
||||
"SAFETY_NUMBER_TRANSITION_HEADER_ALERT" = "Safety numbers are being updated.";
|
||||
|
||||
/* Button text for a sheet informing the user about the transition from phone number to user identifier based. */
|
||||
"SAFETY_NUMBER_TRANSITION_SHEET_HELP_TEXT" = "Need help?";
|
||||
|
||||
/* Informs the user about the transition from phone number to user identifier based. */
|
||||
"SAFETY_NUMBER_TRANSITION_SHEET_PARAGRAPH_1" = "Safety numbers are being updated over a transition period to enable upcoming privacy features in Signal.";
|
||||
|
||||
/* Informs the user about the transition from phone number to user identifier based. */
|
||||
"SAFETY_NUMBER_TRANSITION_SHEET_PARAGRAPH_2" = "To verify safety numbers, match the color card with your contact’s device. If these don’t match, swipe and try the other pair of safety numbers. Only one pair needs to match.";
|
||||
|
||||
/* Title for a sheet informing the user about the transition from phone number to user identifier based. */
|
||||
"SAFETY_NUMBER_TRANSITION_SHEET_TITLE" = "Changes to Safety Numbers";
|
||||
|
||||
/* Action sheet heading */
|
||||
"SAFETY_NUMBERS_ACTIONSHEET_TITLE" = "Your safety number with %@ has changed. You may wish to verify it.";
|
||||
|
||||
@ -7589,7 +7568,7 @@
|
||||
"VERIFY_PRIVACY_MULTIPLE" = "Review Safety Numbers";
|
||||
|
||||
/* Instructions for verifying your safety number. Embeds {{contact's name}} */
|
||||
"VERIFY_SAFETY_NUMBER_INSTRUCTIONS" = "To verify end-to-end encryption with %@, match the color card above with their device and compare the numbers. If these don’t match, swipe and try the other pair of safety numbers. Only one pair needs to match.";
|
||||
"VERIFY_SAFETY_NUMBER_INSTRUCTIONS" = "To verify end-to-end encryption with %1$@, compare the numbers above with their device. You can also scan the code on their device.";
|
||||
|
||||
/* Toast alert text shown when tapping on a video that cannot be played. */
|
||||
"VIDEO_BROKEN" = "This video can’t be played";
|
||||
|
||||
@ -7,20 +7,17 @@ import CommonCrypto
|
||||
import LibSignalClient
|
||||
|
||||
public class OWSFingerprint {
|
||||
public let myAci: Aci
|
||||
public let theirAci: Aci
|
||||
|
||||
public enum Source {
|
||||
case aci(myAci: Aci, theirAci: Aci)
|
||||
case e164(myE164: E164, theirE164: E164)
|
||||
}
|
||||
|
||||
public let source: Source
|
||||
public let myAciIdentityKey: Data
|
||||
public let theirAciIdentityKey: Data
|
||||
|
||||
private let hashIterations: UInt32
|
||||
private let myFingerprintData: Data
|
||||
private let theirFingerprintData: Data
|
||||
private let theirName: String
|
||||
|
||||
private let hashIterations: UInt32
|
||||
public let theirName: String
|
||||
|
||||
/**
|
||||
* Formats numeric fingerprint, 3 lines in groups of 5 digits.
|
||||
@ -34,13 +31,15 @@ public class OWSFingerprint {
|
||||
}
|
||||
|
||||
public init(
|
||||
source: Source,
|
||||
myAci: Aci,
|
||||
theirAci: Aci,
|
||||
myAciIdentityKey: Data,
|
||||
theirAciIdentityKey: Data,
|
||||
theirName: String,
|
||||
hashIterations: UInt32 = Constants.defaultHashIterations
|
||||
) {
|
||||
self.source = source
|
||||
self.myAci = myAci
|
||||
self.theirAci = theirAci
|
||||
let myAciIdentityKey = myAciIdentityKey.prependKeyType()
|
||||
self.myAciIdentityKey = myAciIdentityKey
|
||||
let theirAciIdentityKey = theirAciIdentityKey.prependKeyType()
|
||||
@ -48,7 +47,7 @@ public class OWSFingerprint {
|
||||
self.hashIterations = hashIterations
|
||||
self.theirName = theirName
|
||||
|
||||
let (myStableSourceData, theirStableSourceData) = Self.stableData(for: source)
|
||||
let (myStableSourceData, theirStableSourceData) = Self.stableData(myAci: myAci, theirAci: theirAci)
|
||||
self.myFingerprintData = Self.dataForStableAddress(
|
||||
myStableSourceData,
|
||||
publicKey: myAciIdentityKey,
|
||||
@ -208,19 +207,8 @@ public class OWSFingerprint {
|
||||
|
||||
// MARK: - Private helpers
|
||||
|
||||
private static func stableData(for source: Source) -> (my: Data, their: Data) {
|
||||
switch source {
|
||||
case let .aci(myAci, theirAci):
|
||||
return (my: myAci.rawUUID.data, their: theirAci.rawUUID.data)
|
||||
case let .e164(myE164, theirE164):
|
||||
guard
|
||||
let myData = myE164.stringValue.data(using: .utf8),
|
||||
let theirData = theirE164.stringValue.data(using: .utf8)
|
||||
else {
|
||||
owsFail("Unable to serialize e164")
|
||||
}
|
||||
return (my: myData, their: theirData)
|
||||
}
|
||||
private static func stableData(myAci: Aci, theirAci: Aci) -> (my: Data, their: Data) {
|
||||
return (my: myAci.rawUUID.data, their: theirAci.rawUUID.data)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -296,17 +284,11 @@ public class OWSFingerprint {
|
||||
}
|
||||
|
||||
private var scannableFingerprintVersion: UInt32 {
|
||||
switch source {
|
||||
case .e164:
|
||||
return Constants.e164ScannableFormatVersion
|
||||
case .aci:
|
||||
return Constants.aciScannableFormatVersion
|
||||
}
|
||||
return Constants.aciScannableFormatVersion
|
||||
}
|
||||
|
||||
public enum Constants {
|
||||
static let hashingVersion: UInt32 = 0
|
||||
static let e164ScannableFormatVersion: UInt32 = 1
|
||||
static let aciScannableFormatVersion: UInt32 = 2
|
||||
public static let defaultHashIterations: UInt32 = 5200
|
||||
}
|
||||
|
||||
@ -8,8 +8,8 @@ import LibSignalClient
|
||||
public class OWSFingerprintBuilder {
|
||||
public struct FingerprintResult {
|
||||
public let theirAci: Aci
|
||||
public let fingerprints: [OWSFingerprint]
|
||||
public let initialDisplayIndex: Int
|
||||
public let theirRecipientIdentity: OWSRecipientIdentity
|
||||
public let fingerprint: OWSFingerprint
|
||||
}
|
||||
|
||||
private let contactsManager: ContactsManagerProtocol
|
||||
@ -30,13 +30,12 @@ public class OWSFingerprintBuilder {
|
||||
/// identity key. You can use these to present a new identity key for
|
||||
/// verification.
|
||||
public func fingerprints(
|
||||
theirAddress: SignalServiceAddress,
|
||||
theirAci: Aci,
|
||||
theirRecipientIdentity: OWSRecipientIdentity,
|
||||
tx: SDSAnyReadTransaction
|
||||
) -> FingerprintResult? {
|
||||
guard
|
||||
let localIdentifiers = tsAccountManager.localIdentifiers(tx: tx.asV2Read),
|
||||
let myE164 = E164(localIdentifiers.phoneNumber),
|
||||
let myAciIdentityKey = identityManager.identityKeyPair(for: .aci, tx: tx.asV2Read)?.publicKey
|
||||
else {
|
||||
owsFailDebug("Missing local properties!")
|
||||
@ -44,54 +43,21 @@ public class OWSFingerprintBuilder {
|
||||
}
|
||||
let myAci = localIdentifiers.aci
|
||||
|
||||
guard let theirAci = theirAddress.aci else {
|
||||
Logger.warn("Missing their ACI!")
|
||||
return nil
|
||||
}
|
||||
let theirAciIdentityKey = theirRecipientIdentity.identityKey
|
||||
let theirE164 = theirAddress.e164
|
||||
let theirName = contactsManager.displayName(
|
||||
for: SignalServiceAddress(serviceId: theirAci, e164: theirE164),
|
||||
transaction: tx
|
||||
)
|
||||
let theirName = contactsManager.displayName(for: SignalServiceAddress(theirAci), transaction: tx)
|
||||
|
||||
let aciFingerprint = OWSFingerprint(
|
||||
source: .aci(myAci: myAci, theirAci: theirAci),
|
||||
myAci: myAci,
|
||||
theirAci: theirAci,
|
||||
myAciIdentityKey: myAciIdentityKey,
|
||||
theirAciIdentityKey: theirAciIdentityKey,
|
||||
theirName: theirName
|
||||
)
|
||||
|
||||
let e164Fingerprint: OWSFingerprint? = theirE164.map { theirE164 in
|
||||
return OWSFingerprint(
|
||||
source: .e164(myE164: myE164, theirE164: theirE164),
|
||||
myAciIdentityKey: myAciIdentityKey,
|
||||
theirAciIdentityKey: theirAciIdentityKey,
|
||||
theirName: theirName
|
||||
)
|
||||
}
|
||||
|
||||
if FeatureFlags.onlyAciSafetyNumbers {
|
||||
return FingerprintResult(
|
||||
theirAci: theirAci,
|
||||
fingerprints: [aciFingerprint],
|
||||
initialDisplayIndex: 0
|
||||
)
|
||||
} else if let e164Fingerprint {
|
||||
// We have both, but prefer the ACI.
|
||||
return FingerprintResult(
|
||||
theirAci: theirAci,
|
||||
fingerprints: [aciFingerprint, e164Fingerprint],
|
||||
initialDisplayIndex: 0
|
||||
)
|
||||
} else {
|
||||
// If we default to ACI safety number and don't have the e164,
|
||||
// that's fine. Just show the ACI one.
|
||||
return FingerprintResult(
|
||||
theirAci: theirAci,
|
||||
fingerprints: [aciFingerprint],
|
||||
initialDisplayIndex: 0
|
||||
)
|
||||
}
|
||||
return FingerprintResult(
|
||||
theirAci: theirAci,
|
||||
theirRecipientIdentity: theirRecipientIdentity,
|
||||
fingerprint: aciFingerprint
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -114,10 +114,6 @@ public class FeatureFlags: BaseFlags {
|
||||
}
|
||||
}
|
||||
|
||||
/// If true, _only_ aci safety numbers will be displayed, and e164 safety numbers will not
|
||||
/// be displayed.
|
||||
public static let onlyAciSafetyNumbers = false
|
||||
|
||||
public static let editMessageSend = true
|
||||
|
||||
public static let doNotSendGroupChangeMessagesOnProfileKeyRotation = false
|
||||
|
||||
@ -221,10 +221,6 @@ public class RemoteConfig: BaseFlags {
|
||||
return isEnabled(.enableAutoAPNSRotation, defaultValue: false)
|
||||
}
|
||||
|
||||
public static var defaultToAciSafetyNumber: Bool {
|
||||
return isEnabled(.safetyNumberAci)
|
||||
}
|
||||
|
||||
/// The minimum length for a valid nickname, in Unicode codepoints.
|
||||
public static var minNicknameLength: UInt32 {
|
||||
getUInt32Value(forFlag: .minNicknameLength, defaultValue: 3)
|
||||
@ -582,9 +578,16 @@ private struct Flags {
|
||||
// Even if we don't fetch a fresh remote config, we may cross the time threshold
|
||||
// while the app is in memory, updating the value from false to true.
|
||||
// As such we also take fresh remote config values and swap them in at runtime.
|
||||
#if false
|
||||
// If there are time-gated flags, use this definition.
|
||||
enum SupportedTimeGatedFlags: String, FlagType {
|
||||
case safetyNumberAci
|
||||
}
|
||||
#else
|
||||
// If there aren't any time-gated flags, use this definition.
|
||||
enum SupportedTimeGatedFlags: FlagType {
|
||||
var rawValue: String { fatalError() }
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
@ -610,7 +613,6 @@ private extension FlagType {
|
||||
case "maxGroupCallRingSize": return "global.calling.maxGroupCallRingSize"
|
||||
case "minNicknameLength": return "global.nicknames.min"
|
||||
case "maxNicknameLength": return "global.nicknames.max"
|
||||
case "safetyNumberAci": return "global.safetyNumberAci"
|
||||
case "cdsDisableCompatibilityMode": return "cds.disableCompatibilityMode"
|
||||
case "maxAttachmentDownloadSizeBytes": return "global.attachments.maxBytes"
|
||||
default: return Flags.prefix + rawValue
|
||||
|
||||
@ -10,14 +10,15 @@ import XCTest
|
||||
final class OWSFingerprintTest: XCTestCase {
|
||||
|
||||
func testDisplayableTextInsertsSpaces() {
|
||||
let aliceE164 = E164("+19995550101")!
|
||||
let bobE164 = E164("+18885550102")!
|
||||
let aliceAci = Aci.constantForTesting("00000000-0000-4000-8000-0000000000a1")
|
||||
let bobAci = Aci.constantForTesting("00000000-0000-4000-8000-0000000000b1")
|
||||
|
||||
let aliceIdentityKey = ECKeyPair.generateKeyPair().publicKey
|
||||
let bobIdentityKey = ECKeyPair.generateKeyPair().publicKey
|
||||
|
||||
let aliceToBobFingerprint = OWSFingerprint(
|
||||
source: .e164(myE164: aliceE164, theirE164: bobE164),
|
||||
myAci: aliceAci,
|
||||
theirAci: bobAci,
|
||||
myAciIdentityKey: aliceIdentityKey,
|
||||
theirAciIdentityKey: bobIdentityKey,
|
||||
theirName: "Bob",
|
||||
@ -41,70 +42,46 @@ final class OWSFingerprintTest: XCTestCase {
|
||||
}
|
||||
|
||||
func testTextMatchesReciprocally() {
|
||||
let sourceSets: [(
|
||||
aliceToBob: OWSFingerprint.Source,
|
||||
bobToAlice: OWSFingerprint.Source,
|
||||
charlieToAlice: OWSFingerprint.Source
|
||||
)] = [
|
||||
{
|
||||
let aliceE164 = E164("+19995550101")!
|
||||
let bobE164 = E164("+18885550102")!
|
||||
let charlieE164 = E164("+17775550103")!
|
||||
|
||||
return (
|
||||
.e164(myE164: aliceE164, theirE164: bobE164),
|
||||
.e164(myE164: bobE164, theirE164: aliceE164),
|
||||
.e164(myE164: charlieE164, theirE164: aliceE164)
|
||||
)
|
||||
}(),
|
||||
{
|
||||
let aliceAci = Aci.randomForTesting()
|
||||
let bobAci = Aci.randomForTesting()
|
||||
let charlieAci = Aci.randomForTesting()
|
||||
|
||||
return (
|
||||
.aci(myAci: aliceAci, theirAci: bobAci),
|
||||
.aci(myAci: bobAci, theirAci: aliceAci),
|
||||
.aci(myAci: charlieAci, theirAci: aliceAci)
|
||||
)
|
||||
}()
|
||||
]
|
||||
let aliceAci = Aci.randomForTesting()
|
||||
let bobAci = Aci.randomForTesting()
|
||||
let charlieAci = Aci.randomForTesting()
|
||||
|
||||
let aliceIdentityKey = ECKeyPair.generateKeyPair().publicKey
|
||||
let bobIdentityKey = ECKeyPair.generateKeyPair().publicKey
|
||||
let charlieIdentityKey = ECKeyPair.generateKeyPair().publicKey
|
||||
|
||||
for (aliceToBob, bobToAlice, charlieToAlice) in sourceSets {
|
||||
let aliceToBobFingerprint = OWSFingerprint(
|
||||
source: aliceToBob,
|
||||
myAciIdentityKey: aliceIdentityKey,
|
||||
theirAciIdentityKey: bobIdentityKey,
|
||||
theirName: "Bob",
|
||||
hashIterations: 2
|
||||
)
|
||||
let bobToAliceFingerprint = OWSFingerprint(
|
||||
source: bobToAlice,
|
||||
myAciIdentityKey: bobIdentityKey,
|
||||
theirAciIdentityKey: aliceIdentityKey,
|
||||
theirName: "Alice",
|
||||
hashIterations: 2
|
||||
)
|
||||
XCTAssertEqual(
|
||||
aliceToBobFingerprint.displayableText,
|
||||
bobToAliceFingerprint.displayableText
|
||||
)
|
||||
let aliceToBobFingerprint = OWSFingerprint(
|
||||
myAci: aliceAci,
|
||||
theirAci: bobAci,
|
||||
myAciIdentityKey: aliceIdentityKey,
|
||||
theirAciIdentityKey: bobIdentityKey,
|
||||
theirName: "Bob",
|
||||
hashIterations: 2
|
||||
)
|
||||
let bobToAliceFingerprint = OWSFingerprint(
|
||||
myAci: bobAci,
|
||||
theirAci: aliceAci,
|
||||
myAciIdentityKey: bobIdentityKey,
|
||||
theirAciIdentityKey: aliceIdentityKey,
|
||||
theirName: "Alice",
|
||||
hashIterations: 2
|
||||
)
|
||||
XCTAssertEqual(
|
||||
aliceToBobFingerprint.displayableText,
|
||||
bobToAliceFingerprint.displayableText
|
||||
)
|
||||
|
||||
let charlieToAliceFingerprint = OWSFingerprint(
|
||||
source: charlieToAlice,
|
||||
myAciIdentityKey: charlieIdentityKey,
|
||||
theirAciIdentityKey: aliceIdentityKey,
|
||||
theirName: "Alice",
|
||||
hashIterations: 2
|
||||
)
|
||||
XCTAssertNotEqual(
|
||||
aliceToBobFingerprint.displayableText,
|
||||
charlieToAliceFingerprint.displayableText
|
||||
)
|
||||
}
|
||||
let charlieToAliceFingerprint = OWSFingerprint(
|
||||
myAci: charlieAci,
|
||||
theirAci: aliceAci,
|
||||
myAciIdentityKey: charlieIdentityKey,
|
||||
theirAciIdentityKey: aliceIdentityKey,
|
||||
theirName: "Alice",
|
||||
hashIterations: 2
|
||||
)
|
||||
XCTAssertNotEqual(
|
||||
aliceToBobFingerprint.displayableText,
|
||||
charlieToAliceFingerprint.displayableText
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,20 +14,20 @@ class FingerprintScanViewController: OWSViewController, OWSNavigationChildContro
|
||||
private let recipientIdentity: OWSRecipientIdentity
|
||||
private let contactName: String
|
||||
private let identityKey: Data
|
||||
private let fingerprints: [OWSFingerprint]
|
||||
private let fingerprint: OWSFingerprint
|
||||
|
||||
private lazy var qrCodeScanViewController = QRCodeScanViewController(appearance: .masked())
|
||||
|
||||
init(
|
||||
recipientAci: Aci,
|
||||
recipientIdentity: OWSRecipientIdentity,
|
||||
fingerprints: [OWSFingerprint]
|
||||
fingerprint: OWSFingerprint
|
||||
) {
|
||||
self.recipientAci = recipientAci
|
||||
self.recipientIdentity = recipientIdentity
|
||||
self.identityKey = recipientIdentity.identityKey
|
||||
|
||||
self.fingerprints = fingerprints
|
||||
self.fingerprint = fingerprint
|
||||
self.contactName = Self.contactsManager.displayName(for: SignalServiceAddress(recipientAci))
|
||||
|
||||
super.init()
|
||||
@ -117,26 +117,12 @@ class FingerprintScanViewController: OWSViewController, OWSNavigationChildContro
|
||||
)
|
||||
}
|
||||
|
||||
// Check all of them, if any succeed its success.
|
||||
var localizedErrorDescriptionToShow: String?
|
||||
for (i, fingerprint) in fingerprints.enumerated() {
|
||||
switch fingerprint.matchesLogicalFingerprintsData(combinedFingerprintData) {
|
||||
case .match:
|
||||
showSuccess()
|
||||
return
|
||||
case .noMatch(let localizedErrorDescription):
|
||||
// Prefer no match errors to version erorrs, if we end
|
||||
// up displaying an error.
|
||||
localizedErrorDescriptionToShow = localizedErrorDescription
|
||||
fallthrough
|
||||
case
|
||||
.weHaveOldVersion(let localizedErrorDescription),
|
||||
.theyHaveOldVersion(let localizedErrorDescription):
|
||||
if i == fingerprints.count - 1 {
|
||||
// We reached the end, show the error for the last one.
|
||||
showFailure(localizedErrorDescription: localizedErrorDescriptionToShow ?? localizedErrorDescription)
|
||||
}
|
||||
}
|
||||
switch fingerprint.matchesLogicalFingerprintsData(combinedFingerprintData) {
|
||||
case .match:
|
||||
showSuccess()
|
||||
case .noMatch(let localizedErrorDescription), .weHaveOldVersion(let localizedErrorDescription), .theyHaveOldVersion(let localizedErrorDescription):
|
||||
// We reached the end, show the error for the last one.
|
||||
showFailure(localizedErrorDescription: localizedErrorDescription)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -15,39 +15,30 @@ import UIKit
|
||||
public class FingerprintViewController: OWSViewController, OWSNavigationChildController {
|
||||
|
||||
public class func present(
|
||||
from viewController: UIViewController,
|
||||
address theirAddress: SignalServiceAddress
|
||||
for theirAci: Aci?,
|
||||
from viewController: UIViewController
|
||||
) {
|
||||
owsAssertBeta(theirAddress.isValid)
|
||||
|
||||
let identityManager = DependenciesBridge.shared.identityManager
|
||||
guard let theirRecipientIdentity = databaseStorage.read(block: { tx in
|
||||
identityManager.recipientIdentity(for: theirAddress, tx: tx.asV2Read)
|
||||
}) else {
|
||||
OWSActionSheets.showActionSheet(
|
||||
title: OWSLocalizedString(
|
||||
"CANT_VERIFY_IDENTITY_ALERT_TITLE",
|
||||
comment: "Title for alert explaining that a user cannot be verified."
|
||||
),
|
||||
message: OWSLocalizedString(
|
||||
"CANT_VERIFY_IDENTITY_ALERT_MESSAGE",
|
||||
comment: "Message for alert explaining that a user cannot be verified."
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
guard let fingerprintResult = databaseStorage.read(block: { tx in
|
||||
let fingerprintResult = databaseStorage.read { (tx) -> OWSFingerprintBuilder.FingerprintResult? in
|
||||
guard let theirAci else {
|
||||
return nil
|
||||
}
|
||||
let identityManager = DependenciesBridge.shared.identityManager
|
||||
let theirAddress = SignalServiceAddress(theirAci)
|
||||
guard let theirRecipientIdentity = identityManager.recipientIdentity(for: theirAddress, tx: tx.asV2Read) else {
|
||||
return nil
|
||||
}
|
||||
return OWSFingerprintBuilder(
|
||||
contactsManager: contactsManager,
|
||||
identityManager: identityManager,
|
||||
tsAccountManager: DependenciesBridge.shared.tsAccountManager
|
||||
).fingerprints(
|
||||
theirAddress: theirAddress,
|
||||
theirAci: theirAci,
|
||||
theirRecipientIdentity: theirRecipientIdentity,
|
||||
tx: tx
|
||||
)
|
||||
}) else {
|
||||
}
|
||||
|
||||
guard let fingerprintResult else {
|
||||
let actionSheet = ActionSheetController(message: OWSLocalizedString(
|
||||
"CANT_VERIFY_IDENTITY_EXCHANGE_MESSAGES",
|
||||
comment: "Alert shown when the user needs to exchange messages to see the safety number."
|
||||
@ -66,10 +57,9 @@ public class FingerprintViewController: OWSViewController, OWSNavigationChildCon
|
||||
}
|
||||
|
||||
let fingerprintViewController = FingerprintViewController(
|
||||
fingerprints: fingerprintResult.fingerprints,
|
||||
initialDisplayIndex: fingerprintResult.initialDisplayIndex,
|
||||
fingerprint: fingerprintResult.fingerprint,
|
||||
recipientAci: fingerprintResult.theirAci,
|
||||
recipientIdentity: theirRecipientIdentity
|
||||
recipientIdentity: fingerprintResult.theirRecipientIdentity
|
||||
)
|
||||
let navigationController = OWSNavigationController(rootViewController: fingerprintViewController)
|
||||
viewController.present(navigationController, animated: true)
|
||||
@ -89,26 +79,21 @@ public class FingerprintViewController: OWSViewController, OWSNavigationChildCon
|
||||
|
||||
private let recipientAci: Aci
|
||||
private let recipientIdentity: OWSRecipientIdentity
|
||||
private let contactName: String
|
||||
private let identityKey: Data
|
||||
private let fingerprints: [OWSFingerprint]
|
||||
private let fingerprint: OWSFingerprint
|
||||
private var isVerified = false
|
||||
private var selectedIndex: Int
|
||||
|
||||
public init(
|
||||
fingerprints: [OWSFingerprint],
|
||||
initialDisplayIndex: Int,
|
||||
fingerprint: OWSFingerprint,
|
||||
recipientAci: Aci,
|
||||
recipientIdentity: OWSRecipientIdentity
|
||||
) {
|
||||
self.recipientAci = recipientAci
|
||||
self.contactName = Self.contactsManager.displayName(for: SignalServiceAddress(recipientAci))
|
||||
// By capturing the identity key when we enter these views, we prevent the edge case
|
||||
// where the user verifies a key that we learned about while this view was open.
|
||||
self.recipientIdentity = recipientIdentity
|
||||
self.identityKey = recipientIdentity.identityKey
|
||||
self.fingerprints = fingerprints
|
||||
self.selectedIndex = initialDisplayIndex
|
||||
self.fingerprint = fingerprint
|
||||
|
||||
super.init()
|
||||
|
||||
@ -145,125 +130,18 @@ public class FingerprintViewController: OWSViewController, OWSNavigationChildCon
|
||||
configureUI()
|
||||
}
|
||||
|
||||
public override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
// TODO: This doesn't seem to work, maybe because the views haven't been sized yet. When we build with Xcode 15, we can use `viewIsAppearing()`.
|
||||
if #available(iOS 17, *) { owsFailDebug("Canary to fix this when we're building with Xcode 15!") }
|
||||
fingerprintCarouselPageControl.currentPage = selectedIndex
|
||||
scrollToSelectedIndex(animated: false)
|
||||
}
|
||||
|
||||
public override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
if !DependenciesBridge.shared.db.read(block: self.hasShownTransitionSheet) {
|
||||
// Its fine to not re-read the value in the write tx; stakes are low.
|
||||
DependenciesBridge.shared.db.write(block: self.showTransitionSheet)
|
||||
}
|
||||
}
|
||||
|
||||
public override func themeDidChange() {
|
||||
super.themeDidChange()
|
||||
view.backgroundColor = Self.backgroundColor
|
||||
|
||||
updateVerificationStateLabel()
|
||||
setSafetyNumbersUpdateTextViewText()
|
||||
setCarouselPageControlColors()
|
||||
setInstructionsText()
|
||||
setVerifyUnverifyButtonColors()
|
||||
}
|
||||
|
||||
// MARK: UI
|
||||
|
||||
private lazy var safetyNumbersUpdateTextView: LinkingTextView = {
|
||||
let textView = LinkingTextView()
|
||||
textView.delegate = self
|
||||
return textView
|
||||
}()
|
||||
|
||||
private func setSafetyNumbersUpdateTextViewText() {
|
||||
// Link doesn't matter, we will override tap behavior.
|
||||
let learnMoreString = CommonStrings.learnMore.styled(with: .link(URL(string: Constants.transitionLearnMoreUrl)!))
|
||||
safetyNumbersUpdateTextView.attributedText = NSAttributedString.composed(of: [
|
||||
OWSLocalizedString(
|
||||
"SAFETY_NUMBER_TRANSITION_HEADER_ALERT",
|
||||
comment: "Header informing the user about the transition from phone number to user identifier based."
|
||||
),
|
||||
"\n",
|
||||
learnMoreString
|
||||
]).styled(
|
||||
with: .font(.dynamicTypeFootnote),
|
||||
.color(Theme.secondaryTextAndIconColor)
|
||||
)
|
||||
safetyNumbersUpdateTextView.linkTextAttributes = [
|
||||
.foregroundColor: Theme.primaryTextColor,
|
||||
.underlineColor: UIColor.clear,
|
||||
.underlineStyle: NSUnderlineStyle.single.rawValue
|
||||
]
|
||||
}
|
||||
|
||||
private lazy var safetyNumbersUpdateView: UIView = {
|
||||
let stackView = UIStackView()
|
||||
stackView.axis = .horizontal
|
||||
stackView.distribution = .fill
|
||||
stackView.alignment = .center
|
||||
stackView.spacing = 16
|
||||
|
||||
let imageView = UIImageView(image: UIImage(named: "safety_number_transition"))
|
||||
imageView.autoSetDimensions(to: .square(48))
|
||||
stackView.addArrangedSubview(imageView)
|
||||
|
||||
stackView.addArrangedSubview(safetyNumbersUpdateTextView)
|
||||
|
||||
return stackView
|
||||
}()
|
||||
|
||||
private lazy var fingerprintCards: [FingerprintCard] = {
|
||||
return fingerprints.map { fingerprint in
|
||||
return FingerprintCard(fingerprint: fingerprint, controller: self)
|
||||
}
|
||||
}()
|
||||
|
||||
private lazy var fingerprintCarousel: UIScrollView = {
|
||||
let scrollView = UIScrollView()
|
||||
scrollView.isPagingEnabled = true
|
||||
scrollView.isDirectionalLockEnabled = true
|
||||
scrollView.alwaysBounceVertical = false
|
||||
scrollView.alwaysBounceHorizontal = true
|
||||
scrollView.showsVerticalScrollIndicator = false
|
||||
scrollView.showsHorizontalScrollIndicator = false
|
||||
|
||||
var xOffset: CGFloat = Constants.cardHInset
|
||||
var previousView: UIView = scrollView
|
||||
var nextEdge: ALEdge = .leading
|
||||
for fingerprintCard in fingerprintCards {
|
||||
scrollView.addSubview(fingerprintCard)
|
||||
fingerprintCard.autoPinVerticalEdges(toEdgesOf: scrollView)
|
||||
scrollView.autoPinHeight(toHeightOf: fingerprintCard, relation: .greaterThanOrEqual)
|
||||
fingerprintCard.autoPinEdge(.leading, to: nextEdge, of: previousView, withOffset: xOffset)
|
||||
previousView = fingerprintCard
|
||||
xOffset = Constants.interCardSpacing
|
||||
nextEdge = .trailing
|
||||
}
|
||||
previousView.autoPinEdge(.trailing, to: .trailing, of: scrollView, withOffset: -Constants.cardHInset)
|
||||
|
||||
scrollView.delegate = self
|
||||
|
||||
return scrollView
|
||||
}()
|
||||
|
||||
private lazy var fingerprintCarouselPageControl: UIPageControl = {
|
||||
let control = UIPageControl()
|
||||
control.numberOfPages = fingerprints.count
|
||||
control.addTarget(self, action: #selector(didUpdatePageControl), for: .valueChanged)
|
||||
return control
|
||||
}()
|
||||
|
||||
private func setCarouselPageControlColors() {
|
||||
fingerprintCarouselPageControl.pageIndicatorTintColor = Theme.isDarkThemeEnabled ? .ows_gray65 : .ows_gray25
|
||||
fingerprintCarouselPageControl.currentPageIndicatorTintColor = Theme.primaryTextColor
|
||||
}
|
||||
private lazy var fingerprintCard = FingerprintCard(fingerprint: fingerprint, controller: self)
|
||||
|
||||
private lazy var instructionsTextView: UITextView = {
|
||||
let textView = LinkingTextView()
|
||||
@ -279,7 +157,7 @@ public class FingerprintViewController: OWSViewController, OWSNavigationChildCon
|
||||
// Link doesn't matter, we will override tap behavior.
|
||||
let learnMoreString = CommonStrings.learnMore.styled(with: .link(URL(string: Constants.learnMoreUrl)!))
|
||||
instructionsTextView.attributedText = NSAttributedString.composed(of: [
|
||||
String(format: instructionsFormat, contactName),
|
||||
String(format: instructionsFormat, fingerprint.theirName),
|
||||
" ",
|
||||
learnMoreString
|
||||
]).styled(
|
||||
@ -318,60 +196,34 @@ public class FingerprintViewController: OWSViewController, OWSNavigationChildCon
|
||||
}
|
||||
|
||||
private func configureUI() {
|
||||
|
||||
let scrollView = UIScrollView()
|
||||
scrollView.showsVerticalScrollIndicator = false
|
||||
scrollView.showsHorizontalScrollIndicator = false
|
||||
let containerView = UIView()
|
||||
view.addSubview(scrollView)
|
||||
let containerView = UIView()
|
||||
scrollView.addSubview(containerView)
|
||||
|
||||
scrollView.autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: .bottom)
|
||||
containerView.autoPinEdges(toEdgesOf: scrollView)
|
||||
containerView.autoPinWidth(toWidthOf: view)
|
||||
|
||||
containerView.addSubview(safetyNumbersUpdateView)
|
||||
containerView.addSubview(fingerprintCarousel)
|
||||
containerView.addSubview(fingerprintCarouselPageControl)
|
||||
containerView.addSubview(fingerprintCard)
|
||||
containerView.addSubview(instructionsTextView)
|
||||
view.addSubview(verifyUnverifyButton)
|
||||
|
||||
safetyNumbersUpdateView.autoPinEdge(.leading, to: .leading, of: containerView, withOffset: .scaleFromIPhone5To7Plus(18, 24))
|
||||
safetyNumbersUpdateView.autoPinEdge(.trailing, to: .trailing, of: containerView, withOffset: -.scaleFromIPhone5To7Plus(18, 24))
|
||||
safetyNumbersUpdateView.autoPinEdge(toSuperviewSafeArea: .top, withInset: 12)
|
||||
|
||||
fingerprintCarousel.autoPinHorizontalEdges(toEdgesOf: containerView)
|
||||
|
||||
fingerprintCards.forEach {
|
||||
$0.autoPinWidth(toWidthOf: containerView, offset: -.scaleFromIPhone5To7Plus(60, 105))
|
||||
}
|
||||
|
||||
fingerprintCarouselPageControl.autoHCenterInSuperview()
|
||||
fingerprintCarouselPageControl.autoPinEdge(.top, to: .bottom, of: fingerprintCarousel, withOffset: 8)
|
||||
fingerprintCard.autoPinEdge(toSuperviewSafeArea: .top, withInset: 56)
|
||||
fingerprintCard.autoPinWidth(toWidthOf: containerView, offset: -.scaleFromIPhone5To7Plus(60, 105))
|
||||
fingerprintCard.autoHCenterInSuperview()
|
||||
|
||||
instructionsTextView.autoPinEdge(.leading, to: .leading, of: containerView, withOffset: .scaleFromIPhone5To7Plus(18, 28))
|
||||
instructionsTextView.autoPinEdge(.trailing, to: .trailing, of: containerView, withOffset: -.scaleFromIPhone5To7Plus(18, 28))
|
||||
instructionsTextView.autoPinEdge(.bottom, to: .bottom, of: scrollView)
|
||||
instructionsTextView.autoPinEdge(.bottom, to: .bottom, of: scrollView, withOffset: -8)
|
||||
|
||||
verifyUnverifyButton.autoHCenterInSuperview()
|
||||
verifyUnverifyButton.autoPinEdge(.top, to: .bottom, of: scrollView, withOffset: .scaleFromIPhone5To7Plus(12, 24))
|
||||
verifyUnverifyButton.autoPinEdge(toSuperviewSafeArea: .bottom, withInset: .scaleFromIPhone5To7Plus(16, 40))
|
||||
|
||||
if fingerprints.count <= 1 {
|
||||
safetyNumbersUpdateView.isHidden = true
|
||||
fingerprintCarouselPageControl.isHidden = true
|
||||
scrollView.isScrollEnabled = false
|
||||
|
||||
fingerprintCarousel.autoPinEdge(toSuperviewSafeArea: .top, withInset: 56)
|
||||
instructionsTextView.autoPinEdge(.top, to: .bottom, of: fingerprintCarousel, withOffset: 24)
|
||||
} else {
|
||||
fingerprintCarousel.autoPinEdge(.top, to: .bottom, of: safetyNumbersUpdateView, withOffset: 24)
|
||||
instructionsTextView.autoPinEdge(.top, to: .bottom, of: fingerprintCarouselPageControl, withOffset: 16)
|
||||
}
|
||||
instructionsTextView.autoPinEdge(.top, to: .bottom, of: fingerprintCard, withOffset: 24)
|
||||
|
||||
updateVerificationStateLabel()
|
||||
setSafetyNumbersUpdateTextViewText()
|
||||
setCarouselPageControlColors()
|
||||
setInstructionsText()
|
||||
setVerifyUnverifyButtonColors()
|
||||
}
|
||||
@ -410,12 +262,7 @@ public class FingerprintViewController: OWSViewController, OWSNavigationChildCon
|
||||
|
||||
layer.cornerRadius = Constants.cornerRadius
|
||||
|
||||
self.backgroundColor = {
|
||||
switch fingerprint.source {
|
||||
case .aci: return UIColor(rgbHex: 0x506ecd)
|
||||
case .e164: return UIColor(rgbHex: 0xdeddda)
|
||||
}
|
||||
}()
|
||||
self.backgroundColor = UIColor(rgbHex: 0x506ecd)
|
||||
|
||||
addSubview(shareButton)
|
||||
addSubview(qrCodeView)
|
||||
@ -450,16 +297,9 @@ public class FingerprintViewController: OWSViewController, OWSNavigationChildCon
|
||||
|
||||
private lazy var shareButton: UIButton = {
|
||||
let button = UIButton()
|
||||
let tintColor: UIColor
|
||||
switch fingerprint.source {
|
||||
case .aci:
|
||||
tintColor = .white
|
||||
case .e164:
|
||||
tintColor = .black
|
||||
}
|
||||
button.setTemplateImage(
|
||||
Theme.iconImage(.buttonShare).withRenderingMode(.alwaysTemplate),
|
||||
tintColor: tintColor
|
||||
tintColor: .white
|
||||
)
|
||||
button.addTarget(self, action: #selector(didTapShare), for: .touchUpInside)
|
||||
return button
|
||||
@ -500,12 +340,7 @@ public class FingerprintViewController: OWSViewController, OWSNavigationChildCon
|
||||
label.text = fingerprint.displayableText
|
||||
label.font = UIFont(name: "Menlo-Regular", size: 23)
|
||||
label.textAlignment = .center
|
||||
switch fingerprint.source {
|
||||
case .aci:
|
||||
label.textColor = .white
|
||||
case .e164:
|
||||
label.textColor = Theme.lightThemeSecondaryTextAndIconColor
|
||||
}
|
||||
label.textColor = .white
|
||||
label.numberOfLines = 3
|
||||
label.lineBreakMode = .byTruncatingTail
|
||||
label.adjustsFontSizeToFitWidth = true
|
||||
@ -538,7 +373,6 @@ public class FingerprintViewController: OWSViewController, OWSNavigationChildCon
|
||||
// MARK: PillBoxView
|
||||
|
||||
class PillBoxView: UIView {
|
||||
|
||||
override var bounds: CGRect {
|
||||
didSet {
|
||||
self.layer.cornerRadius = bounds.height / 2
|
||||
@ -546,180 +380,6 @@ public class FingerprintViewController: OWSViewController, OWSNavigationChildCon
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Transition Sheet
|
||||
|
||||
private lazy var kvStore: KeyValueStore = {
|
||||
return DependenciesBridge.shared.keyValueStoreFactory.keyValueStore(collection: "MultiFingerprintVC")
|
||||
}()
|
||||
|
||||
private static let hasShownTransitionSheetKey = "hasShownTransitionSheetKey"
|
||||
|
||||
private func hasShownTransitionSheet(_ tx: DBReadTransaction) -> Bool {
|
||||
return self.kvStore.getBool(Self.hasShownTransitionSheetKey, defaultValue: false, transaction: tx)
|
||||
}
|
||||
|
||||
private func setHasShownTransitionSheet(_ tx: DBWriteTransaction) {
|
||||
self.kvStore.setBool(true, key: Self.hasShownTransitionSheetKey, transaction: tx)
|
||||
}
|
||||
|
||||
private func showTransitionSheet(_ tx: DBWriteTransaction) {
|
||||
self.setHasShownTransitionSheet(tx)
|
||||
tx.addAsyncCompletion(on: DispatchQueue.main) {
|
||||
let sheet = TransitionSheetViewController(parent: self)
|
||||
self.present(sheet, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
class TransitionSheetViewController: InteractiveSheetViewController {
|
||||
let contentScrollView = UIScrollView()
|
||||
let stackView = UIStackView()
|
||||
public override var interactiveScrollViews: [UIScrollView] { [contentScrollView] }
|
||||
public override var sheetBackgroundColor: UIColor { Theme.tableView2PresentedBackgroundColor }
|
||||
|
||||
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
||||
return .portrait
|
||||
}
|
||||
|
||||
private weak var parentVc: FingerprintViewController?
|
||||
|
||||
init(parent: FingerprintViewController) {
|
||||
self.parentVc = parent
|
||||
super.init()
|
||||
}
|
||||
|
||||
override public func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
minimizedHeight = 600
|
||||
super.allowsExpansion = true
|
||||
|
||||
contentView.addSubview(contentScrollView)
|
||||
|
||||
stackView.axis = .vertical
|
||||
stackView.layoutMargins = UIEdgeInsets(hMargin: 24, vMargin: 24)
|
||||
stackView.spacing = 16
|
||||
stackView.isLayoutMarginsRelativeArrangement = true
|
||||
contentScrollView.addSubview(stackView)
|
||||
stackView.autoPinHeightToSuperview()
|
||||
// Pin to the scroll view's viewport, not to its scrollable area
|
||||
stackView.autoPinWidth(toWidthOf: contentScrollView)
|
||||
|
||||
contentScrollView.autoPinEdgesToSuperviewEdges()
|
||||
contentScrollView.alwaysBounceVertical = true
|
||||
|
||||
buildContents()
|
||||
}
|
||||
|
||||
override public func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
if !animationView.isAnimationQueued && !animationView.isAnimationPlaying {
|
||||
animationView.play { [weak self] success in
|
||||
guard success else { return }
|
||||
self?.loopAnimation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override public func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
|
||||
if animationView.isAnimationQueued || animationView.isAnimationPlaying {
|
||||
animationView.stop()
|
||||
}
|
||||
}
|
||||
|
||||
private func loopAnimation() {
|
||||
animationView.play(fromFrame: 60, toFrame: 360, completion: { [weak self] success in
|
||||
guard success else { return }
|
||||
self?.loopAnimation()
|
||||
})
|
||||
}
|
||||
|
||||
private lazy var animationView: AnimationView = {
|
||||
let animationView = AnimationView(name: "safety-numbers")
|
||||
animationView.contentMode = .scaleAspectFit
|
||||
animationView.isUserInteractionEnabled = false
|
||||
animationView.backgroundColor = .white
|
||||
animationView.layer.cornerRadius = 12
|
||||
animationView.layer.masksToBounds = true
|
||||
return animationView
|
||||
}()
|
||||
|
||||
private func buildContents() {
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.textAlignment = .center
|
||||
titleLabel.font = UIFont.dynamicTypeTitle2.semibold()
|
||||
titleLabel.text = OWSLocalizedString(
|
||||
"SAFETY_NUMBER_TRANSITION_SHEET_TITLE",
|
||||
comment: "Title for a sheet informing the user about the transition from phone number to user identifier based."
|
||||
)
|
||||
titleLabel.numberOfLines = 0
|
||||
titleLabel.lineBreakMode = .byWordWrapping
|
||||
stackView.addArrangedSubview(titleLabel)
|
||||
|
||||
let paragraphs: [String] = [
|
||||
OWSLocalizedString(
|
||||
"SAFETY_NUMBER_TRANSITION_SHEET_PARAGRAPH_1",
|
||||
comment: "Informs the user about the transition from phone number to user identifier based."
|
||||
),
|
||||
OWSLocalizedString(
|
||||
"SAFETY_NUMBER_TRANSITION_SHEET_PARAGRAPH_2",
|
||||
comment: "Informs the user about the transition from phone number to user identifier based."
|
||||
)
|
||||
]
|
||||
var lastParagraphLabel: UILabel!
|
||||
for paragraph in paragraphs {
|
||||
let paragraphLabel = UILabel()
|
||||
paragraphLabel.text = paragraph
|
||||
paragraphLabel.textAlignment = .natural
|
||||
paragraphLabel.font = .dynamicTypeSubheadlineClamped
|
||||
paragraphLabel.numberOfLines = 0
|
||||
paragraphLabel.lineBreakMode = .byWordWrapping
|
||||
paragraphLabel.textColor = Theme.secondaryTextAndIconColor
|
||||
stackView.addArrangedSubview(paragraphLabel)
|
||||
lastParagraphLabel = paragraphLabel
|
||||
}
|
||||
stackView.setCustomSpacing(20, after: lastParagraphLabel)
|
||||
|
||||
stackView.addArrangedSubview(animationView)
|
||||
stackView.setCustomSpacing(18, after: animationView)
|
||||
animationView.autoMatch(.height, to: .width, of: animationView, withMultiplier: 172/346)
|
||||
|
||||
let learnMoreTitle = OWSLocalizedString(
|
||||
"SAFETY_NUMBER_TRANSITION_SHEET_HELP_TEXT",
|
||||
comment: "Button text for a sheet informing the user about the transition from phone number to user identifier based."
|
||||
)
|
||||
let learnMoreButton = UIButton(type: .system)
|
||||
learnMoreButton.setTitle(learnMoreTitle, for: .normal)
|
||||
learnMoreButton.titleLabel?.font = .dynamicTypeBody
|
||||
learnMoreButton.setTitleColor(Theme.isDarkThemeEnabled ? .ows_accentBlueDark : .link, for: .normal)
|
||||
learnMoreButton.addTarget(self, action: #selector(didTapLearnMore), for: .touchUpInside)
|
||||
stackView.addArrangedSubview(learnMoreButton)
|
||||
stackView.setCustomSpacing(24, after: learnMoreButton)
|
||||
|
||||
let continueButton = OWSButton(
|
||||
title: OWSLocalizedString(
|
||||
"ALERT_ACTION_ACKNOWLEDGE",
|
||||
comment: "generic button text to acknowledge that the corresponding text was read."
|
||||
)
|
||||
) { [weak self] in
|
||||
self?.dismiss(animated: true)
|
||||
}
|
||||
continueButton.layer.cornerRadius = 16
|
||||
continueButton.backgroundColor = .ows_accentBlue
|
||||
continueButton.dimsWhenHighlighted = true
|
||||
continueButton.titleLabel?.font = UIFont.dynamicTypeBody.semibold()
|
||||
continueButton.autoSetDimension(.height, toSize: 50, relation: .greaterThanOrEqual)
|
||||
stackView.addArrangedSubview(continueButton)
|
||||
}
|
||||
|
||||
@objc
|
||||
func didTapLearnMore() {
|
||||
FingerprintViewController.showLearnMoreUrl(from: self)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Actions
|
||||
|
||||
@objc
|
||||
@ -737,12 +397,6 @@ public class FingerprintViewController: OWSViewController, OWSNavigationChildCon
|
||||
viewController.present(safariVC, animated: true)
|
||||
}
|
||||
|
||||
@objc
|
||||
private func didUpdatePageControl() {
|
||||
self.selectedIndex = fingerprintCarouselPageControl.currentPage
|
||||
scrollToSelectedIndex()
|
||||
}
|
||||
|
||||
@objc
|
||||
private func didTapVerifyUnverify(_ gestureRecognizer: UITapGestureRecognizer) {
|
||||
guard gestureRecognizer.state == .recognized else { return }
|
||||
@ -764,10 +418,6 @@ public class FingerprintViewController: OWSViewController, OWSNavigationChildCon
|
||||
}
|
||||
|
||||
private func shareFingerprint(from fromView: UIView) {
|
||||
let fingerprint = fingerprints[selectedIndex]
|
||||
|
||||
Logger.debug("Sharing safety numbers")
|
||||
|
||||
let compareActivity = CompareSafetyNumbersActivity(delegate: self)
|
||||
|
||||
let shareFormat = NSLocalizedString(
|
||||
@ -802,21 +452,11 @@ public class FingerprintViewController: OWSViewController, OWSNavigationChildCon
|
||||
let viewController = FingerprintScanViewController(
|
||||
recipientAci: recipientAci,
|
||||
recipientIdentity: recipientIdentity,
|
||||
fingerprints: self.fingerprints
|
||||
fingerprint: self.fingerprint
|
||||
)
|
||||
navigationController?.pushViewController(viewController, animated: true)
|
||||
}
|
||||
|
||||
private func scrollToSelectedIndex(animated: Bool = true) {
|
||||
let xOffset: CGFloat
|
||||
if selectedIndex == 0 {
|
||||
xOffset = 0
|
||||
} else {
|
||||
xOffset = (CGFloat(selectedIndex) * UIScreen.main.bounds.width) - (Constants.interCardSpacing + Constants.cardHInset)
|
||||
}
|
||||
fingerprintCarousel.setContentOffset(.init(x: xOffset, y: 0), animated: animated)
|
||||
}
|
||||
|
||||
// MARK: Notifications
|
||||
|
||||
private var identityStateChangeObserver: Any?
|
||||
@ -830,10 +470,6 @@ public class FingerprintViewController: OWSViewController, OWSNavigationChildCon
|
||||
|
||||
enum Constants {
|
||||
static let cardHInset: CGFloat = .scaleFromIPhone5To7Plus(30, 53)
|
||||
static var interCardSpacing: CGFloat = cardHInset / 2
|
||||
|
||||
// Link doesn't matter, we will override tap behavior.
|
||||
static let transitionLearnMoreUrl = "https://support.signal.org/"
|
||||
static let learnMoreUrl = "https://support.signal.org/learnMore"
|
||||
}
|
||||
}
|
||||
@ -845,7 +481,7 @@ extension FingerprintViewController: CompareSafetyNumbersActivityDelegate {
|
||||
from: self,
|
||||
identityKey: identityKey,
|
||||
recipientAci: recipientAci,
|
||||
contactName: contactName,
|
||||
contactName: fingerprint.theirName,
|
||||
tag: logTag
|
||||
)
|
||||
}
|
||||
@ -864,24 +500,10 @@ extension FingerprintViewController: CompareSafetyNumbersActivityDelegate {
|
||||
}
|
||||
|
||||
extension FingerprintViewController: UITextViewDelegate {
|
||||
|
||||
public func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
|
||||
if URL.absoluteString == Constants.transitionLearnMoreUrl {
|
||||
DependenciesBridge.shared.db.write {
|
||||
self.showTransitionSheet($0)
|
||||
}
|
||||
} else if URL.absoluteString == Constants.learnMoreUrl {
|
||||
if URL.absoluteString == Constants.learnMoreUrl {
|
||||
self.didTapLearnMore()
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
extension FingerprintViewController: UIScrollViewDelegate {
|
||||
|
||||
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
||||
let selectedIndex = Int(scrollView.contentOffset.x / (scrollView.frame.width - (Constants.cardHInset * 2)))
|
||||
self.selectedIndex = selectedIndex
|
||||
self.fingerprintCarouselPageControl.currentPage = selectedIndex
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,799 +0,0 @@
|
||||
//
|
||||
// Copyright 2023 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
import Lottie
|
||||
import PureLayout
|
||||
import SafariServices
|
||||
import SignalMessaging
|
||||
import SignalServiceKit
|
||||
import UIKit
|
||||
|
||||
public class MultiFingerprintViewController: OWSViewController, OWSNavigationChildController {
|
||||
|
||||
public var preferredNavigationBarStyle: OWSNavigationBarStyle {
|
||||
return .solid
|
||||
}
|
||||
|
||||
public var navbarBackgroundColorOverride: UIColor? {
|
||||
return Self.backgroundColor
|
||||
}
|
||||
|
||||
public override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
||||
return .portrait
|
||||
}
|
||||
|
||||
private let recipientAddress: SignalServiceAddress
|
||||
private let recipientIdentity: OWSRecipientIdentity
|
||||
private let contactName: String
|
||||
private let identityKey: IdentityKey
|
||||
private let fingerprints: [OWSFingerprint]
|
||||
private var selectedIndex: Int
|
||||
|
||||
public init(
|
||||
fingerprints: [OWSFingerprint],
|
||||
defaultIndex: Int,
|
||||
recipientAddress: SignalServiceAddress,
|
||||
recipientIdentity: OWSRecipientIdentity
|
||||
) {
|
||||
self.recipientAddress = recipientAddress
|
||||
self.contactName = SSKEnvironment.shared.contactsManagerRef.displayName(for: recipientAddress)
|
||||
// By capturing the identity key when we enter these views, we prevent the edge case
|
||||
// where the user verifies a key that we learned about while this view was open.
|
||||
self.recipientIdentity = recipientIdentity
|
||||
self.identityKey = recipientIdentity.identityKey
|
||||
self.fingerprints = fingerprints
|
||||
self.selectedIndex = defaultIndex
|
||||
|
||||
super.init()
|
||||
|
||||
title = NSLocalizedString("PRIVACY_VERIFICATION_TITLE", comment: "Navbar title")
|
||||
navigationItem.leftBarButtonItem = .init(
|
||||
barButtonSystemItem: .done,
|
||||
target: self, action: #selector(didTapDone),
|
||||
accessibilityIdentifier: "FingerprintViewController.done"
|
||||
)
|
||||
|
||||
identityStateChangeObserver = NotificationCenter.default.addObserver(
|
||||
forName: .identityStateDidChange,
|
||||
object: nil,
|
||||
queue: .main) { [weak self] _ in
|
||||
self?.identityStateDidChange()
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
if let identityStateChangeObserver {
|
||||
NotificationCenter.default.removeObserver(identityStateChangeObserver)
|
||||
}
|
||||
}
|
||||
|
||||
private static var backgroundColor: UIColor {
|
||||
return Theme.isDarkThemeEnabled ? .ows_gray90 : .ows_gray02
|
||||
}
|
||||
|
||||
public override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
view.backgroundColor = Self.backgroundColor
|
||||
|
||||
configureUI()
|
||||
}
|
||||
|
||||
public override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
scrollToSelectedIndex(animated: false)
|
||||
}
|
||||
|
||||
public override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
if !DependenciesBridge.shared.db.read(block: self.hasShownTransitionSheet) {
|
||||
// Its fine to not re-read the value in the write tx; stakes are low.
|
||||
DependenciesBridge.shared.db.write(block: self.showTransitionSheet)
|
||||
}
|
||||
}
|
||||
|
||||
public override func themeDidChange() {
|
||||
super.themeDidChange()
|
||||
view.backgroundColor = Self.backgroundColor
|
||||
|
||||
updateVerificationStateLabel()
|
||||
setSafetyNumbersUpdateTextViewText()
|
||||
setCarouselPageControlColors()
|
||||
setInstructionsText()
|
||||
setVerifyUnverifyButtonColors()
|
||||
}
|
||||
|
||||
// MARK: UI
|
||||
|
||||
private lazy var safetyNumbersUpdateTextView: LinkingTextView = {
|
||||
let textView = LinkingTextView()
|
||||
textView.delegate = self
|
||||
return textView
|
||||
}()
|
||||
|
||||
private func setSafetyNumbersUpdateTextViewText() {
|
||||
// Link doesn't matter, we will override tap behavior.
|
||||
let learnMoreString = CommonStrings.learnMore.styled(with: .link(URL(string: Constants.transitionLearnMoreUrl)!))
|
||||
safetyNumbersUpdateTextView.attributedText = NSAttributedString.composed(of: [
|
||||
OWSLocalizedString(
|
||||
"SAFETY_NUMBER_TRANSITION_HEADER_ALERT",
|
||||
comment: "Header informing the user about the transition from phone number to user identifier based."
|
||||
),
|
||||
"\n",
|
||||
learnMoreString
|
||||
]).styled(
|
||||
with: .font(.dynamicTypeFootnote),
|
||||
.color(Theme.secondaryTextAndIconColor)
|
||||
)
|
||||
safetyNumbersUpdateTextView.linkTextAttributes = [
|
||||
.foregroundColor: Theme.primaryTextColor,
|
||||
.underlineColor: UIColor.clear,
|
||||
.underlineStyle: NSUnderlineStyle.single.rawValue
|
||||
]
|
||||
}
|
||||
|
||||
private lazy var safetyNumbersUpdateView: UIView = {
|
||||
let stackView = UIStackView()
|
||||
stackView.axis = .horizontal
|
||||
stackView.distribution = .fill
|
||||
stackView.alignment = .center
|
||||
stackView.spacing = 16
|
||||
|
||||
let imageView = UIImageView(image: UIImage(named: "safety_number_transition"))
|
||||
imageView.autoSetDimensions(to: .square(48))
|
||||
stackView.addArrangedSubview(imageView)
|
||||
|
||||
stackView.addArrangedSubview(safetyNumbersUpdateTextView)
|
||||
|
||||
return stackView
|
||||
}()
|
||||
|
||||
private lazy var fingerprintCards: [FingerprintCard] = {
|
||||
return fingerprints.map { fingerprint in
|
||||
return FingerprintCard(fingerprint: fingerprint, controller: self)
|
||||
}
|
||||
}()
|
||||
|
||||
private lazy var fingerprintCarousel: UIScrollView = {
|
||||
let scrollView = UIScrollView()
|
||||
scrollView.isPagingEnabled = true
|
||||
scrollView.isDirectionalLockEnabled = true
|
||||
scrollView.alwaysBounceVertical = false
|
||||
scrollView.alwaysBounceHorizontal = true
|
||||
scrollView.showsVerticalScrollIndicator = false
|
||||
scrollView.showsHorizontalScrollIndicator = false
|
||||
|
||||
var xOffset: CGFloat = Constants.cardHInset
|
||||
var previousView: UIView = scrollView
|
||||
var nextEdge: ALEdge = .leading
|
||||
for fingerprintCard in fingerprintCards {
|
||||
scrollView.addSubview(fingerprintCard)
|
||||
fingerprintCard.autoPinVerticalEdges(toEdgesOf: scrollView)
|
||||
scrollView.autoPinHeight(toHeightOf: fingerprintCard, relation: .greaterThanOrEqual)
|
||||
fingerprintCard.autoPinEdge(.leading, to: nextEdge, of: previousView, withOffset: xOffset)
|
||||
previousView = fingerprintCard
|
||||
xOffset = Constants.interCardSpacing
|
||||
nextEdge = .trailing
|
||||
}
|
||||
previousView.autoPinEdge(.trailing, to: .trailing, of: scrollView, withOffset: -Constants.cardHInset)
|
||||
|
||||
scrollView.delegate = self
|
||||
|
||||
return scrollView
|
||||
}()
|
||||
|
||||
private lazy var fingerprintCarouselPageControl: UIPageControl = {
|
||||
let control = UIPageControl()
|
||||
control.numberOfPages = fingerprints.count
|
||||
control.addTarget(self, action: #selector(didUpdatePageControl), for: .valueChanged)
|
||||
return control
|
||||
}()
|
||||
|
||||
private func setCarouselPageControlColors() {
|
||||
fingerprintCarouselPageControl.pageIndicatorTintColor = Theme.isDarkThemeEnabled ? .ows_gray65 : .ows_gray25
|
||||
fingerprintCarouselPageControl.currentPageIndicatorTintColor = Theme.primaryTextColor
|
||||
}
|
||||
|
||||
private lazy var instructionsTextView: UITextView = {
|
||||
let textView = LinkingTextView()
|
||||
textView.delegate = self
|
||||
return textView
|
||||
}()
|
||||
|
||||
private func setInstructionsText() {
|
||||
let instructionsFormat = OWSLocalizedString(
|
||||
"VERIFY_SAFETY_NUMBER_INSTRUCTIONS",
|
||||
comment: "Instructions for verifying your safety number. Embeds {{contact's name}}"
|
||||
)
|
||||
// Link doesn't matter, we will override tap behavior.
|
||||
let learnMoreString = CommonStrings.learnMore.styled(with: .link(URL(string: Constants.learnMoreUrl)!))
|
||||
instructionsTextView.attributedText = NSAttributedString.composed(of: [
|
||||
String(format: instructionsFormat, contactName),
|
||||
" ",
|
||||
learnMoreString
|
||||
]).styled(
|
||||
with: .font(.dynamicTypeFootnote),
|
||||
.color(Theme.secondaryTextAndIconColor),
|
||||
.alignment(.center)
|
||||
)
|
||||
instructionsTextView.linkTextAttributes = [
|
||||
.foregroundColor: Theme.primaryTextColor,
|
||||
.underlineColor: UIColor.clear,
|
||||
.underlineStyle: NSUnderlineStyle.single.rawValue
|
||||
]
|
||||
}
|
||||
|
||||
private lazy var verifyUnverifyButtonLabel = UILabel()
|
||||
private lazy var verifyUnverifyPillbox = PillBoxView()
|
||||
|
||||
private lazy var verifyUnverifyButton: UIView = {
|
||||
verifyUnverifyPillbox.layer.masksToBounds = true
|
||||
verifyUnverifyPillbox.accessibilityIdentifier = "FingerprintViewController.verifyUnverifyButton"
|
||||
verifyUnverifyPillbox.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(didTapVerifyUnverify)))
|
||||
|
||||
verifyUnverifyButtonLabel.font = .systemFont(ofSize: 13, weight: .bold)
|
||||
verifyUnverifyButtonLabel.textAlignment = .center
|
||||
verifyUnverifyButtonLabel.setContentCompressionResistancePriority(.defaultHigh, for: .vertical)
|
||||
verifyUnverifyPillbox.addSubview(verifyUnverifyButtonLabel)
|
||||
verifyUnverifyButtonLabel.autoPinWidthToSuperview(withMargin: 24)
|
||||
verifyUnverifyButtonLabel.autoPinHeightToSuperview(withMargin: 12)
|
||||
|
||||
return verifyUnverifyPillbox
|
||||
}()
|
||||
|
||||
private func setVerifyUnverifyButtonColors() {
|
||||
verifyUnverifyButtonLabel.textColor = Theme.primaryTextColor
|
||||
verifyUnverifyPillbox.backgroundColor = Theme.isDarkThemeEnabled ? .ows_gray80 : .white
|
||||
}
|
||||
|
||||
private func configureUI() {
|
||||
|
||||
let scrollView = UIScrollView()
|
||||
scrollView.showsVerticalScrollIndicator = false
|
||||
scrollView.showsHorizontalScrollIndicator = false
|
||||
let containerView = UIView()
|
||||
view.addSubview(scrollView)
|
||||
scrollView.addSubview(containerView)
|
||||
|
||||
scrollView.autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: .bottom)
|
||||
containerView.autoPinEdges(toEdgesOf: scrollView)
|
||||
containerView.autoPinWidth(toWidthOf: view)
|
||||
|
||||
containerView.addSubview(safetyNumbersUpdateView)
|
||||
containerView.addSubview(fingerprintCarousel)
|
||||
containerView.addSubview(fingerprintCarouselPageControl)
|
||||
containerView.addSubview(instructionsTextView)
|
||||
view.addSubview(verifyUnverifyButton)
|
||||
|
||||
safetyNumbersUpdateView.autoPinEdge(.leading, to: .leading, of: containerView, withOffset: .scaleFromIPhone5To7Plus(18, 24))
|
||||
safetyNumbersUpdateView.autoPinEdge(.trailing, to: .trailing, of: containerView, withOffset: -.scaleFromIPhone5To7Plus(18, 24))
|
||||
safetyNumbersUpdateView.autoPinEdge(toSuperviewSafeArea: .top, withInset: 12)
|
||||
|
||||
fingerprintCarousel.autoPinHorizontalEdges(toEdgesOf: containerView)
|
||||
|
||||
fingerprintCards.forEach {
|
||||
$0.autoPinWidth(toWidthOf: containerView, offset: -.scaleFromIPhone5To7Plus(60, 105))
|
||||
}
|
||||
|
||||
fingerprintCarouselPageControl.autoHCenterInSuperview()
|
||||
fingerprintCarouselPageControl.autoPinEdge(.top, to: .bottom, of: fingerprintCarousel, withOffset: 8)
|
||||
|
||||
instructionsTextView.autoPinEdge(.leading, to: .leading, of: containerView, withOffset: .scaleFromIPhone5To7Plus(18, 28))
|
||||
instructionsTextView.autoPinEdge(.trailing, to: .trailing, of: containerView, withOffset: -.scaleFromIPhone5To7Plus(18, 28))
|
||||
instructionsTextView.autoPinEdge(.bottom, to: .bottom, of: scrollView)
|
||||
|
||||
verifyUnverifyButton.autoHCenterInSuperview()
|
||||
verifyUnverifyButton.autoPinEdge(.top, to: .bottom, of: scrollView, withOffset: .scaleFromIPhone5To7Plus(12, 24))
|
||||
verifyUnverifyButton.autoPinEdge(toSuperviewSafeArea: .bottom, withInset: .scaleFromIPhone5To7Plus(16, 40))
|
||||
|
||||
if fingerprints.count <= 1 {
|
||||
safetyNumbersUpdateView.isHidden = true
|
||||
fingerprintCarouselPageControl.isHidden = true
|
||||
scrollView.isScrollEnabled = false
|
||||
|
||||
fingerprintCarousel.autoPinEdge(toSuperviewSafeArea: .top, withInset: 56)
|
||||
instructionsTextView.autoPinEdge(.top, to: .bottom, of: fingerprintCarousel, withOffset: 24)
|
||||
} else {
|
||||
fingerprintCarousel.autoPinEdge(.top, to: .bottom, of: safetyNumbersUpdateView, withOffset: 24)
|
||||
instructionsTextView.autoPinEdge(.top, to: .bottom, of: fingerprintCarouselPageControl, withOffset: 16)
|
||||
}
|
||||
|
||||
updateVerificationStateLabel()
|
||||
setSafetyNumbersUpdateTextViewText()
|
||||
setCarouselPageControlColors()
|
||||
setInstructionsText()
|
||||
setVerifyUnverifyButtonColors()
|
||||
}
|
||||
|
||||
private func updateVerificationStateLabel() {
|
||||
owsAssertBeta(recipientAddress.isValid)
|
||||
|
||||
let isVerified = OWSIdentityManager.shared.verificationState(for: recipientAddress) == .verified
|
||||
|
||||
if isVerified {
|
||||
verifyUnverifyButtonLabel.text = NSLocalizedString(
|
||||
"PRIVACY_UNVERIFY_BUTTON",
|
||||
comment: "Button that lets user mark another user's identity as unverified."
|
||||
)
|
||||
} else {
|
||||
verifyUnverifyButtonLabel.text = OWSLocalizedString(
|
||||
"PRIVACY_VERIFY_BUTTON",
|
||||
comment: "Button that lets user mark another user's identity as verified."
|
||||
)
|
||||
}
|
||||
view.setNeedsLayout()
|
||||
}
|
||||
|
||||
// MARK: - Fingerprint Card
|
||||
|
||||
class FingerprintCard: UIView {
|
||||
|
||||
private let fingerprint: OWSFingerprint
|
||||
private weak var controller: MultiFingerprintViewController?
|
||||
|
||||
init(fingerprint: OWSFingerprint, controller: MultiFingerprintViewController) {
|
||||
self.fingerprint = fingerprint
|
||||
self.controller = controller
|
||||
super.init(frame: .zero)
|
||||
|
||||
layer.cornerRadius = Constants.cornerRadius
|
||||
|
||||
self.backgroundColor = {
|
||||
switch fingerprint.source {
|
||||
case .aci: return UIColor(rgbHex: 0x506ecd)
|
||||
case .e164: return UIColor(rgbHex: 0xdeddda)
|
||||
}
|
||||
}()
|
||||
|
||||
addSubview(shareButton)
|
||||
addSubview(qrCodeView)
|
||||
addSubview(safetyNumberLabel)
|
||||
|
||||
shareButton.autoPinEdge(.top, to: .top, of: self, withOffset: 16)
|
||||
shareButton.autoPinEdge(.trailing, to: .trailing, of: self, withOffset: -16)
|
||||
|
||||
qrCodeView.autoPinEdge(.top, to: .bottom, of: shareButton, withOffset: 8)
|
||||
qrCodeView.autoPinEdge(.leading, to: .leading, of: self, withOffset: .scaleFromIPhone5To7Plus(44, 64))
|
||||
qrCodeView.autoPinEdge(.trailing, to: .trailing, of: self, withOffset: -.scaleFromIPhone5To7Plus(44, 64))
|
||||
|
||||
safetyNumberLabel.autoPinEdge(.top, to: .bottom, of: qrCodeView, withOffset: 30)
|
||||
safetyNumberLabel.autoPinEdge(.leading, to: .leading, of: self, withOffset: .scaleFromIPhone5To7Plus(20, 35))
|
||||
safetyNumberLabel.autoPinEdge(.trailing, to: .trailing, of: self, withOffset: -.scaleFromIPhone5To7Plus(20, 35))
|
||||
safetyNumberLabel.autoPinEdge(.bottom, to: .bottom, of: self, withOffset: -.scaleFromIPhone5To7Plus(27, 47))
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
private lazy var shareButton: UIButton = {
|
||||
let button = UIButton()
|
||||
let tintColor: UIColor
|
||||
switch fingerprint.source {
|
||||
case .aci:
|
||||
tintColor = .white
|
||||
case .e164:
|
||||
tintColor = .black
|
||||
}
|
||||
button.setTemplateImage(
|
||||
Theme.iconImage(.buttonShare).withRenderingMode(.alwaysTemplate),
|
||||
tintColor: tintColor
|
||||
)
|
||||
button.addTarget(self, action: #selector(didTapShare), for: .touchUpInside)
|
||||
return button
|
||||
}()
|
||||
|
||||
private lazy var qrCodeView: UIView = {
|
||||
let containerView = UIView()
|
||||
containerView.backgroundColor = .white
|
||||
containerView.layer.cornerRadius = Constants.cornerRadius
|
||||
containerView.layer.masksToBounds = true
|
||||
|
||||
let fingerprintImageView = UIImageView()
|
||||
fingerprintImageView.image = fingerprint.image
|
||||
// Don't antialias QR Codes.
|
||||
fingerprintImageView.layer.magnificationFilter = .nearest
|
||||
fingerprintImageView.layer.minificationFilter = .nearest
|
||||
fingerprintImageView.setCompressionResistanceLow()
|
||||
containerView.addSubview(fingerprintImageView)
|
||||
fingerprintImageView.autoPin(toAspectRatio: 1)
|
||||
fingerprintImageView.autoPinEdgesToSuperviewEdges(with: UIEdgeInsets(margin: 20), excludingEdge: .bottom)
|
||||
|
||||
let scanLabel = UILabel()
|
||||
scanLabel.text = NSLocalizedString("PRIVACY_TAP_TO_SCAN", comment: "Button that shows the 'scan with camera' view.")
|
||||
scanLabel.font = .systemFont(ofSize: .scaleFromIPhone5To7Plus(13, 15))
|
||||
scanLabel.textColor = Theme.lightThemeSecondaryTextAndIconColor
|
||||
containerView.addSubview(scanLabel)
|
||||
scanLabel.autoHCenterInSuperview()
|
||||
scanLabel.autoPinEdge(.top, to: .bottom, of: fingerprintImageView, withOffset: 12)
|
||||
scanLabel.autoPinEdge(.bottom, to: .bottom, of: containerView, withOffset: -14)
|
||||
|
||||
containerView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(didTapToScan)))
|
||||
|
||||
return containerView
|
||||
}()
|
||||
|
||||
private lazy var safetyNumberLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = fingerprint.displayableText
|
||||
label.font = UIFont(name: "Menlo-Regular", size: 23)
|
||||
label.textAlignment = .center
|
||||
switch fingerprint.source {
|
||||
case .aci:
|
||||
label.textColor = .white
|
||||
case .e164:
|
||||
label.textColor = Theme.lightThemeSecondaryTextAndIconColor
|
||||
}
|
||||
label.numberOfLines = 3
|
||||
label.lineBreakMode = .byTruncatingTail
|
||||
label.adjustsFontSizeToFitWidth = true
|
||||
label.isUserInteractionEnabled = true
|
||||
label.accessibilityIdentifier = "FingerprintViewController.fingerprintLabel"
|
||||
return label
|
||||
}()
|
||||
|
||||
@objc
|
||||
func didTapToScan() {
|
||||
controller?.didTapToScan()
|
||||
}
|
||||
|
||||
@objc
|
||||
func didTapShare() {
|
||||
controller?.shareFingerprint(from: shareButton)
|
||||
}
|
||||
|
||||
enum Constants {
|
||||
static let cornerRadius: CGFloat = 18
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: PillBoxView
|
||||
|
||||
class PillBoxView: UIView {
|
||||
|
||||
override var bounds: CGRect {
|
||||
didSet {
|
||||
self.layer.cornerRadius = bounds.height / 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Transition Sheet
|
||||
|
||||
private lazy var kvStore: KeyValueStore = {
|
||||
return DependenciesBridge.shared.keyValueStoreFactory.keyValueStore(collection: "MultiFingerprintVC")
|
||||
}()
|
||||
|
||||
private static let hasShownTransitionSheetKey = "hasShownTransitionSheetKey"
|
||||
|
||||
private func hasShownTransitionSheet(_ tx: DBReadTransaction) -> Bool {
|
||||
return self.kvStore.getBool(Self.hasShownTransitionSheetKey, defaultValue: false, transaction: tx)
|
||||
}
|
||||
|
||||
private func setHasShownTransitionSheet(_ tx: DBWriteTransaction) {
|
||||
self.kvStore.setBool(true, key: Self.hasShownTransitionSheetKey, transaction: tx)
|
||||
}
|
||||
|
||||
private func showTransitionSheet(_ tx: DBWriteTransaction) {
|
||||
self.setHasShownTransitionSheet(tx)
|
||||
tx.addAsyncCompletion(on: DispatchQueue.main) {
|
||||
let sheet = TransitionSheetViewController(parent: self)
|
||||
self.present(sheet, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
class TransitionSheetViewController: InteractiveSheetViewController {
|
||||
let contentScrollView = UIScrollView()
|
||||
let stackView = UIStackView()
|
||||
public override var interactiveScrollViews: [UIScrollView] { [contentScrollView] }
|
||||
public override var sheetBackgroundColor: UIColor { Theme.tableView2PresentedBackgroundColor }
|
||||
|
||||
private weak var parentVc: MultiFingerprintViewController?
|
||||
|
||||
init(parent: MultiFingerprintViewController) {
|
||||
self.parentVc = parent
|
||||
super.init()
|
||||
}
|
||||
|
||||
override public func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
minimizedHeight = 600
|
||||
super.allowsExpansion = true
|
||||
|
||||
contentView.addSubview(contentScrollView)
|
||||
|
||||
stackView.axis = .vertical
|
||||
stackView.layoutMargins = UIEdgeInsets(hMargin: 24, vMargin: 24)
|
||||
stackView.spacing = 16
|
||||
stackView.isLayoutMarginsRelativeArrangement = true
|
||||
contentScrollView.addSubview(stackView)
|
||||
stackView.autoPinHeightToSuperview()
|
||||
// Pin to the scroll view's viewport, not to its scrollable area
|
||||
stackView.autoPinWidth(toWidthOf: contentScrollView)
|
||||
|
||||
contentScrollView.autoPinEdgesToSuperviewEdges()
|
||||
contentScrollView.alwaysBounceVertical = true
|
||||
|
||||
buildContents()
|
||||
}
|
||||
|
||||
override public func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
if !animationView.isAnimationQueued && !animationView.isAnimationPlaying {
|
||||
animationView.play { [weak self] success in
|
||||
guard success else { return }
|
||||
self?.loopAnimation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override public func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
|
||||
if animationView.isAnimationQueued || animationView.isAnimationPlaying {
|
||||
animationView.stop()
|
||||
}
|
||||
}
|
||||
|
||||
private func loopAnimation() {
|
||||
animationView.play(fromFrame: 60, toFrame: 360, completion: { [weak self] success in
|
||||
guard success else { return }
|
||||
self?.loopAnimation()
|
||||
})
|
||||
}
|
||||
|
||||
private lazy var animationView: AnimationView = {
|
||||
let animationView = AnimationView(name: "safety-numbers")
|
||||
animationView.contentMode = .scaleAspectFit
|
||||
animationView.isUserInteractionEnabled = false
|
||||
animationView.backgroundColor = .white
|
||||
animationView.layer.cornerRadius = 12
|
||||
animationView.layer.masksToBounds = true
|
||||
return animationView
|
||||
}()
|
||||
|
||||
private func buildContents() {
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.textAlignment = .center
|
||||
titleLabel.font = UIFont.dynamicTypeTitle2.semibold()
|
||||
titleLabel.text = OWSLocalizedString(
|
||||
"SAFETY_NUMBER_TRANSITION_SHEET_TITLE",
|
||||
comment: "Title for a sheet informing the user about the transition from phone number to user identifier based."
|
||||
)
|
||||
titleLabel.numberOfLines = 0
|
||||
titleLabel.lineBreakMode = .byWordWrapping
|
||||
stackView.addArrangedSubview(titleLabel)
|
||||
|
||||
let paragraphs: [String] = [
|
||||
OWSLocalizedString(
|
||||
"SAFETY_NUMBER_TRANSITION_SHEET_PARAGRAPH_1",
|
||||
comment: "Informs the user about the transition from phone number to user identifier based."
|
||||
),
|
||||
OWSLocalizedString(
|
||||
"SAFETY_NUMBER_TRANSITION_SHEET_PARAGRAPH_2",
|
||||
comment: "Informs the user about the transition from phone number to user identifier based."
|
||||
)
|
||||
]
|
||||
var lastParagraphLabel: UILabel!
|
||||
for paragraph in paragraphs {
|
||||
let paragraphLabel = UILabel()
|
||||
paragraphLabel.text = paragraph
|
||||
paragraphLabel.textAlignment = .natural
|
||||
paragraphLabel.font = .dynamicTypeSubheadlineClamped
|
||||
paragraphLabel.numberOfLines = 0
|
||||
paragraphLabel.lineBreakMode = .byWordWrapping
|
||||
paragraphLabel.textColor = Theme.secondaryTextAndIconColor
|
||||
stackView.addArrangedSubview(paragraphLabel)
|
||||
lastParagraphLabel = paragraphLabel
|
||||
}
|
||||
stackView.setCustomSpacing(20, after: lastParagraphLabel)
|
||||
|
||||
stackView.addArrangedSubview(animationView)
|
||||
stackView.setCustomSpacing(24, after: animationView)
|
||||
animationView.autoPinWidth(toWidthOf: self.view, offset: -48)
|
||||
animationView.autoMatch(.height, to: .width, of: animationView, withMultiplier: 172/346)
|
||||
|
||||
let learnMoreLabel = UILabel()
|
||||
learnMoreLabel.text = OWSLocalizedString(
|
||||
"SAFETY_NUMBER_TRANSITION_SHEET_HELP_TEXT",
|
||||
comment: "Button text for a sheet informing the user about the transition from phone number to user identifier based."
|
||||
)
|
||||
learnMoreLabel.textAlignment = .center
|
||||
learnMoreLabel.font = .dynamicTypeBody
|
||||
learnMoreLabel.textColor = Theme.isDarkThemeEnabled ? .ows_accentBlueDark : .link
|
||||
learnMoreLabel.isUserInteractionEnabled = true
|
||||
learnMoreLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(didTapLearnMore)))
|
||||
stackView.addArrangedSubview(learnMoreLabel)
|
||||
stackView.setCustomSpacing(30, after: learnMoreLabel)
|
||||
|
||||
let continueButton = OWSButton(
|
||||
title: OWSLocalizedString(
|
||||
"ALERT_ACTION_ACKNOWLEDGE",
|
||||
comment: "generic button text to acknowledge that the corresponding text was read."
|
||||
)
|
||||
) { [weak self] in
|
||||
self?.dismiss(animated: true)
|
||||
}
|
||||
continueButton.layer.cornerRadius = 16
|
||||
continueButton.backgroundColor = .ows_accentBlue
|
||||
continueButton.titleLabel?.font = UIFont.dynamicTypeBody.semibold()
|
||||
continueButton.autoSetDimension(.height, toSize: 50, relation: .greaterThanOrEqual)
|
||||
stackView.addArrangedSubview(continueButton)
|
||||
}
|
||||
|
||||
@objc
|
||||
func didTapLearnMore() {
|
||||
self.dismiss(animated: true) { [weak self] in
|
||||
self?.parentVc?.didTapLearnMore()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Actions
|
||||
|
||||
@objc
|
||||
private func didTapDone() {
|
||||
dismiss(animated: true)
|
||||
}
|
||||
|
||||
private func didTapLearnMore() {
|
||||
let learnMoreUrl = URL(string: "https://support.signal.org/hc/articles/213134107")!
|
||||
let safariVC = SFSafariViewController(url: learnMoreUrl)
|
||||
present(safariVC, animated: true)
|
||||
}
|
||||
|
||||
@objc
|
||||
private func didUpdatePageControl() {
|
||||
self.selectedIndex = fingerprintCarouselPageControl.currentPage
|
||||
scrollToSelectedIndex()
|
||||
}
|
||||
|
||||
@objc
|
||||
private func didTapVerifyUnverify(_ gestureRecognizer: UITapGestureRecognizer) {
|
||||
guard gestureRecognizer.state == .recognized else { return }
|
||||
|
||||
databaseStorage.write { transaction in
|
||||
let isVerified = OWSIdentityManager.shared.verificationState(for: recipientAddress, transaction: transaction) == .verified
|
||||
let newVerificationState: OWSVerificationState = isVerified ? .default : .verified
|
||||
OWSIdentityManager.shared.setVerificationState(
|
||||
newVerificationState,
|
||||
identityKey: identityKey,
|
||||
address: recipientAddress,
|
||||
isUserInitiatedChange: true,
|
||||
transaction: transaction
|
||||
)
|
||||
}
|
||||
|
||||
dismiss(animated: true)
|
||||
}
|
||||
|
||||
private func shareFingerprint(from fromView: UIView) {
|
||||
let fingerprint = fingerprints[selectedIndex]
|
||||
|
||||
Logger.debug("Sharing safety numbers")
|
||||
|
||||
let compareActivity = CompareSafetyNumbersActivity(delegate: self)
|
||||
|
||||
let shareFormat = NSLocalizedString(
|
||||
"SAFETY_NUMBER_SHARE_FORMAT",
|
||||
comment: "Snippet to share {{safety number}} with a friend. sent e.g. via SMS"
|
||||
)
|
||||
let shareString = String(format: shareFormat, fingerprint.displayableText)
|
||||
|
||||
let activityController = UIActivityViewController(
|
||||
activityItems: [ shareString ],
|
||||
applicationActivities: [ compareActivity ]
|
||||
)
|
||||
|
||||
if let popoverPresentationController = activityController.popoverPresentationController {
|
||||
popoverPresentationController.sourceView = fromView
|
||||
}
|
||||
|
||||
// This value was extracted by inspecting `activityType` in the activityController.completionHandler
|
||||
let iCloudActivityType = "com.apple.CloudDocsUI.AddToiCloudDrive"
|
||||
activityController.excludedActivityTypes = [
|
||||
.postToFacebook,
|
||||
.postToWeibo,
|
||||
.airDrop,
|
||||
.postToTwitter,
|
||||
.init(rawValue: iCloudActivityType) // This isn't being excluded. RADAR https://openradar.appspot.com/27493621
|
||||
]
|
||||
|
||||
present(activityController, animated: true)
|
||||
}
|
||||
|
||||
fileprivate func didTapToScan() {
|
||||
let viewController = FingerprintScanViewController(
|
||||
recipientAddress: recipientAddress,
|
||||
recipientIdentity: recipientIdentity,
|
||||
fingerprints: .multiFingerprint(self.fingerprints, defaultIndex: self.selectedIndex)
|
||||
)
|
||||
navigationController?.pushViewController(viewController, animated: true)
|
||||
}
|
||||
|
||||
private func scrollToSelectedIndex(animated: Bool = true) {
|
||||
let xOffset: CGFloat
|
||||
if selectedIndex == 0 {
|
||||
xOffset = 0
|
||||
} else {
|
||||
xOffset = (CGFloat(selectedIndex) * UIScreen.main.bounds.width) - (Constants.interCardSpacing + Constants.cardHInset)
|
||||
}
|
||||
fingerprintCarousel.setContentOffset(.init(x: xOffset, y: 0), animated: animated)
|
||||
}
|
||||
|
||||
// MARK: Notifications
|
||||
|
||||
private var identityStateChangeObserver: Any?
|
||||
|
||||
private func identityStateDidChange() {
|
||||
AssertIsOnMainThread()
|
||||
updateVerificationStateLabel()
|
||||
}
|
||||
|
||||
// MARK: - Constants
|
||||
|
||||
enum Constants {
|
||||
static let cardHInset: CGFloat = .scaleFromIPhone5To7Plus(30, 53)
|
||||
static var interCardSpacing: CGFloat = cardHInset / 2
|
||||
|
||||
// Link doesn't matter, we will override tap behavior.
|
||||
static let transitionLearnMoreUrl = "https://support.signal.org/"
|
||||
static let learnMoreUrl = "https://support.signal.org/learnMore"
|
||||
}
|
||||
}
|
||||
|
||||
extension MultiFingerprintViewController: CompareSafetyNumbersActivityDelegate {
|
||||
|
||||
public func compareSafetyNumbersActivitySucceeded(activity: CompareSafetyNumbersActivity) {
|
||||
FingerprintScanViewController.showVerificationSucceeded(
|
||||
from: self,
|
||||
identityKey: identityKey,
|
||||
recipientAddress: recipientAddress,
|
||||
contactName: contactName,
|
||||
tag: logTag
|
||||
)
|
||||
}
|
||||
|
||||
public func compareSafetyNumbersActivity(_ activity: CompareSafetyNumbersActivity, failedWithError error: Error) {
|
||||
let isUserError = (error as NSError).code == OWSErrorCode.userError.rawValue
|
||||
|
||||
FingerprintScanViewController.showVerificationFailed(
|
||||
from: self,
|
||||
isUserError: isUserError,
|
||||
localizedErrorDescription: error.userErrorDescription,
|
||||
tag: logTag
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension MultiFingerprintViewController: UITextViewDelegate {
|
||||
|
||||
public func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
|
||||
if URL.absoluteString == Constants.transitionLearnMoreUrl {
|
||||
DependenciesBridge.shared.db.write {
|
||||
self.showTransitionSheet($0)
|
||||
}
|
||||
} else if URL.absoluteString == Constants.learnMoreUrl {
|
||||
self.didTapLearnMore()
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
extension MultiFingerprintViewController: UIScrollViewDelegate {
|
||||
|
||||
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
||||
let selectedIndex = Int(scrollView.contentOffset.x / (scrollView.frame.width - (Constants.cardHInset * 2)))
|
||||
self.selectedIndex = selectedIndex
|
||||
self.fingerprintCarouselPageControl.currentPage = selectedIndex
|
||||
}
|
||||
}
|
||||
@ -576,7 +576,7 @@ private class SafetyNumberCell: ContactTableViewCell {
|
||||
|
||||
func configure(item: SafetyNumberConfirmationSheet.Item, theme: Theme.ActionSheet, viewController: UIViewController) {
|
||||
button.setPressedBlock {
|
||||
FingerprintViewController.present(from: viewController, address: item.address)
|
||||
FingerprintViewController.present(for: item.address.aci, from: viewController)
|
||||
}
|
||||
|
||||
Self.databaseStorage.read { transaction in
|
||||
|
||||
Loading…
Reference in New Issue
Block a user