Merge branch 'dev' into push-notification
This commit is contained in:
commit
66678e7b6d
|
@ -11,7 +11,6 @@
|
|||
241C6315231F64CE00B4198E /* CGFloat+Rounding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 241C6312231F5F1D00B4198E /* CGFloat+Rounding.swift */; };
|
||||
241C6316231F64CE00B4198E /* UIColor+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 241C6310231F5C4400B4198E /* UIColor+Helper.swift */; };
|
||||
24A830A22293CD0100F4CAC0 /* LokiP2PServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A830A12293CD0100F4CAC0 /* LokiP2PServer.swift */; };
|
||||
24BD2609234DA2050008EB0A /* NewPublicChatVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24BD2608234DA2050008EB0A /* NewPublicChatVC.swift */; };
|
||||
2AE2882E4C2B96BFFF9EE27C /* Pods_SignalShareExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0F94C85CB0B235DA37F68ED0 /* Pods_SignalShareExtension.framework */; };
|
||||
3403B95D20EA9527001A1F44 /* OWSContactShareButtonsView.m in Sources */ = {isa = PBXBuildFile; fileRef = 3403B95B20EA9526001A1F44 /* OWSContactShareButtonsView.m */; };
|
||||
34074F61203D0CBE004596AE /* OWSSounds.m in Sources */ = {isa = PBXBuildFile; fileRef = 34074F5F203D0CBD004596AE /* OWSSounds.m */; };
|
||||
|
@ -566,31 +565,57 @@
|
|||
B80C6B5B2384C7F900FDBC8B /* DeviceNameModalDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B80C6B5A2384C7F900FDBC8B /* DeviceNameModalDelegate.swift */; };
|
||||
B8162F0322891AD600D46544 /* FriendRequestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8162F0222891AD600D46544 /* FriendRequestView.swift */; };
|
||||
B8162F0522892C5F00D46544 /* FriendRequestViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8162F0422892C5F00D46544 /* FriendRequestViewDelegate.swift */; };
|
||||
B821F2F82272CED3002C88C0 /* DisplayNameVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B821F2F72272CED3002C88C0 /* DisplayNameVC.swift */; };
|
||||
B821F2FA2272CEEE002C88C0 /* SeedVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B821F2F92272CEEE002C88C0 /* SeedVC.swift */; };
|
||||
B82584A02315024B001B41CB /* LokiRSSFeedPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = B825849F2315024B001B41CB /* LokiRSSFeedPoller.swift */; };
|
||||
B82B40882399EB0E00A248E7 /* LandingVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B82B40872399EB0E00A248E7 /* LandingVC.swift */; };
|
||||
B82B408A2399EC0600A248E7 /* FakeChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B82B40892399EC0600A248E7 /* FakeChatView.swift */; };
|
||||
B82B408C239A068800A248E7 /* RegisterVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B82B408B239A068800A248E7 /* RegisterVC.swift */; };
|
||||
B82B408E239DC00D00A248E7 /* DisplayNameVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B82B408D239DC00D00A248E7 /* DisplayNameVC.swift */; };
|
||||
B82B4090239DD75000A248E7 /* RestoreVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B82B408F239DD75000A248E7 /* RestoreVC.swift */; };
|
||||
B82B4094239DF15900A248E7 /* ConversationTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B82B4093239DF15900A248E7 /* ConversationTitleView.swift */; };
|
||||
B846365B22B7418B00AF1514 /* Identicon+ObjC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B846365A22B7418B00AF1514 /* Identicon+ObjC.swift */; };
|
||||
B84664F5235022F30083A1CD /* MentionUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = B84664F4235022F30083A1CD /* MentionUtilities.swift */; };
|
||||
B86BD08123399883000F5AE3 /* QRCodeModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B86BD08023399883000F5AE3 /* QRCodeModal.swift */; };
|
||||
B85357BF23A1AE0800AAF6CD /* SeedReminderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B85357BE23A1AE0800AAF6CD /* SeedReminderView.swift */; };
|
||||
B85357C123A1B81900AAF6CD /* SeedReminderViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B85357C023A1B81900AAF6CD /* SeedReminderViewDelegate.swift */; };
|
||||
B85357C323A1BD1200AAF6CD /* SeedVCV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = B85357C223A1BD1200AAF6CD /* SeedVCV2.swift */; };
|
||||
B85357C523A1F13800AAF6CD /* LinkDeviceVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B85357C423A1F13800AAF6CD /* LinkDeviceVC.swift */; };
|
||||
B85357C723A1FB5100AAF6CD /* LinkDeviceVCDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B85357C623A1FB5100AAF6CD /* LinkDeviceVCDelegate.swift */; };
|
||||
B86BD08423399ACF000F5AE3 /* Modal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B86BD08323399ACF000F5AE3 /* Modal.swift */; };
|
||||
B86BD08623399CEF000F5AE3 /* SeedModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B86BD08523399CEF000F5AE3 /* SeedModal.swift */; };
|
||||
B885D5F4233491AB00EE0D8E /* DeviceLinkingModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B885D5F3233491AB00EE0D8E /* DeviceLinkingModal.swift */; };
|
||||
B885D5F62334A32100EE0D8E /* UIView+Constraint.swift in Sources */ = {isa = PBXBuildFile; fileRef = B885D5F52334A32100EE0D8E /* UIView+Constraint.swift */; };
|
||||
B885D5F62334A32100EE0D8E /* UIView+Constraints.swift in Sources */ = {isa = PBXBuildFile; fileRef = B885D5F52334A32100EE0D8E /* UIView+Constraints.swift */; };
|
||||
B886B4A72398B23E00211ABE /* QRCodeVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B886B4A62398B23E00211ABE /* QRCodeVC.swift */; };
|
||||
B886B4A92398BA1500211ABE /* QRCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = B886B4A82398BA1500211ABE /* QRCode.swift */; };
|
||||
B891105C2320872800F15FCC /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = B891105B2320872800F15FCC /* GoogleService-Info.plist */; };
|
||||
B891105E2320872800F15FCC /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = B891105B2320872800F15FCC /* GoogleService-Info.plist */; };
|
||||
B891105F2320872800F15FCC /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = B891105B2320872800F15FCC /* GoogleService-Info.plist */; };
|
||||
B893063F2383961A005EAA8E /* ScanQRCodeWrapperVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B893063E2383961A005EAA8E /* ScanQRCodeWrapperVC.swift */; };
|
||||
B894D0712339D6F300B4D94D /* DeviceLinkingModalDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B894D0702339D6F300B4D94D /* DeviceLinkingModalDelegate.swift */; };
|
||||
B894D0752339EDCF00B4D94D /* NukeDataModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B894D0742339EDCF00B4D94D /* NukeDataModal.swift */; };
|
||||
B89841E322B7579F00B1BDC6 /* NewConversationVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B89841E222B7579F00B1BDC6 /* NewConversationVC.swift */; };
|
||||
B8B26C8F234D629C004ED98C /* MentionCandidateSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B26C8E234D629C004ED98C /* MentionCandidateSelectionView.swift */; };
|
||||
B8B26C91234D8CBD004ED98C /* MentionCandidateSelectionViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B26C90234D8CBD004ED98C /* MentionCandidateSelectionViewDelegate.swift */; };
|
||||
B8B5BCEC2394D869003823C9 /* Button.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B5BCEB2394D869003823C9 /* Button.swift */; };
|
||||
B8BB82A0238F322400BA5194 /* Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BB829F238F322400BA5194 /* Colors.swift */; };
|
||||
B8BB82A2238F356100BA5194 /* Values.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BB82A1238F356100BA5194 /* Values.swift */; };
|
||||
B8BB82A5238F627000BA5194 /* HomeVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BB82A4238F627000BA5194 /* HomeVC.swift */; };
|
||||
B8BB82A9238F62FB00BA5194 /* Gradients.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BB82A8238F62FB00BA5194 /* Gradients.swift */; };
|
||||
B8BB82AB238F669C00BA5194 /* ConversationCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BB82AA238F669C00BA5194 /* ConversationCell.swift */; };
|
||||
B8BB82AD238F734800BA5194 /* ProfilePictureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BB82AC238F734800BA5194 /* ProfilePictureView.swift */; };
|
||||
B8BB82B12390C37000BA5194 /* SearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BB82B02390C37000BA5194 /* SearchBar.swift */; };
|
||||
B8BB82B523947F2D00BA5194 /* TextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BB82B423947F2D00BA5194 /* TextField.swift */; };
|
||||
B8BB82B92394911B00BA5194 /* Separator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BB82B82394911B00BA5194 /* Separator.swift */; };
|
||||
B8BB82BE2394D4CE00BA5194 /* Fonts.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BB82BD2394D4CE00BA5194 /* Fonts.swift */; };
|
||||
B8CCF6352396005F0091D419 /* SpaceMono-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = B8CCF6342396005F0091D419 /* SpaceMono-Regular.ttf */; };
|
||||
B8CCF63723961D6D0091D419 /* NewPrivateChatVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8CCF63623961D6D0091D419 /* NewPrivateChatVC.swift */; };
|
||||
B8CCF639239721E20091D419 /* TabBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8CCF638239721E20091D419 /* TabBar.swift */; };
|
||||
B8CCF63F23975CFB0091D419 /* JoinPublicChatVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8CCF63E23975CFB0091D419 /* JoinPublicChatVC.swift */; };
|
||||
B8CCF6432397711F0091D419 /* SettingsVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8CCF6422397711F0091D419 /* SettingsVC.swift */; };
|
||||
B90418E6183E9DD40038554A /* DateUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = B90418E5183E9DD40038554A /* DateUtil.m */; };
|
||||
B9EB5ABD1884C002007CBB57 /* MessageUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B9EB5ABC1884C002007CBB57 /* MessageUI.framework */; };
|
||||
BFF3FB9730634F37D25903F4 /* Pods_Signal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D17BB5C25D615AB49813100C /* Pods_Signal.framework */; };
|
||||
C32B08E82322170B007FD6E5 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = C32B08E72322170B007FD6E5 /* GoogleService-Info.plist */; };
|
||||
C32B08E92322170B007FD6E5 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = C32B08E72322170B007FD6E5 /* GoogleService-Info.plist */; };
|
||||
C32B08EA2322170B007FD6E5 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = C32B08E72322170B007FD6E5 /* GoogleService-Info.plist */; };
|
||||
C34C8F7423A7830B00D82669 /* SpaceMono-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = C34C8F7323A7830A00D82669 /* SpaceMono-Bold.ttf */; };
|
||||
CC875800737563D6891B741D /* Pods_SignalTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 748A5CAEDD7C919FC64C6807 /* Pods_SignalTests.framework */; };
|
||||
D202868116DBE0E7009068E9 /* CFNetwork.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2AEACDB16C426DA00C364C0 /* CFNetwork.framework */; };
|
||||
D202868216DBE0F4009068E9 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2179CFD16BB0B480006F3AB /* SystemConfiguration.framework */; };
|
||||
|
@ -686,7 +711,6 @@
|
|||
241C6310231F5C4400B4198E /* UIColor+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Helper.swift"; sourceTree = "<group>"; };
|
||||
241C6312231F5F1D00B4198E /* CGFloat+Rounding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGFloat+Rounding.swift"; sourceTree = "<group>"; };
|
||||
24A830A12293CD0100F4CAC0 /* LokiP2PServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LokiP2PServer.swift; sourceTree = "<group>"; };
|
||||
24BD2608234DA2050008EB0A /* NewPublicChatVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewPublicChatVC.swift; sourceTree = "<group>"; };
|
||||
264242150E87D10A357DB07B /* Pods_SignalMessaging.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SignalMessaging.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
3403B95B20EA9526001A1F44 /* OWSContactShareButtonsView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSContactShareButtonsView.m; sourceTree = "<group>"; };
|
||||
3403B95C20EA9527001A1F44 /* OWSContactShareButtonsView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSContactShareButtonsView.h; sourceTree = "<group>"; };
|
||||
|
@ -1381,29 +1405,55 @@
|
|||
B80C6B5A2384C7F900FDBC8B /* DeviceNameModalDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceNameModalDelegate.swift; sourceTree = "<group>"; };
|
||||
B8162F0222891AD600D46544 /* FriendRequestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FriendRequestView.swift; sourceTree = "<group>"; };
|
||||
B8162F0422892C5F00D46544 /* FriendRequestViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FriendRequestViewDelegate.swift; sourceTree = "<group>"; };
|
||||
B821F2F72272CED3002C88C0 /* DisplayNameVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayNameVC.swift; sourceTree = "<group>"; };
|
||||
B821F2F92272CEEE002C88C0 /* SeedVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedVC.swift; sourceTree = "<group>"; };
|
||||
B825849F2315024B001B41CB /* LokiRSSFeedPoller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LokiRSSFeedPoller.swift; sourceTree = "<group>"; };
|
||||
B82B40872399EB0E00A248E7 /* LandingVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LandingVC.swift; sourceTree = "<group>"; };
|
||||
B82B40892399EC0600A248E7 /* FakeChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakeChatView.swift; sourceTree = "<group>"; };
|
||||
B82B408B239A068800A248E7 /* RegisterVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisterVC.swift; sourceTree = "<group>"; };
|
||||
B82B408D239DC00D00A248E7 /* DisplayNameVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayNameVC.swift; sourceTree = "<group>"; };
|
||||
B82B408F239DD75000A248E7 /* RestoreVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestoreVC.swift; sourceTree = "<group>"; };
|
||||
B82B4093239DF15900A248E7 /* ConversationTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationTitleView.swift; sourceTree = "<group>"; };
|
||||
B846365A22B7418B00AF1514 /* Identicon+ObjC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Identicon+ObjC.swift"; sourceTree = "<group>"; };
|
||||
B84664F4235022F30083A1CD /* MentionUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionUtilities.swift; sourceTree = "<group>"; };
|
||||
B86BD08023399883000F5AE3 /* QRCodeModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeModal.swift; sourceTree = "<group>"; };
|
||||
B85357BE23A1AE0800AAF6CD /* SeedReminderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedReminderView.swift; sourceTree = "<group>"; };
|
||||
B85357C023A1B81900AAF6CD /* SeedReminderViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedReminderViewDelegate.swift; sourceTree = "<group>"; };
|
||||
B85357C223A1BD1200AAF6CD /* SeedVCV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedVCV2.swift; sourceTree = "<group>"; };
|
||||
B85357C423A1F13800AAF6CD /* LinkDeviceVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkDeviceVC.swift; sourceTree = "<group>"; };
|
||||
B85357C623A1FB5100AAF6CD /* LinkDeviceVCDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkDeviceVCDelegate.swift; sourceTree = "<group>"; };
|
||||
B86BD08323399ACF000F5AE3 /* Modal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modal.swift; sourceTree = "<group>"; };
|
||||
B86BD08523399CEF000F5AE3 /* SeedModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedModal.swift; sourceTree = "<group>"; };
|
||||
B885D5F3233491AB00EE0D8E /* DeviceLinkingModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceLinkingModal.swift; sourceTree = "<group>"; };
|
||||
B885D5F52334A32100EE0D8E /* UIView+Constraint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Constraint.swift"; sourceTree = "<group>"; };
|
||||
B885D5F52334A32100EE0D8E /* UIView+Constraints.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Constraints.swift"; sourceTree = "<group>"; };
|
||||
B886B4A62398B23E00211ABE /* QRCodeVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeVC.swift; sourceTree = "<group>"; };
|
||||
B886B4A82398BA1500211ABE /* QRCode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCode.swift; sourceTree = "<group>"; };
|
||||
B891105B2320872800F15FCC /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
|
||||
B893063E2383961A005EAA8E /* ScanQRCodeWrapperVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanQRCodeWrapperVC.swift; sourceTree = "<group>"; };
|
||||
B894D0702339D6F300B4D94D /* DeviceLinkingModalDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceLinkingModalDelegate.swift; sourceTree = "<group>"; };
|
||||
B894D0742339EDCF00B4D94D /* NukeDataModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NukeDataModal.swift; sourceTree = "<group>"; };
|
||||
B89841E222B7579F00B1BDC6 /* NewConversationVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewConversationVC.swift; sourceTree = "<group>"; };
|
||||
B8B26C8E234D629C004ED98C /* MentionCandidateSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionCandidateSelectionView.swift; sourceTree = "<group>"; };
|
||||
B8B26C90234D8CBD004ED98C /* MentionCandidateSelectionViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionCandidateSelectionViewDelegate.swift; sourceTree = "<group>"; };
|
||||
B8B5BCEB2394D869003823C9 /* Button.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Button.swift; sourceTree = "<group>"; };
|
||||
B8BB829F238F322400BA5194 /* Colors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Colors.swift; sourceTree = "<group>"; };
|
||||
B8BB82A1238F356100BA5194 /* Values.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Values.swift; sourceTree = "<group>"; };
|
||||
B8BB82A4238F627000BA5194 /* HomeVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeVC.swift; sourceTree = "<group>"; };
|
||||
B8BB82A8238F62FB00BA5194 /* Gradients.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Gradients.swift; sourceTree = "<group>"; };
|
||||
B8BB82AA238F669C00BA5194 /* ConversationCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationCell.swift; sourceTree = "<group>"; };
|
||||
B8BB82AC238F734800BA5194 /* ProfilePictureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePictureView.swift; sourceTree = "<group>"; };
|
||||
B8BB82B02390C37000BA5194 /* SearchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBar.swift; sourceTree = "<group>"; };
|
||||
B8BB82B423947F2D00BA5194 /* TextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextField.swift; sourceTree = "<group>"; };
|
||||
B8BB82B82394911B00BA5194 /* Separator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Separator.swift; sourceTree = "<group>"; };
|
||||
B8BB82BD2394D4CE00BA5194 /* Fonts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fonts.swift; sourceTree = "<group>"; };
|
||||
B8CCF6342396005F0091D419 /* SpaceMono-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SpaceMono-Regular.ttf"; sourceTree = "<group>"; };
|
||||
B8CCF63623961D6D0091D419 /* NewPrivateChatVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewPrivateChatVC.swift; sourceTree = "<group>"; };
|
||||
B8CCF638239721E20091D419 /* TabBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBar.swift; sourceTree = "<group>"; };
|
||||
B8CCF63E23975CFB0091D419 /* JoinPublicChatVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinPublicChatVC.swift; sourceTree = "<group>"; };
|
||||
B8CCF6422397711F0091D419 /* SettingsVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsVC.swift; sourceTree = "<group>"; };
|
||||
B90418E4183E9DD40038554A /* DateUtil.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DateUtil.h; sourceTree = "<group>"; };
|
||||
B90418E5183E9DD40038554A /* DateUtil.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DateUtil.m; sourceTree = "<group>"; };
|
||||
B97940251832BD2400BD66CB /* UIUtil.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UIUtil.h; sourceTree = "<group>"; };
|
||||
B97940261832BD2400BD66CB /* UIUtil.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UIUtil.m; sourceTree = "<group>"; };
|
||||
B9EB5ABC1884C002007CBB57 /* MessageUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MessageUI.framework; path = System/Library/Frameworks/MessageUI.framework; sourceTree = SDKROOT; };
|
||||
C32B08E72322170B007FD6E5 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
|
||||
C34C8F7323A7830A00D82669 /* SpaceMono-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SpaceMono-Bold.ttf"; sourceTree = "<group>"; };
|
||||
D17BB5C25D615AB49813100C /* Pods_Signal.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Signal.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
D2179CFB16BB0B3A0006F3AB /* CoreTelephony.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreTelephony.framework; path = System/Library/Frameworks/CoreTelephony.framework; sourceTree = SDKROOT; };
|
||||
D2179CFD16BB0B480006F3AB /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; };
|
||||
|
@ -1657,6 +1707,7 @@
|
|||
34330A5B1E787A9800DF2FB9 /* dripicons-v2.ttf */,
|
||||
34330A5D1E787BD800DF2FB9 /* ElegantIcons.ttf */,
|
||||
34330A591E7875FB00DF2FB9 /* fontawesome-webfont.ttf */,
|
||||
B8BB82BA2394D47000BA5194 /* Loki */,
|
||||
);
|
||||
path = Fonts;
|
||||
sourceTree = "<group>";
|
||||
|
@ -2663,10 +2714,11 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
7B53C4EE239E0343008DF635 /* PushNotification */,
|
||||
B8CCF63B239757C10091D419 /* Components */,
|
||||
B8BFFF392355426100102A27 /* Messaging */,
|
||||
B86BD0872339A1ED000F5AE3 /* Onboarding */,
|
||||
B86BD08223399ABF000F5AE3 /* Settings */,
|
||||
B86BD0882339A253000F5AE3 /* Utilities */,
|
||||
B8BB82A3238F356800BA5194 /* Style Guide */,
|
||||
B8CCF63C239757DB0091D419 /* Utilities */,
|
||||
B8CCF63D2397580E0091D419 /* View Controllers */,
|
||||
);
|
||||
path = Loki;
|
||||
sourceTree = "<group>";
|
||||
|
@ -2682,7 +2734,67 @@
|
|||
path = Loki;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B86BD08223399ABF000F5AE3 /* Settings */ = {
|
||||
B8BB82A3238F356800BA5194 /* Style Guide */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B8BB829F238F322400BA5194 /* Colors.swift */,
|
||||
B8BB82BD2394D4CE00BA5194 /* Fonts.swift */,
|
||||
B8BB82A8238F62FB00BA5194 /* Gradients.swift */,
|
||||
B8BB82A1238F356100BA5194 /* Values.swift */,
|
||||
);
|
||||
path = "Style Guide";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B8BB82BA2394D47000BA5194 /* Loki */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B8CCF6342396005F0091D419 /* SpaceMono-Regular.ttf */,
|
||||
);
|
||||
path = Loki;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B8BFFF392355426100102A27 /* Messaging */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
24A830A12293CD0100F4CAC0 /* LokiP2PServer.swift */,
|
||||
B825849F2315024B001B41CB /* LokiRSSFeedPoller.swift */,
|
||||
);
|
||||
path = Messaging;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B8CCF63B239757C10091D419 /* Components */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B8B5BCEB2394D869003823C9 /* Button.swift */,
|
||||
B8BB82AA238F669C00BA5194 /* ConversationCell.swift */,
|
||||
B82B4093239DF15900A248E7 /* ConversationTitleView.swift */,
|
||||
B82B40892399EC0600A248E7 /* FakeChatView.swift */,
|
||||
B8162F0222891AD600D46544 /* FriendRequestView.swift */,
|
||||
B8162F0422892C5F00D46544 /* FriendRequestViewDelegate.swift */,
|
||||
B8B26C8E234D629C004ED98C /* MentionCandidateSelectionView.swift */,
|
||||
B8B26C90234D8CBD004ED98C /* MentionCandidateSelectionViewDelegate.swift */,
|
||||
B8BB82AC238F734800BA5194 /* ProfilePictureView.swift */,
|
||||
B8BB82B02390C37000BA5194 /* SearchBar.swift */,
|
||||
B85357BE23A1AE0800AAF6CD /* SeedReminderView.swift */,
|
||||
B85357C023A1B81900AAF6CD /* SeedReminderViewDelegate.swift */,
|
||||
B8BB82B82394911B00BA5194 /* Separator.swift */,
|
||||
B8CCF638239721E20091D419 /* TabBar.swift */,
|
||||
B8BB82B423947F2D00BA5194 /* TextField.swift */,
|
||||
);
|
||||
path = Components;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B8CCF63C239757DB0091D419 /* Utilities */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B84664F4235022F30083A1CD /* MentionUtilities.swift */,
|
||||
B885D5F52334A32100EE0D8E /* UIView+Constraints.swift */,
|
||||
B886B4A82398BA1500211ABE /* QRCode.swift */,
|
||||
);
|
||||
path = Utilities;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B8CCF63D2397580E0091D419 /* View Controllers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B885D5F3233491AB00EE0D8E /* DeviceLinkingModal.swift */,
|
||||
|
@ -2690,46 +2802,24 @@
|
|||
B80C6B562384A56D00FDBC8B /* DeviceLinksVC.swift */,
|
||||
B80C6B582384C4E700FDBC8B /* DeviceNameModal.swift */,
|
||||
B80C6B5A2384C7F900FDBC8B /* DeviceNameModalDelegate.swift */,
|
||||
B894D0742339EDCF00B4D94D /* NukeDataModal.swift */,
|
||||
B86BD08023399883000F5AE3 /* QRCodeModal.swift */,
|
||||
B86BD08523399CEF000F5AE3 /* SeedModal.swift */,
|
||||
);
|
||||
path = Settings;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B86BD0872339A1ED000F5AE3 /* Onboarding */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B821F2F72272CED3002C88C0 /* DisplayNameVC.swift */,
|
||||
B821F2F92272CEEE002C88C0 /* SeedVC.swift */,
|
||||
);
|
||||
path = Onboarding;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B86BD0882339A253000F5AE3 /* Utilities */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B84664F4235022F30083A1CD /* MentionUtilities.swift */,
|
||||
B82B408D239DC00D00A248E7 /* DisplayNameVC.swift */,
|
||||
B8BB82A4238F627000BA5194 /* HomeVC.swift */,
|
||||
B8CCF63E23975CFB0091D419 /* JoinPublicChatVC.swift */,
|
||||
B82B40872399EB0E00A248E7 /* LandingVC.swift */,
|
||||
B85357C423A1F13800AAF6CD /* LinkDeviceVC.swift */,
|
||||
B85357C623A1FB5100AAF6CD /* LinkDeviceVCDelegate.swift */,
|
||||
B86BD08323399ACF000F5AE3 /* Modal.swift */,
|
||||
B885D5F52334A32100EE0D8E /* UIView+Constraint.swift */,
|
||||
);
|
||||
path = Utilities;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B8BFFF392355426100102A27 /* Messaging */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B8162F0222891AD600D46544 /* FriendRequestView.swift */,
|
||||
B8162F0422892C5F00D46544 /* FriendRequestViewDelegate.swift */,
|
||||
24A830A12293CD0100F4CAC0 /* LokiP2PServer.swift */,
|
||||
B825849F2315024B001B41CB /* LokiRSSFeedPoller.swift */,
|
||||
B8B26C8E234D629C004ED98C /* MentionCandidateSelectionView.swift */,
|
||||
B8B26C90234D8CBD004ED98C /* MentionCandidateSelectionViewDelegate.swift */,
|
||||
B89841E222B7579F00B1BDC6 /* NewConversationVC.swift */,
|
||||
24BD2608234DA2050008EB0A /* NewPublicChatVC.swift */,
|
||||
B8CCF63623961D6D0091D419 /* NewPrivateChatVC.swift */,
|
||||
B894D0742339EDCF00B4D94D /* NukeDataModal.swift */,
|
||||
B886B4A62398B23E00211ABE /* QRCodeVC.swift */,
|
||||
B82B408B239A068800A248E7 /* RegisterVC.swift */,
|
||||
B82B408F239DD75000A248E7 /* RestoreVC.swift */,
|
||||
B893063E2383961A005EAA8E /* ScanQRCodeWrapperVC.swift */,
|
||||
B86BD08523399CEF000F5AE3 /* SeedModal.swift */,
|
||||
B85357C223A1BD1200AAF6CD /* SeedVCV2.swift */,
|
||||
B8CCF6422397711F0091D419 /* SettingsVC.swift */,
|
||||
);
|
||||
path = Messaging;
|
||||
path = "View Controllers";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D221A07E169C9E5E00537ABF = {
|
||||
|
@ -2808,6 +2898,7 @@
|
|||
B67EBF5C19194AC60084CCFD /* Settings.bundle */,
|
||||
B657DDC91911A40500F45B0C /* Signal.entitlements */,
|
||||
C32B08E72322170B007FD6E5 /* GoogleService-Info.plist */,
|
||||
C34C8F7323A7830A00D82669 /* SpaceMono-Bold.ttf */,
|
||||
34074F54203D0722004596AE /* Sounds */,
|
||||
76EB03C118170B33006006FC /* src */,
|
||||
D221A094169C9E5E00537ABF /* Supporting Files */,
|
||||
|
@ -3241,9 +3332,11 @@
|
|||
45B74A812044AAB600CD42F8 /* chord-quiet.aifc in Resources */,
|
||||
45B74A832044AAB600CD42F8 /* circles.aifc in Resources */,
|
||||
45B74A892044AAB600CD42F8 /* circles-quiet.aifc in Resources */,
|
||||
C34C8F7423A7830B00D82669 /* SpaceMono-Bold.ttf in Resources */,
|
||||
4503F1BF20470A5B00CEE724 /* classic.aifc in Resources */,
|
||||
4503F1BE20470A5B00CEE724 /* classic-quiet.aifc in Resources */,
|
||||
45B74A7E2044AAB600CD42F8 /* complete.aifc in Resources */,
|
||||
B8CCF6352396005F0091D419 /* SpaceMono-Regular.ttf in Resources */,
|
||||
45B74A872044AAB600CD42F8 /* complete-quiet.aifc in Resources */,
|
||||
45B74A772044AAB600CD42F8 /* hello.aifc in Resources */,
|
||||
4C61819F219E1796009BD6B5 /* typing-animation-dark.gif in Resources */,
|
||||
|
@ -3733,6 +3826,7 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
B8CCF63723961D6D0091D419 /* NewPrivateChatVC.swift in Sources */,
|
||||
4CC0B59C20EC5F2E00CF6EE0 /* ConversationConfigurationSyncOperation.swift in Sources */,
|
||||
3461293E1FD1D72B00532771 /* ExperienceUpgradeFinder.swift in Sources */,
|
||||
34C4E2582118957600BEA353 /* WebRTCProto.swift in Sources */,
|
||||
|
@ -3764,22 +3858,26 @@
|
|||
348570A820F67575004FF32B /* OWSMessageHeaderView.m in Sources */,
|
||||
450DF2091E0DD2C6003D14BE /* UserNotificationsAdaptee.swift in Sources */,
|
||||
34B6A907218B5241007C4606 /* TypingIndicatorCell.swift in Sources */,
|
||||
B86BD08123399883000F5AE3 /* QRCodeModal.swift in Sources */,
|
||||
4CFD151D22415AA400F2450F /* CallVideoHintView.swift in Sources */,
|
||||
34D1F0AB1F867BFC0066283D /* OWSContactOffersCell.m in Sources */,
|
||||
B82B408A2399EC0600A248E7 /* FakeChatView.swift in Sources */,
|
||||
B8BB82B92394911B00BA5194 /* Separator.swift in Sources */,
|
||||
343A65981FC4CFE7000477A1 /* ConversationScrollButton.m in Sources */,
|
||||
B82B40882399EB0E00A248E7 /* LandingVC.swift in Sources */,
|
||||
34386A51207D0C01009F5D9C /* HomeViewController.m in Sources */,
|
||||
34D1F0A91F867BFC0066283D /* ConversationViewCell.m in Sources */,
|
||||
34A4C62022175C5C0042EF2E /* OnboardingProfileViewController.swift in Sources */,
|
||||
4505C2BF1E648EA300CEBF41 /* ExperienceUpgrade.swift in Sources */,
|
||||
EF764C351DB67CC5000D9A87 /* UIViewController+Permissions.m in Sources */,
|
||||
45CD81EF1DC030E7004C9430 /* SyncPushTokensJob.swift in Sources */,
|
||||
B8BB82A0238F322400BA5194 /* Colors.swift in Sources */,
|
||||
34D2CCE0206939B400CB1A14 /* DebugUIMessagesAssetLoader.m in Sources */,
|
||||
4CEB78C92178EBAB00F315D2 /* OWSSessionResetJobRecord.m in Sources */,
|
||||
45794E861E00620000066731 /* CallUIAdapter.swift in Sources */,
|
||||
340FC8BA204DAC8D007AEB0F /* FingerprintViewScanController.m in Sources */,
|
||||
4585C4681ED8F8D200896AEA /* SafetyNumberConfirmationAlert.swift in Sources */,
|
||||
4C20B2B920CA10DE001BAC90 /* ConversationSearchViewController.swift in Sources */,
|
||||
B85357C123A1B81900AAF6CD /* SeedReminderViewDelegate.swift in Sources */,
|
||||
450D19131F85236600970622 /* RemoteVideoView.m in Sources */,
|
||||
34129B8621EF877A005457A8 /* LinkPreviewView.swift in Sources */,
|
||||
34386A54207D271D009F5D9C /* NeverClearView.swift in Sources */,
|
||||
|
@ -3797,6 +3895,7 @@
|
|||
34B3F8931E8DF1710035BE1A /* SignalsNavigationController.m in Sources */,
|
||||
34F308A21ECB469700BB7697 /* OWSBezierPathView.m in Sources */,
|
||||
45B27B862037FFB400A539DF /* DebugUIFileBrowser.swift in Sources */,
|
||||
B886B4A92398BA1500211ABE /* QRCode.swift in Sources */,
|
||||
3496955D219B605E00DCFE74 /* PhotoCollectionPickerController.swift in Sources */,
|
||||
34CE88E71F2FB9A10098030F /* ProfileViewController.m in Sources */,
|
||||
3403B95D20EA9527001A1F44 /* OWSContactShareButtonsView.m in Sources */,
|
||||
|
@ -3805,6 +3904,8 @@
|
|||
B80C6B572384A56D00FDBC8B /* DeviceLinksVC.swift in Sources */,
|
||||
34A8B3512190A40E00218A25 /* MediaAlbumCellView.swift in Sources */,
|
||||
34D1F0AE1F867BFC0066283D /* OWSMessageCell.m in Sources */,
|
||||
B8BB82A2238F356100BA5194 /* Values.swift in Sources */,
|
||||
B8BB82AB238F669C00BA5194 /* ConversationCell.swift in Sources */,
|
||||
4C4AEC4520EC343B0020E72B /* DismissableTextField.swift in Sources */,
|
||||
4CB5F26720F6E1E2004D1B42 /* MenuActionsViewController.swift in Sources */,
|
||||
3496955E219B605E00DCFE74 /* PhotoLibrary.swift in Sources */,
|
||||
|
@ -3817,6 +3918,7 @@
|
|||
D221A09A169C9E5E00537ABF /* main.m in Sources */,
|
||||
3496957221A301A100DCFE74 /* OWSBackup.m in Sources */,
|
||||
B86BD08623399CEF000F5AE3 /* SeedModal.swift in Sources */,
|
||||
B8BB82A9238F62FB00BA5194 /* Gradients.swift in Sources */,
|
||||
34B3F87B1E8DF1700035BE1A /* ExperienceUpgradesPageViewController.swift in Sources */,
|
||||
3448E1622213585C004B052E /* OnboardingBaseViewController.swift in Sources */,
|
||||
34E5DC8220D8050D00C08145 /* RegistrationUtils.m in Sources */,
|
||||
|
@ -3829,6 +3931,7 @@
|
|||
4CC613362227A00400E21A3A /* ConversationSearch.swift in Sources */,
|
||||
340FC8B2204DAC8D007AEB0F /* AdvancedSettingsTableViewController.m in Sources */,
|
||||
452B999020A34B6B006F2F9E /* AddContactShareToExistingContactViewController.swift in Sources */,
|
||||
B82B408C239A068800A248E7 /* RegisterVC.swift in Sources */,
|
||||
346129991FD1E4DA00532771 /* SignalApp.m in Sources */,
|
||||
3496957121A301A100DCFE74 /* OWSBackupImportJob.m in Sources */,
|
||||
34BECE301F7ABCF800D7438D /* GifPickerLayout.swift in Sources */,
|
||||
|
@ -3838,6 +3941,7 @@
|
|||
4556FA681F54AA9500AF40DD /* DebugUIProfile.swift in Sources */,
|
||||
45A6DAD61EBBF85500893231 /* ReminderView.swift in Sources */,
|
||||
34D1F0881F8678AA0066283D /* ConversationViewLayout.m in Sources */,
|
||||
B82B408E239DC00D00A248E7 /* DisplayNameVC.swift in Sources */,
|
||||
4CA485BB2232339F004B9E7D /* PhotoCaptureViewController.swift in Sources */,
|
||||
34330AA31E79686200DF2FB9 /* OWSProgressView.m in Sources */,
|
||||
344825C6211390C800DB4BD8 /* OWSOrphanDataCleaner.m in Sources */,
|
||||
|
@ -3846,21 +3950,26 @@
|
|||
34D5CCA91EAE3D30005515DB /* AvatarViewHelper.m in Sources */,
|
||||
34D1F0B71F87F8850066283D /* OWSGenericAttachmentView.m in Sources */,
|
||||
3448E15C22133274004B052E /* OnboardingPermissionsViewController.swift in Sources */,
|
||||
B8B5BCEC2394D869003823C9 /* Button.swift in Sources */,
|
||||
34D920E720E179C200D51158 /* OWSMessageFooterView.m in Sources */,
|
||||
341341EF2187467A00192D59 /* ConversationViewModel.m in Sources */,
|
||||
B8BB82B12390C37000BA5194 /* SearchBar.swift in Sources */,
|
||||
348BB25D20A0C5530047AEC2 /* ContactShareViewHelper.swift in Sources */,
|
||||
34B3F8801E8DF1700035BE1A /* InviteFlow.swift in Sources */,
|
||||
B85357C523A1F13800AAF6CD /* LinkDeviceVC.swift in Sources */,
|
||||
457C87B82032645C008D52D6 /* DebugUINotifications.swift in Sources */,
|
||||
4C21D5D8223AC60F00EF8A77 /* PhotoCapture.swift in Sources */,
|
||||
4C13C9F620E57BA30089A98B /* ColorPickerViewController.swift in Sources */,
|
||||
B8162F0522892C5F00D46544 /* FriendRequestViewDelegate.swift in Sources */,
|
||||
B821F2FA2272CEEE002C88C0 /* SeedVC.swift in Sources */,
|
||||
4CC1ECFB211A553000CC13BE /* AppUpdateNag.swift in Sources */,
|
||||
3448E16022134C89004B052E /* OnboardingSplashViewController.swift in Sources */,
|
||||
34B6A903218B3F63007C4606 /* TypingIndicatorView.swift in Sources */,
|
||||
B8BB82AD238F734800BA5194 /* ProfilePictureView.swift in Sources */,
|
||||
B8CCF639239721E20091D419 /* TabBar.swift in Sources */,
|
||||
B8162F0322891AD600D46544 /* FriendRequestView.swift in Sources */,
|
||||
458E38371D668EBF0094BD24 /* OWSDeviceProvisioningURLParser.m in Sources */,
|
||||
34B6A905218B4C91007C4606 /* TypingIndicatorInteraction.swift in Sources */,
|
||||
B886B4A72398B23E00211ABE /* QRCodeVC.swift in Sources */,
|
||||
4517642B1DE939FD00EDB8B9 /* ContactCell.swift in Sources */,
|
||||
34EA69402194933900702471 /* MediaDownloadView.swift in Sources */,
|
||||
340FC8AB204DAC8D007AEB0F /* DomainFrontingCountryViewController.m in Sources */,
|
||||
|
@ -3872,25 +3981,25 @@
|
|||
45F32C222057297A00A300D5 /* MediaDetailViewController.m in Sources */,
|
||||
B8B26C8F234D629C004ED98C /* MentionCandidateSelectionView.swift in Sources */,
|
||||
34B3F8851E8DF1700035BE1A /* NewGroupViewController.m in Sources */,
|
||||
B8CCF63F23975CFB0091D419 /* JoinPublicChatVC.swift in Sources */,
|
||||
34ABC0E421DD20C500ED9469 /* ConversationMessageMapping.swift in Sources */,
|
||||
B821F2F82272CED3002C88C0 /* DisplayNameVC.swift in Sources */,
|
||||
34D8C0271ED3673300188D7C /* DebugUIMessages.m in Sources */,
|
||||
B885D5F62334A32100EE0D8E /* UIView+Constraint.swift in Sources */,
|
||||
24BD2609234DA2050008EB0A /* NewPublicChatVC.swift in Sources */,
|
||||
B885D5F62334A32100EE0D8E /* UIView+Constraints.swift in Sources */,
|
||||
34DBF003206BD5A500025978 /* OWSMessageTextView.m in Sources */,
|
||||
34D1F0B41F86D31D0066283D /* ConversationCollectionView.m in Sources */,
|
||||
34B3F8821E8DF1700035BE1A /* NewContactThreadViewController.m in Sources */,
|
||||
45D308AD2049A439000189E4 /* PinEntryView.m in Sources */,
|
||||
B85357C323A1BD1200AAF6CD /* SeedVCV2.swift in Sources */,
|
||||
340FC8B1204DAC8D007AEB0F /* BlockListViewController.m in Sources */,
|
||||
45B5360E206DD8BB00D61655 /* UIResponder+OWS.swift in Sources */,
|
||||
4CFE6B6C21F92BA700006701 /* LegacyNotificationsAdaptee.swift in Sources */,
|
||||
B8CCF6432397711F0091D419 /* SettingsVC.swift in Sources */,
|
||||
3441FD9F21A3604F00BB9542 /* BackupRestoreViewController.swift in Sources */,
|
||||
45F659821E1BE77000444429 /* NonCallKitCallUIAdaptee.swift in Sources */,
|
||||
4C5250D221E7BD7D00CE3D95 /* PhoneNumberValidator.swift in Sources */,
|
||||
45AE48511E0732D6004D96C2 /* TurnServerInfo.swift in Sources */,
|
||||
34B3F8771E8DF1700035BE1A /* ContactsPicker.swift in Sources */,
|
||||
45C0DC1B1E68FE9000E04C47 /* UIApplication+OWS.swift in Sources */,
|
||||
B89841E322B7579F00B1BDC6 /* NewConversationVC.swift in Sources */,
|
||||
45FBC5C81DF8575700E9B410 /* CallKitCallManager.swift in Sources */,
|
||||
4539B5861F79348F007141FF /* PushRegistrationManager.swift in Sources */,
|
||||
45FBC5D11DF8592E00E9B410 /* SignalCall.swift in Sources */,
|
||||
|
@ -3898,6 +4007,7 @@
|
|||
340FC8BB204DAC8D007AEB0F /* OWSAddToContactViewController.m in Sources */,
|
||||
45F32C232057297A00A300D5 /* MediaPageViewController.swift in Sources */,
|
||||
452C468F1E427E200087B011 /* OutboundCallInitiator.swift in Sources */,
|
||||
B82B4094239DF15900A248E7 /* ConversationTitleView.swift in Sources */,
|
||||
34D2CCDA2062E7D000CB1A14 /* OWSScreenLockUI.m in Sources */,
|
||||
45F170BB1E2FC5D3003FC1F2 /* CallAudioService.swift in Sources */,
|
||||
4CA46F4C219CCC630038ABDE /* CaptionView.swift in Sources */,
|
||||
|
@ -3911,9 +4021,11 @@
|
|||
458DE9D61DEE3FD00071BB03 /* PeerConnectionClient.swift in Sources */,
|
||||
45DDA6242090CEB500DE97F8 /* ConversationHeaderView.swift in Sources */,
|
||||
7B53C4F0239E038C008DF635 /* PushNotificationManager.swift in Sources */,
|
||||
B82B4090239DD75000A248E7 /* RestoreVC.swift in Sources */,
|
||||
3488F9362191CC4000E524CC /* ConversationMediaView.swift in Sources */,
|
||||
45F32C242057297A00A300D5 /* MessageDetailViewController.swift in Sources */,
|
||||
3496955C219B605E00DCFE74 /* ImagePickerController.swift in Sources */,
|
||||
B8BB82B523947F2D00BA5194 /* TextField.swift in Sources */,
|
||||
34D1F0841F8678AA0066283D /* ConversationInputToolbar.m in Sources */,
|
||||
457F671B20746193000EABCD /* QuotedReplyPreview.swift in Sources */,
|
||||
34A6C28021E503E700B5B12E /* OWSImagePickerController.swift in Sources */,
|
||||
|
@ -3930,16 +4042,20 @@
|
|||
340FC8B5204DAC8D007AEB0F /* AboutTableViewController.m in Sources */,
|
||||
34BECE2B1F74C12700D7438D /* DebugUIStress.m in Sources */,
|
||||
340FC8B9204DAC8D007AEB0F /* UpdateGroupViewController.m in Sources */,
|
||||
B8BB82A5238F627000BA5194 /* HomeVC.swift in Sources */,
|
||||
3448E1662215B313004B052E /* OnboardingCaptchaViewController.swift in Sources */,
|
||||
4574A5D61DD6704700C6B692 /* CallService.swift in Sources */,
|
||||
4521C3C01F59F3BA00B4C582 /* TextFieldHelper.swift in Sources */,
|
||||
34D2CCDF206939B400CB1A14 /* DebugUIMessagesAction.m in Sources */,
|
||||
340FC8AC204DAC8D007AEB0F /* PrivacySettingsTableViewController.m in Sources */,
|
||||
B85357BF23A1AE0800AAF6CD /* SeedReminderView.swift in Sources */,
|
||||
B85357C723A1FB5100AAF6CD /* LinkDeviceVCDelegate.swift in Sources */,
|
||||
340FC8C5204DE223007AEB0F /* DebugUIBackup.m in Sources */,
|
||||
4C11AA5020FD59C700351FBD /* MessageStatusView.swift in Sources */,
|
||||
340FC8AE204DAC8D007AEB0F /* OWSSoundSettingsViewController.m in Sources */,
|
||||
4579431E1E7C8CE9008ED0C0 /* Pastelog.m in Sources */,
|
||||
340FC8B0204DAC8D007AEB0F /* AddToBlockListViewController.m in Sources */,
|
||||
B8BB82BE2394D4CE00BA5194 /* Fonts.swift in Sources */,
|
||||
3496957321A301A100DCFE74 /* OWSBackupJob.m in Sources */,
|
||||
340FC8B3204DAC8D007AEB0F /* AppSettingsViewController.m in Sources */,
|
||||
34C4E2572118957600BEA353 /* OWSWebRTCDataProtos.pb.swift in Sources */,
|
||||
|
@ -4110,7 +4226,7 @@
|
|||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 23;
|
||||
CURRENT_PROJECT_VERSION = 24;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
|
||||
|
@ -4124,7 +4240,7 @@
|
|||
INFOPLIST_FILE = SignalShareExtension/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
||||
MARKETING_VERSION = 1.5.1;
|
||||
MARKETING_VERSION = 1.0.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.share-extension";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
@ -4172,7 +4288,7 @@
|
|||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 23;
|
||||
CURRENT_PROJECT_VERSION = 24;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
|
@ -4191,7 +4307,7 @@
|
|||
INFOPLIST_FILE = SignalShareExtension/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
||||
MARKETING_VERSION = 1.5.1;
|
||||
MARKETING_VERSION = 1.0.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.share-extension";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
@ -4226,7 +4342,7 @@
|
|||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CURRENT_PROJECT_VERSION = 24;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||
|
@ -4245,7 +4361,7 @@
|
|||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
MARKETING_VERSION = 1.5.1;
|
||||
MARKETING_VERSION = 1.0.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.utilities";
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
|
@ -4295,7 +4411,7 @@
|
|||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CURRENT_PROJECT_VERSION = 24;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||
|
@ -4319,7 +4435,7 @@
|
|||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
MARKETING_VERSION = 1.5.1;
|
||||
MARKETING_VERSION = 1.0.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.utilities";
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
|
@ -4504,7 +4620,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = Signal/Signal.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CURRENT_PROJECT_VERSION = 23;
|
||||
CURRENT_PROJECT_VERSION = 24;
|
||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
|
@ -4539,7 +4655,7 @@
|
|||
"$(SRCROOT)",
|
||||
);
|
||||
LLVM_LTO = NO;
|
||||
MARKETING_VERSION = 1.5.1;
|
||||
MARKETING_VERSION = 1.0.0;
|
||||
OTHER_LDFLAGS = "$(inherited)";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger";
|
||||
PRODUCT_NAME = "Loki Messenger";
|
||||
|
@ -4571,7 +4687,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = Signal/Signal.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CURRENT_PROJECT_VERSION = 23;
|
||||
CURRENT_PROJECT_VERSION = 24;
|
||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
|
@ -4606,7 +4722,7 @@
|
|||
"$(SRCROOT)",
|
||||
);
|
||||
LLVM_LTO = NO;
|
||||
MARKETING_VERSION = 1.5.1;
|
||||
MARKETING_VERSION = 1.0.0;
|
||||
OTHER_LDFLAGS = "$(inherited)";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger";
|
||||
PRODUCT_NAME = "Loki Messenger";
|
||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "AddPerson.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
Binary file not shown.
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "ArrowUp.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
Binary file not shown.
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "Check.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
Binary file not shown.
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "CircleCheck.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
BIN
Signal/Images.xcassets/Loki V2/CircleDotDotDot.imageset/CircleDotDotDot.pdf
vendored
Normal file
BIN
Signal/Images.xcassets/Loki V2/CircleDotDotDot.imageset/CircleDotDotDot.pdf
vendored
Normal file
Binary file not shown.
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "CircleDotDotDot.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
Binary file not shown.
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "CirclePause.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
Binary file not shown.
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "CirclePlay.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
Binary file not shown.
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "CirclePlus.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "FilledCircleCheck.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
BIN
Signal/Images.xcassets/Loki V2/FilledCircleCheck.imageset/FilledCircleCheck.pdf
vendored
Normal file
BIN
Signal/Images.xcassets/Loki V2/FilledCircleCheck.imageset/FilledCircleCheck.pdf
vendored
Normal file
Binary file not shown.
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "Gear.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
Binary file not shown.
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "Globe.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
Binary file not shown.
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "Key.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
Binary file not shown.
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "MagnifyingGlass.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
BIN
Signal/Images.xcassets/Loki V2/MagnifyingGlass.imageset/MagnifyingGlass.pdf
vendored
Normal file
BIN
Signal/Images.xcassets/Loki V2/MagnifyingGlass.imageset/MagnifyingGlass.pdf
vendored
Normal file
Binary file not shown.
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "Microphone.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
Binary file not shown.
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "People.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
Binary file not shown.
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "Play.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "QRCodeFilled.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
Binary file not shown.
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "Session.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "StarOutline.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
Binary file not shown.
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "X.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
Binary file not shown.
|
@ -5,9 +5,9 @@
|
|||
<key>BuildDetails</key>
|
||||
<dict>
|
||||
<key>CarthageVersion</key>
|
||||
<string>0.33.0</string>
|
||||
<string>0.34.0</string>
|
||||
<key>OSXVersion</key>
|
||||
<string>10.15</string>
|
||||
<string>10.15.1</string>
|
||||
<key>WebRTCCommit</key>
|
||||
<string>1445d719bf05280270e9f77576f80f973fd847f8 M73</string>
|
||||
</dict>
|
||||
|
@ -139,6 +139,8 @@
|
|||
<string>dripicons-v2.ttf</string>
|
||||
<string>ElegantIcons.ttf</string>
|
||||
<string>fontawesome-webfont.ttf</string>
|
||||
<string>SpaceMono-Bold.ttf</string>
|
||||
<string>SpaceMono-Regular.ttf</string>
|
||||
</array>
|
||||
<key>UIApplicationShortcutItems</key>
|
||||
<array>
|
||||
|
@ -166,7 +168,11 @@
|
|||
</array>
|
||||
<key>UIStatusBarStyle</key>
|
||||
<string>UIStatusBarStyleLightContent</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<true/>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
Binary file not shown.
|
@ -878,7 +878,7 @@ static NSTimeInterval launchStartedAt;
|
|||
return;
|
||||
}
|
||||
|
||||
[SignalApp.sharedApp.homeViewController showNewConversationVC];
|
||||
[SignalApp.sharedApp.homeViewController createPrivateChat];
|
||||
|
||||
completionHandler(YES);
|
||||
}];
|
||||
|
@ -1443,11 +1443,11 @@ static NSTimeInterval launchStartedAt;
|
|||
if (self.backup.hasPendingRestoreDecision) {
|
||||
rootViewController = [BackupRestoreViewController new];
|
||||
} else {
|
||||
rootViewController = [HomeViewController new];
|
||||
rootViewController = [HomeVC new];
|
||||
}
|
||||
} else {
|
||||
rootViewController = [[OnboardingController new] initialViewController];
|
||||
navigationBarHidden = YES;
|
||||
navigationBarHidden = NO;
|
||||
}
|
||||
OWSAssertDebug(rootViewController);
|
||||
OWSNavigationController *navigationController =
|
||||
|
@ -1590,13 +1590,6 @@ static NSTimeInterval launchStartedAt;
|
|||
__block TSGroupThread *thread;
|
||||
[OWSPrimaryStorage.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
thread = [TSGroupThread getOrCreateThreadWithGroupModel:group transaction:transaction];
|
||||
NSTimeZone *timeZone = [NSTimeZone timeZoneWithName:@"UTC"];
|
||||
NSCalendar *calendar = NSCalendar.currentCalendar;
|
||||
[calendar setTimeZone:timeZone];
|
||||
NSDateComponents *dateComponents = [NSDateComponents new];
|
||||
[dateComponents setYear:999];
|
||||
NSDate *date = [calendar dateByAddingComponents:dateComponents toDate:[NSDate new] options:0];
|
||||
[thread updateWithMutedUntilDate:date transaction:transaction];
|
||||
}];
|
||||
[OWSProfileManager.sharedManager addThreadToProfileWhitelist:thread];
|
||||
[NSUserDefaults.standardUserDefaults setBool:YES forKey:userDefaultsKey];
|
||||
|
|
|
@ -145,6 +145,7 @@ public class SearchResultsBar: UIToolbar {
|
|||
override init(frame: CGRect) {
|
||||
|
||||
labelItem = UIBarButtonItem(title: nil, style: .plain, target: nil, action: nil)
|
||||
labelItem.setTitleTextAttributes([ .font : UIFont.systemFont(ofSize: Values.mediumFontSize) ], for: UIControl.State.normal)
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
|
@ -161,12 +162,12 @@ public class SearchResultsBar: UIToolbar {
|
|||
let upChevron = #imageLiteral(resourceName: "ic_chevron_up").withRenderingMode(.alwaysTemplate)
|
||||
showLessRecentButton = UIBarButtonItem(image: upChevron, style: .plain, target: self, action: #selector(didTapShowLessRecent))
|
||||
showLessRecentButton.imageInsets = UIEdgeInsets(top: 2, left: leftExteriorChevronMargin, bottom: 2, right: leftInteriorChevronMargin)
|
||||
showLessRecentButton.tintColor = UIColor.ows_systemPrimaryButton
|
||||
showLessRecentButton.tintColor = Colors.accent
|
||||
|
||||
let downChevron = #imageLiteral(resourceName: "ic_chevron_down").withRenderingMode(.alwaysTemplate)
|
||||
showMoreRecentButton = UIBarButtonItem(image: downChevron, style: .plain, target: self, action: #selector(didTapShowMoreRecent))
|
||||
showMoreRecentButton.imageInsets = UIEdgeInsets(top: 2, left: leftInteriorChevronMargin, bottom: 2, right: leftExteriorChevronMargin)
|
||||
showMoreRecentButton.tintColor = UIColor.ows_systemPrimaryButton
|
||||
showMoreRecentButton.tintColor = Colors.accent
|
||||
|
||||
let spacer1 = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
|
||||
let spacer2 = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
|
||||
|
@ -175,7 +176,7 @@ public class SearchResultsBar: UIToolbar {
|
|||
|
||||
self.isTranslucent = false
|
||||
self.isOpaque = true
|
||||
self.barTintColor = Theme.toolbarBackgroundColor
|
||||
self.barTintColor = Colors.navigationBarBackground
|
||||
|
||||
self.autoresizingMask = .flexibleHeight
|
||||
self.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
|
||||
final class Button : UIButton {
|
||||
private let style: Style
|
||||
private let size: Size
|
||||
|
||||
enum Style {
|
||||
case unimportant, regular, prominentOutline, prominentFilled, regularBorderless
|
||||
}
|
||||
|
||||
enum Size {
|
||||
case medium, large, small
|
||||
}
|
||||
|
||||
init(style: Style, size: Size) {
|
||||
self.style = style
|
||||
self.size = size
|
||||
super.init(frame: .zero)
|
||||
setUpStyle()
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
preconditionFailure("Use init(style:) instead.")
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
preconditionFailure("Use init(style:) instead.")
|
||||
}
|
||||
|
||||
private func setUpStyle() {
|
||||
let fillColor: UIColor
|
||||
switch style {
|
||||
case .unimportant: fillColor = Colors.unimportantButtonBackground
|
||||
case .regular: fillColor = UIColor.clear
|
||||
case .prominentOutline: fillColor = UIColor.clear
|
||||
case .prominentFilled: fillColor = Colors.accent
|
||||
case .regularBorderless: fillColor = UIColor.clear
|
||||
}
|
||||
let borderColor: UIColor
|
||||
switch style {
|
||||
case .unimportant: borderColor = Colors.unimportantButtonBackground
|
||||
case .regular: borderColor = Colors.text
|
||||
case .prominentOutline: borderColor = Colors.accent
|
||||
case .prominentFilled: borderColor = Colors.accent
|
||||
case .regularBorderless: borderColor = UIColor.clear
|
||||
}
|
||||
let textColor: UIColor
|
||||
switch style {
|
||||
case .unimportant: textColor = Colors.text
|
||||
case .regular: textColor = Colors.text
|
||||
case .prominentOutline: textColor = Colors.accent
|
||||
case .prominentFilled: textColor = Colors.text
|
||||
case .regularBorderless: textColor = Colors.text
|
||||
}
|
||||
let height: CGFloat
|
||||
switch size {
|
||||
case .small: height = Values.smallButtonHeight
|
||||
case .medium: height = Values.mediumButtonHeight
|
||||
case .large: height = Values.largeButtonHeight
|
||||
}
|
||||
set(.height, to: height)
|
||||
layer.cornerRadius = height / 2
|
||||
backgroundColor = fillColor
|
||||
layer.borderColor = borderColor.cgColor
|
||||
layer.borderWidth = Values.borderThickness
|
||||
let fontSize = (size == .small) ? Values.smallFontSize : Values.mediumFontSize
|
||||
titleLabel!.font = Fonts.spaceMono(ofSize: fontSize)
|
||||
setTitleColor(textColor, for: UIControl.State.normal)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,189 @@
|
|||
|
||||
final class ConversationCell : UITableViewCell {
|
||||
var threadViewModel: ThreadViewModel! { didSet { update() } }
|
||||
|
||||
static let reuseIdentifier = "ConversationCell"
|
||||
|
||||
// MARK: Components
|
||||
private lazy var unreadMessagesIndicatorView: UIView = {
|
||||
let result = UIView()
|
||||
result.backgroundColor = Colors.accent
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var profilePictureView = ProfilePictureView()
|
||||
|
||||
private lazy var displayNameLabel: UILabel = {
|
||||
let result = UILabel()
|
||||
result.font = .boldSystemFont(ofSize: Values.mediumFontSize)
|
||||
result.textColor = Colors.text
|
||||
result.lineBreakMode = .byTruncatingTail
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var timestampLabel: UILabel = {
|
||||
let result = UILabel()
|
||||
result.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
result.textColor = Colors.text
|
||||
result.lineBreakMode = .byTruncatingTail
|
||||
result.alpha = Values.conversationCellTimestampOpacity
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var snippetLabel: UILabel = {
|
||||
let result = UILabel()
|
||||
result.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
result.textColor = Colors.text
|
||||
result.lineBreakMode = .byTruncatingTail
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var typingIndicatorView = TypingIndicatorView()
|
||||
|
||||
private lazy var bottomLabelStackViewSpacer = UIView.hStretchingSpacer()
|
||||
|
||||
private lazy var statusIndicatorView: UIImageView = {
|
||||
let result = UIImageView()
|
||||
result.contentMode = .center
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: Initialization
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
private func setUpViewHierarchy() {
|
||||
// Set the cell background color
|
||||
backgroundColor = Colors.cellBackground
|
||||
// Set up the highlight color
|
||||
let selectedBackgroundView = UIView()
|
||||
selectedBackgroundView.backgroundColor = Colors.cellSelected
|
||||
self.selectedBackgroundView = selectedBackgroundView
|
||||
// Set up the unread messages indicator view
|
||||
unreadMessagesIndicatorView.set(.width, to: Values.accentLineThickness)
|
||||
// Set up the profile picture view
|
||||
let profilePictureViewSize = Values.mediumProfilePictureSize
|
||||
profilePictureView.set(.width, to: profilePictureViewSize)
|
||||
profilePictureView.set(.height, to: profilePictureViewSize)
|
||||
profilePictureView.size = profilePictureViewSize
|
||||
// Set up the label stack view
|
||||
let topLabelStackView = UIStackView(arrangedSubviews: [ displayNameLabel, UIView.hStretchingSpacer(), timestampLabel ])
|
||||
topLabelStackView.axis = .horizontal
|
||||
topLabelStackView.spacing = Values.smallSpacing / 2 // Effectively Values.smallSpacing because there'll be spacing before and after the invisible spacer
|
||||
let snippetLabelContainer = UIView()
|
||||
snippetLabelContainer.addSubview(snippetLabel)
|
||||
snippetLabelContainer.addSubview(typingIndicatorView)
|
||||
let bottomLabelStackView = UIStackView(arrangedSubviews: [ snippetLabelContainer, bottomLabelStackViewSpacer, statusIndicatorView ])
|
||||
bottomLabelStackView.axis = .horizontal
|
||||
bottomLabelStackView.spacing = Values.smallSpacing / 2 // Effectively Values.smallSpacing because there'll be spacing before and after the invisible spacer
|
||||
let labelStackView = UIStackView(arrangedSubviews: [ UIView.spacer(withHeight: Values.smallSpacing), topLabelStackView, bottomLabelStackView, UIView.spacer(withHeight: Values.smallSpacing) ])
|
||||
labelStackView.axis = .vertical
|
||||
labelStackView.spacing = Values.smallSpacing
|
||||
// Set up the main stack view
|
||||
let stackView = UIStackView(arrangedSubviews: [ unreadMessagesIndicatorView, profilePictureView, labelStackView ])
|
||||
stackView.axis = .horizontal
|
||||
stackView.alignment = .center
|
||||
stackView.spacing = Values.mediumSpacing
|
||||
contentView.addSubview(stackView)
|
||||
// Set up the constraints
|
||||
unreadMessagesIndicatorView.pin(.top, to: .top, of: stackView)
|
||||
unreadMessagesIndicatorView.pin(.bottom, to: .bottom, of: stackView)
|
||||
timestampLabel.setContentCompressionResistancePriority(.required, for: NSLayoutConstraint.Axis.horizontal)
|
||||
statusIndicatorView.set(.width, to: Values.conversationCellStatusIndicatorSize)
|
||||
statusIndicatorView.set(.height, to: Values.conversationCellStatusIndicatorSize)
|
||||
snippetLabel.pin(to: snippetLabelContainer)
|
||||
typingIndicatorView.pin(.leading, to: .leading, of: snippetLabelContainer)
|
||||
typingIndicatorView.centerYAnchor.constraint(equalTo: snippetLabel.centerYAnchor).isActive = true
|
||||
stackView.pin(.leading, to: .leading, of: contentView)
|
||||
stackView.pin(.top, to: .top, of: contentView)
|
||||
contentView.pin(.trailing, to: .trailing, of: stackView, withInset: Values.mediumSpacing)
|
||||
contentView.pin(.bottom, to: .bottom, of: stackView)
|
||||
stackView.set(.width, to: UIScreen.main.bounds.width - Values.mediumSpacing) // Workaround for weird constraints issue
|
||||
}
|
||||
|
||||
// MARK: Updating
|
||||
private func update() {
|
||||
LokiAPI.populateUserHexEncodedPublicKeyCacheIfNeeded(for: threadViewModel.threadRecord.uniqueId!) // FIXME: This is a terrible place to do this
|
||||
unreadMessagesIndicatorView.alpha = threadViewModel.hasUnreadMessages ? 1 : 0
|
||||
if threadViewModel.isGroupThread {
|
||||
let users = LokiAPI.userHexEncodedPublicKeyCache[threadViewModel.threadRecord.uniqueId!] ?? []
|
||||
let randomUsers = users.sorted().prefix(2) // Sort to provide a level of stability
|
||||
if !randomUsers.isEmpty {
|
||||
profilePictureView.hexEncodedPublicKey = randomUsers[0]
|
||||
profilePictureView.additionalHexEncodedPublicKey = randomUsers.count == 2 ? randomUsers[1] : ""
|
||||
} else {
|
||||
profilePictureView.hexEncodedPublicKey = ""
|
||||
profilePictureView.additionalHexEncodedPublicKey = ""
|
||||
}
|
||||
profilePictureView.isRSSFeed = (threadViewModel.threadRecord as? TSGroupThread)?.isRSSFeed ?? false
|
||||
} else {
|
||||
profilePictureView.hexEncodedPublicKey = threadViewModel.contactIdentifier!
|
||||
profilePictureView.additionalHexEncodedPublicKey = nil
|
||||
profilePictureView.isRSSFeed = false
|
||||
}
|
||||
profilePictureView.update()
|
||||
displayNameLabel.text = getDisplayName()
|
||||
timestampLabel.text = DateUtil.formatDateShort(threadViewModel.lastMessageDate)
|
||||
if SSKEnvironment.shared.typingIndicators.typingRecipientId(forThread: self.threadViewModel.threadRecord) != nil {
|
||||
snippetLabel.text = ""
|
||||
typingIndicatorView.isHidden = false
|
||||
typingIndicatorView.startAnimation()
|
||||
} else {
|
||||
snippetLabel.attributedText = getSnippet()
|
||||
typingIndicatorView.isHidden = true
|
||||
typingIndicatorView.stopAnimation()
|
||||
}
|
||||
let lastMessage = threadViewModel.lastMessageForInbox
|
||||
if let lastMessage = lastMessage as? TSOutgoingMessage {
|
||||
let image: UIImage
|
||||
let status = MessageRecipientStatusUtils.recipientStatus(outgoingMessage: lastMessage)
|
||||
switch status {
|
||||
case .calculatingPoW, .uploading, .sending: image = #imageLiteral(resourceName: "CircleDotDotDot")
|
||||
case .sent, .skipped, .delivered: image = #imageLiteral(resourceName: "CircleCheck")
|
||||
case .read: image = #imageLiteral(resourceName: "FilledCircleCheck")
|
||||
case .failed: image = #imageLiteral(resourceName: "message_status_failed")
|
||||
}
|
||||
statusIndicatorView.image = image
|
||||
statusIndicatorView.isHidden = false
|
||||
} else {
|
||||
statusIndicatorView.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
private func getDisplayName() -> String {
|
||||
if threadViewModel.isGroupThread {
|
||||
if threadViewModel.name.isEmpty {
|
||||
return NSLocalizedString("New Group", comment: "")
|
||||
} else {
|
||||
return threadViewModel.name
|
||||
}
|
||||
} else {
|
||||
if threadViewModel.threadRecord.isNoteToSelf() {
|
||||
return NSLocalizedString("Note to Self", comment: "")
|
||||
} else {
|
||||
let hexEncodedPublicKey = threadViewModel.contactIdentifier!
|
||||
return DisplayNameUtilities.getPrivateChatDisplayName(for: hexEncodedPublicKey) ?? hexEncodedPublicKey
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func getSnippet() -> NSMutableAttributedString {
|
||||
let result = NSMutableAttributedString()
|
||||
if threadViewModel.isMuted {
|
||||
result.append(NSAttributedString(string: "\u{e067} ", attributes: [ .font : UIFont.ows_elegantIconsFont(10), .foregroundColor : Colors.unimportant ]))
|
||||
}
|
||||
if let rawSnippet = threadViewModel.lastMessageText {
|
||||
let snippet = MentionUtilities.highlightMentions(in: rawSnippet, threadID: threadViewModel.threadRecord.uniqueId!)
|
||||
let font = threadViewModel.hasUnreadMessages ? UIFont.boldSystemFont(ofSize: Values.smallFontSize) : UIFont.systemFont(ofSize: Values.smallFontSize)
|
||||
result.append(NSAttributedString(string: snippet, attributes: [ .font : font, .foregroundColor : Colors.text ]))
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
|
@ -0,0 +1,167 @@
|
|||
|
||||
@objc final class ConversationTitleView : UIView {
|
||||
private let thread: TSThread
|
||||
private var currentStatus: Status? { didSet { updateSubtitleForCurrentStatus() } }
|
||||
|
||||
// MARK: Types
|
||||
private enum Status : Int {
|
||||
case calculatingPoW = 1
|
||||
case contactingNetwork = 2
|
||||
case sendingMessage = 3
|
||||
case messageSent = 4
|
||||
case messageFailed = 5
|
||||
}
|
||||
|
||||
// MARK: Components
|
||||
private lazy var titleLabel: UILabel = {
|
||||
let result = UILabel()
|
||||
result.textColor = Colors.text
|
||||
result.font = .boldSystemFont(ofSize: Values.mediumFontSize)
|
||||
result.lineBreakMode = .byTruncatingTail
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var subtitleLabel: UILabel = {
|
||||
let result = UILabel()
|
||||
result.textColor = Colors.text
|
||||
result.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
result.lineBreakMode = .byTruncatingTail
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: Lifecycle
|
||||
@objc init(thread: TSThread) {
|
||||
self.thread = thread
|
||||
super.init(frame: CGRect.zero)
|
||||
setUpViewHierarchy()
|
||||
updateTitle()
|
||||
updateSubtitleForCurrentStatus()
|
||||
let notificationCenter = NotificationCenter.default
|
||||
notificationCenter.addObserver(self, selector: #selector(handleProfileChangedNotification(_:)), name: NSNotification.Name(rawValue: kNSNotificationName_OtherUsersProfileDidChange), object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(handleCalculatingPoWNotification(_:)), name: .calculatingPoW, object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(handleContactingNetworkNotification(_:)), name: .contactingNetwork, object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(handleSendingMessageNotification(_:)), name: .sendingMessage, object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(handleMessageSentNotification(_:)), name: .messageSent, object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(handleMessageFailedNotification(_:)), name: .messageFailed, object: nil)
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
preconditionFailure("Use init(thread:) instead.")
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
preconditionFailure("Use init(thread:) instead.")
|
||||
}
|
||||
|
||||
private func setUpViewHierarchy() {
|
||||
let stackView = UIStackView(arrangedSubviews: [ titleLabel, subtitleLabel ])
|
||||
stackView.axis = .vertical
|
||||
stackView.alignment = .center
|
||||
stackView.layoutMargins = UIEdgeInsets(top: 4, left: 8, bottom: 4, right: 0) // Compensate for settings button trailing margin
|
||||
stackView.isLayoutMarginsRelativeArrangement = true
|
||||
addSubview(stackView)
|
||||
stackView.pin(to: self)
|
||||
}
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
// MARK: Updating
|
||||
private func updateTitle() {
|
||||
let title: String
|
||||
if thread.isGroupThread() {
|
||||
if thread.name().isEmpty {
|
||||
title = NSLocalizedString("New Group", comment: "")
|
||||
} else {
|
||||
title = thread.name()
|
||||
}
|
||||
} else {
|
||||
if thread.isNoteToSelf() {
|
||||
title = NSLocalizedString("Note to Self", comment: "")
|
||||
} else {
|
||||
let hexEncodedPublicKey = thread.contactIdentifier()!
|
||||
title = DisplayNameUtilities.getPrivateChatDisplayName(for: hexEncodedPublicKey) ?? hexEncodedPublicKey
|
||||
}
|
||||
}
|
||||
titleLabel.text = title
|
||||
}
|
||||
|
||||
@objc private func handleProfileChangedNotification(_ notification: Notification) {
|
||||
guard let hexEncodedPublicKey = notification.userInfo?[kNSNotificationKey_ProfileRecipientId] as? String, let thread = self.thread as? TSContactThread,
|
||||
hexEncodedPublicKey == thread.contactIdentifier() else { return }
|
||||
updateTitle()
|
||||
}
|
||||
|
||||
@objc private func handleCalculatingPoWNotification(_ notification: Notification) {
|
||||
guard let timestamp = notification.object as? NSNumber else { return }
|
||||
setStatusIfNeeded(to: .calculatingPoW, forMessageWithTimestamp: timestamp)
|
||||
}
|
||||
|
||||
@objc private func handleContactingNetworkNotification(_ notification: Notification) {
|
||||
guard let timestamp = notification.object as? NSNumber else { return }
|
||||
setStatusIfNeeded(to: .contactingNetwork, forMessageWithTimestamp: timestamp)
|
||||
}
|
||||
|
||||
@objc private func handleSendingMessageNotification(_ notification: Notification) {
|
||||
guard let timestamp = notification.object as? NSNumber else { return }
|
||||
setStatusIfNeeded(to: .sendingMessage, forMessageWithTimestamp: timestamp)
|
||||
}
|
||||
|
||||
@objc private func handleMessageSentNotification(_ notification: Notification) {
|
||||
guard let timestamp = notification.object as? NSNumber else { return }
|
||||
setStatusIfNeeded(to: .messageSent, forMessageWithTimestamp: timestamp)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
self.clearStatusIfNeededForMessageWithTimestamp(timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func handleMessageFailedNotification(_ notification: Notification) {
|
||||
guard let timestamp = notification.object as? NSNumber else { return }
|
||||
clearStatusIfNeededForMessageWithTimestamp(timestamp)
|
||||
}
|
||||
|
||||
private func setStatusIfNeeded(to status: Status, forMessageWithTimestamp timestamp: NSNumber) {
|
||||
var uncheckedTargetInteraction: TSInteraction? = nil
|
||||
thread.enumerateInteractions { interaction in
|
||||
guard interaction.timestamp == timestamp.uint64Value else { return }
|
||||
uncheckedTargetInteraction = interaction
|
||||
}
|
||||
guard let targetInteraction = uncheckedTargetInteraction, targetInteraction.interactionType() == .outgoingMessage, status.rawValue > (currentStatus?.rawValue ?? 0) else { return }
|
||||
currentStatus = status
|
||||
}
|
||||
|
||||
private func clearStatusIfNeededForMessageWithTimestamp(_ timestamp: NSNumber) {
|
||||
var uncheckedTargetInteraction: TSInteraction? = nil
|
||||
thread.enumerateInteractions { interaction in
|
||||
guard interaction.timestamp == timestamp.uint64Value else { return }
|
||||
uncheckedTargetInteraction = interaction
|
||||
}
|
||||
guard let targetInteraction = uncheckedTargetInteraction, targetInteraction.interactionType() == .outgoingMessage else { return }
|
||||
self.currentStatus = nil
|
||||
}
|
||||
|
||||
private func updateSubtitleForCurrentStatus() {
|
||||
DispatchQueue.main.async {
|
||||
switch self.currentStatus {
|
||||
case .calculatingPoW: self.subtitleLabel.text = NSLocalizedString("Encrypting message", comment: "")
|
||||
case .contactingNetwork: self.subtitleLabel.text = NSLocalizedString("Tracing a path", comment: "")
|
||||
case .sendingMessage: self.subtitleLabel.text = NSLocalizedString("Sending message", comment: "")
|
||||
case .messageSent: self.subtitleLabel.text = NSLocalizedString("Message sent securely", comment: "")
|
||||
case .messageFailed: self.subtitleLabel.text = NSLocalizedString("Message failed to send", comment: "")
|
||||
case nil:
|
||||
let subtitle = NSMutableAttributedString()
|
||||
if self.thread.isMuted {
|
||||
subtitle.append(NSAttributedString(string: "\u{e067} ", attributes: [ .font : UIFont.ows_elegantIconsFont(10), .foregroundColor : Colors.unimportant ]))
|
||||
}
|
||||
subtitle.append(NSAttributedString(string: "26 members")) // TODO: Implement
|
||||
self.subtitleLabel.attributedText = subtitle
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Layout
|
||||
public override var intrinsicContentSize: CGSize {
|
||||
return UIView.layoutFittingExpandedSize
|
||||
}
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
|
||||
final class FakeChatView : UIView {
|
||||
private let spacing = Values.mediumSpacing
|
||||
|
||||
var contentOffset: CGPoint {
|
||||
get { return scrollView.contentOffset }
|
||||
set { scrollView.contentOffset = newValue }
|
||||
}
|
||||
|
||||
private lazy var chatBubbles = [
|
||||
getChatBubble(withText: NSLocalizedString("What's Session?", comment: ""), wasSentByCurrentUser: true),
|
||||
getChatBubble(withText: NSLocalizedString("It's a secure, decentralized cross-platform private messaging app", comment: ""), wasSentByCurrentUser: false),
|
||||
getChatBubble(withText: NSLocalizedString("So it doesn't collect my personal information or my conversation metadata? How's it work?", comment: ""), wasSentByCurrentUser: true),
|
||||
getChatBubble(withText: NSLocalizedString("Using a combination of advanced anonymous routing and end-to-end encryption technologies.", comment: ""), wasSentByCurrentUser: false),
|
||||
getChatBubble(withText: NSLocalizedString("Friends don't let friends use compromised messengers. You're welcome.", comment: ""), wasSentByCurrentUser: false)
|
||||
]
|
||||
|
||||
private lazy var scrollView: UIScrollView = {
|
||||
let result = UIScrollView()
|
||||
result.showsHorizontalScrollIndicator = false
|
||||
result.showsVerticalScrollIndicator = false
|
||||
return result
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
setUpViewHierarchy()
|
||||
animate()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
setUpViewHierarchy()
|
||||
animate()
|
||||
}
|
||||
|
||||
private func setUpViewHierarchy() {
|
||||
let stackView = UIStackView(arrangedSubviews: chatBubbles)
|
||||
stackView.axis = .vertical
|
||||
stackView.spacing = spacing
|
||||
stackView.alignment = .fill
|
||||
stackView.set(.width, to: UIScreen.main.bounds.width)
|
||||
stackView.layoutMargins = UIEdgeInsets(top: 8, leading: Values.veryLargeSpacing, bottom: 8, trailing: Values.veryLargeSpacing)
|
||||
stackView.isLayoutMarginsRelativeArrangement = true
|
||||
scrollView.addSubview(stackView)
|
||||
stackView.pin(to: scrollView)
|
||||
addSubview(scrollView)
|
||||
scrollView.pin(to: self)
|
||||
}
|
||||
|
||||
private func getChatBubble(withText text: String, wasSentByCurrentUser: Bool) -> UIView {
|
||||
let result = UIView()
|
||||
let bubbleView = UIView()
|
||||
bubbleView.set(.width, to: Values.fakeChatBubbleWidth)
|
||||
bubbleView.layer.cornerRadius = Values.fakeChatBubbleCornerRadius
|
||||
bubbleView.layer.shadowColor = UIColor.black.cgColor
|
||||
bubbleView.layer.shadowRadius = 8
|
||||
bubbleView.layer.shadowOpacity = 0.64
|
||||
let backgroundColor = wasSentByCurrentUser ? Colors.fakeChatBubbleBackground : Colors.accent
|
||||
bubbleView.backgroundColor = backgroundColor
|
||||
let label = UILabel()
|
||||
let textColor = wasSentByCurrentUser ? Colors.text : Colors.fakeChatBubbleText
|
||||
label.textColor = textColor
|
||||
label.font = .boldSystemFont(ofSize: Values.mediumFontSize)
|
||||
label.numberOfLines = 0
|
||||
label.lineBreakMode = .byWordWrapping
|
||||
label.text = text
|
||||
bubbleView.addSubview(label)
|
||||
label.pin(to: bubbleView, withInset: 12)
|
||||
result.addSubview(bubbleView)
|
||||
bubbleView.pin(.top, to: .top, of: result)
|
||||
result.pin(.bottom, to: .bottom, of: bubbleView)
|
||||
if wasSentByCurrentUser {
|
||||
bubbleView.pin(.trailing, to: .trailing, of: result)
|
||||
} else {
|
||||
result.pin(.leading, to: .leading, of: bubbleView)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private func animate() {
|
||||
let animationDuration = Values.fakeChatAnimationDuration
|
||||
let delayBetweenMessages = Values.fakeChatDelay
|
||||
chatBubbles.forEach { $0.alpha = 0 }
|
||||
Timer.scheduledTimer(withTimeInterval: Values.fakeChatStartDelay, repeats: false) { [weak self] _ in
|
||||
self?.showChatBubble(at: 0)
|
||||
Timer.scheduledTimer(withTimeInterval: 1.5 * delayBetweenMessages, repeats: false) { _ in
|
||||
self?.showChatBubble(at: 1)
|
||||
Timer.scheduledTimer(withTimeInterval: 1.5 * delayBetweenMessages, repeats: false) { _ in
|
||||
self?.showChatBubble(at: 2)
|
||||
UIView.animate(withDuration: animationDuration) {
|
||||
guard let self = self else { return }
|
||||
self.scrollView.contentOffset = CGPoint(x: 0, y: self.chatBubbles[0].height() + self.spacing)
|
||||
}
|
||||
Timer.scheduledTimer(withTimeInterval: 1.5 * delayBetweenMessages, repeats: false) { _ in
|
||||
self?.showChatBubble(at: 3)
|
||||
UIView.animate(withDuration: animationDuration) {
|
||||
guard let self = self else { return }
|
||||
self.scrollView.contentOffset = CGPoint(x: 0, y: self.chatBubbles[0].height() + self.spacing + self.chatBubbles[1].height() + self.spacing)
|
||||
}
|
||||
Timer.scheduledTimer(withTimeInterval: delayBetweenMessages, repeats: false) { _ in
|
||||
self?.showChatBubble(at: 4)
|
||||
UIView.animate(withDuration: animationDuration) {
|
||||
guard let self = self else { return }
|
||||
self.scrollView.contentOffset = CGPoint(x: 0, y: self.chatBubbles[0].height() + self.spacing + self.chatBubbles[1].height() + self.spacing + self.chatBubbles[2].height() + self.spacing)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func showChatBubble(at index: Int) {
|
||||
let chatBubble = chatBubbles[index]
|
||||
UIView.animate(withDuration: Values.fakeChatAnimationDuration) {
|
||||
chatBubble.alpha = 1
|
||||
}
|
||||
let scale = Values.fakeChatMessagePopAnimationStartScale
|
||||
chatBubble.transform = CGAffineTransform(scaleX: scale, y: scale)
|
||||
UIView.animate(withDuration: Values.fakeChatAnimationDuration, delay: 0, usingSpringWithDamping: 0.68, initialSpringVelocity: 4, options: .curveEaseInOut, animations: {
|
||||
chatBubble.transform = CGAffineTransform(scaleX: 1, y: 1)
|
||||
}, completion: nil)
|
||||
}
|
||||
}
|
|
@ -13,16 +13,22 @@ final class FriendRequestView : UIView {
|
|||
enum Kind : String { case incoming, outgoing }
|
||||
|
||||
// MARK: Components
|
||||
private lazy var topSpacer: UIView = {
|
||||
private lazy var spacer1: UIView = {
|
||||
let result = UIView()
|
||||
result.autoSetDimension(.height, toSize: 12)
|
||||
result.set(.height, to: 12)
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var spacer2: UIView = {
|
||||
let result = UIView()
|
||||
result.set(.height, to: Values.mediumSpacing)
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var label: UILabel = {
|
||||
let result = UILabel()
|
||||
result.textColor = Theme.secondaryColor
|
||||
result.font = UIFont.ows_dynamicTypeSubheadlineClamped
|
||||
result.textColor = Colors.text
|
||||
result.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
result.numberOfLines = 0
|
||||
result.textAlignment = .center
|
||||
result.lineBreakMode = .byWordWrapping
|
||||
|
@ -32,12 +38,10 @@ final class FriendRequestView : UIView {
|
|||
private lazy var buttonStackView: UIStackView = {
|
||||
let result = UIStackView()
|
||||
result.axis = .horizontal
|
||||
result.spacing = Values.mediumSpacing
|
||||
result.distribution = .fillEqually
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var buttonFont = UIFont.ows_dynamicTypeBodyClamped.ows_mediumWeight()
|
||||
private lazy var buttonHeight = buttonFont.pointSize * 48 / 17
|
||||
|
||||
// MARK: Lifecycle
|
||||
@objc init(message: TSMessage) {
|
||||
|
@ -54,23 +58,36 @@ final class FriendRequestView : UIView {
|
|||
let mainStackView = UIStackView()
|
||||
mainStackView.axis = .vertical
|
||||
mainStackView.distribution = .fill
|
||||
mainStackView.addArrangedSubview(topSpacer)
|
||||
mainStackView.layoutMargins = UIEdgeInsets(top: 0, left: Values.largeSpacing, bottom: 0, right: Values.largeSpacing)
|
||||
mainStackView.isLayoutMarginsRelativeArrangement = true
|
||||
mainStackView.addArrangedSubview(spacer1)
|
||||
mainStackView.addArrangedSubview(label)
|
||||
switch kind {
|
||||
case .incoming:
|
||||
mainStackView.addArrangedSubview(spacer2)
|
||||
mainStackView.addArrangedSubview(buttonStackView)
|
||||
let acceptButton = OWSFlatButton.button(title: NSLocalizedString("Accept", comment: ""), font: buttonFont, titleColor: .ows_materialBlue, backgroundColor: .white, target: self, selector: #selector(accept))
|
||||
acceptButton.setBackgroundColors(upColor: .clear, downColor: .clear)
|
||||
acceptButton.autoSetDimension(.height, toSize: buttonHeight)
|
||||
buttonStackView.addArrangedSubview(acceptButton)
|
||||
let declineButton = OWSFlatButton.button(title: NSLocalizedString("Decline", comment: ""), font: buttonFont, titleColor: .ows_destructiveRed, backgroundColor: .white, target: self, selector: #selector(decline))
|
||||
declineButton.setBackgroundColors(upColor: .clear, downColor: .clear)
|
||||
declineButton.autoSetDimension(.height, toSize: buttonHeight)
|
||||
let declineButton = UIButton()
|
||||
declineButton.set(.height, to: Values.mediumButtonHeight)
|
||||
declineButton.layer.cornerRadius = Values.modalButtonCornerRadius
|
||||
declineButton.backgroundColor = Colors.buttonBackground
|
||||
declineButton.titleLabel!.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
declineButton.setTitleColor(Colors.text, for: UIControl.State.normal)
|
||||
declineButton.setTitle(NSLocalizedString("Decline", comment: ""), for: UIControl.State.normal)
|
||||
declineButton.addTarget(self, action: #selector(decline), for: UIControl.Event.touchUpInside)
|
||||
buttonStackView.addArrangedSubview(declineButton)
|
||||
let acceptButton = UIButton()
|
||||
acceptButton.set(.height, to: Values.mediumButtonHeight)
|
||||
acceptButton.layer.cornerRadius = Values.modalButtonCornerRadius
|
||||
acceptButton.backgroundColor = Colors.accent
|
||||
acceptButton.titleLabel!.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
acceptButton.setTitleColor(Colors.text, for: UIControl.State.normal)
|
||||
acceptButton.setTitle(NSLocalizedString("Accept", comment: ""), for: UIControl.State.normal)
|
||||
acceptButton.addTarget(self, action: #selector(accept), for: UIControl.Event.touchUpInside)
|
||||
buttonStackView.addArrangedSubview(acceptButton)
|
||||
case .outgoing: break
|
||||
}
|
||||
addSubview(mainStackView)
|
||||
mainStackView.autoPin(toEdgesOf: self)
|
||||
mainStackView.pin(to: self)
|
||||
updateUI()
|
||||
// Observe friend request status changes
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(handleFriendRequestStatusChangedNotification), name: .messageFriendRequestStatusChanged, object: nil)
|
||||
|
@ -93,18 +110,19 @@ final class FriendRequestView : UIView {
|
|||
case .incoming:
|
||||
guard let message = message as? TSIncomingMessage else { preconditionFailure() }
|
||||
buttonStackView.isHidden = message.friendRequestStatus != .pending
|
||||
spacer2.isHidden = buttonStackView.isHidden
|
||||
let format: String = {
|
||||
switch (message.friendRequestStatus) {
|
||||
case .none, .sendingOrFailed: preconditionFailure()
|
||||
case .pending: return NSLocalizedString("%@ sent you a friend request", comment: "")
|
||||
case .accepted: return NSLocalizedString("You've accepted %@'s friend request", comment: "")
|
||||
case .declined: return NSLocalizedString("You've declined %@'s friend request", comment: "")
|
||||
case .expired: return NSLocalizedString("%@'s friend request has expired", comment: "")
|
||||
case .pending: return NSLocalizedString("%@ sent you a message request", comment: "")
|
||||
case .accepted: return NSLocalizedString("You've accepted %@'s message request", comment: "")
|
||||
case .declined: return NSLocalizedString("You've declined %@'s message request", comment: "")
|
||||
case .expired: return NSLocalizedString("%@'s message request has expired", comment: "")
|
||||
default: preconditionFailure()
|
||||
}
|
||||
}()
|
||||
let contactID = message.authorId
|
||||
let displayName = Environment.shared.contactsManager.profileName(forRecipientId: contactID) ?? contactID
|
||||
let displayName = DisplayNameUtilities.getPrivateChatDisplayName(for: contactID) ?? contactID
|
||||
label.text = String(format: format, displayName)
|
||||
case .outgoing:
|
||||
guard let message = message as? TSOutgoingMessage else { preconditionFailure() }
|
||||
|
@ -112,20 +130,20 @@ final class FriendRequestView : UIView {
|
|||
switch (message.friendRequestStatus) {
|
||||
case .none: preconditionFailure()
|
||||
case .sendingOrFailed: return nil
|
||||
case .pending: return NSLocalizedString("You've sent %@ a friend request", comment: "")
|
||||
case .accepted: return NSLocalizedString("%@ accepted your friend request", comment: "")
|
||||
case .pending: return NSLocalizedString("You've sent %@ a message request", comment: "")
|
||||
case .accepted: return NSLocalizedString("%@ accepted your message request", comment: "")
|
||||
case .declined: preconditionFailure()
|
||||
case .expired: return NSLocalizedString("Your friend request to %@ has expired", comment: "")
|
||||
case .expired: return NSLocalizedString("Your message request to %@ has expired", comment: "")
|
||||
default: preconditionFailure()
|
||||
}
|
||||
}()
|
||||
if let format = format {
|
||||
let contactID = message.thread.contactIdentifier()!
|
||||
let displayName = Environment.shared.contactsManager.profileName(forRecipientId: contactID) ?? contactID
|
||||
let displayName = DisplayNameUtilities.getPrivateChatDisplayName(for: contactID) ?? contactID
|
||||
label.text = String(format: format, displayName)
|
||||
}
|
||||
label.isHidden = (format == nil)
|
||||
topSpacer.isHidden = (label.isHidden)
|
||||
spacer1.isHidden = (label.isHidden)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -144,14 +162,14 @@ final class FriendRequestView : UIView {
|
|||
|
||||
// MARK: Measuring
|
||||
@objc static func calculateHeight(message: TSMessage, conversationStyle: ConversationStyle) -> CGFloat {
|
||||
let width = conversationStyle.contentWidth
|
||||
let width = conversationStyle.contentWidth - 2 * Values.largeSpacing
|
||||
let dummyFriendRequestView = FriendRequestView(message: message)
|
||||
let hasTopSpacer = !dummyFriendRequestView.topSpacer.isHidden
|
||||
let hasTopSpacer = !dummyFriendRequestView.spacer1.isHidden
|
||||
let topSpacing: CGFloat = hasTopSpacer ? 12 : 0
|
||||
let hasLabel = !dummyFriendRequestView.label.isHidden
|
||||
let labelHeight = hasLabel ? dummyFriendRequestView.label.sizeThatFits(CGSize(width: width, height: CGFloat.greatestFiniteMagnitude)).height : 0
|
||||
let hasButtonStackView = dummyFriendRequestView.buttonStackView.superview != nil && !dummyFriendRequestView.buttonStackView.isHidden
|
||||
let buttonHeight = hasButtonStackView ? dummyFriendRequestView.buttonHeight : 0
|
||||
let buttonHeight = hasButtonStackView ? Values.mediumButtonHeight + Values.mediumSpacing : 0 // Values.mediumSpacing is the height of the spacer
|
||||
let totalHeight = topSpacing + labelHeight + buttonHeight
|
||||
return totalHeight.rounded(.up)
|
||||
}
|
|
@ -22,7 +22,7 @@ final class MentionCandidateSelectionView : UIView, UITableViewDataSource, UITab
|
|||
result.register(Cell.self, forCellReuseIdentifier: "Cell")
|
||||
result.separatorStyle = .none
|
||||
result.backgroundColor = .clear
|
||||
result.contentInset = UIEdgeInsets(top: 6, leading: 0, bottom: 0, trailing: 0)
|
||||
result.showsVerticalScrollIndicator = false
|
||||
return result
|
||||
}()
|
||||
|
||||
|
@ -40,6 +40,20 @@ final class MentionCandidateSelectionView : UIView, UITableViewDataSource, UITab
|
|||
private func setUpViewHierarchy() {
|
||||
addSubview(tableView)
|
||||
tableView.pin(to: self)
|
||||
let topSeparator = UIView()
|
||||
topSeparator.backgroundColor = Colors.separator
|
||||
topSeparator.set(.height, to: Values.separatorThickness)
|
||||
addSubview(topSeparator)
|
||||
topSeparator.pin(.leading, to: .leading, of: self)
|
||||
topSeparator.pin(.top, to: .top, of: self)
|
||||
topSeparator.pin(.trailing, to: .trailing, of: self)
|
||||
let bottomSeparator = UIView()
|
||||
bottomSeparator.backgroundColor = Colors.separator
|
||||
bottomSeparator.set(.height, to: Values.separatorThickness)
|
||||
addSubview(bottomSeparator)
|
||||
bottomSeparator.pin(.leading, to: .leading, of: self)
|
||||
bottomSeparator.pin(.trailing, to: .trailing, of: self)
|
||||
bottomSeparator.pin(.bottom, to: .bottom, of: self)
|
||||
}
|
||||
|
||||
// MARK: Data
|
||||
|
@ -53,6 +67,7 @@ final class MentionCandidateSelectionView : UIView, UITableViewDataSource, UITab
|
|||
cell.mentionCandidate = mentionCandidate
|
||||
cell.publicChatServer = publicChatServer
|
||||
cell.publicChatChannel = publicChatChannel
|
||||
cell.separator.isHidden = (indexPath.row == (mentionCandidates.count - 1))
|
||||
return cell
|
||||
}
|
||||
|
||||
|
@ -73,7 +88,7 @@ private extension MentionCandidateSelectionView {
|
|||
var publicChatChannel: UInt64?
|
||||
|
||||
// MARK: Components
|
||||
private lazy var profilePictureImageView = AvatarImageView()
|
||||
private lazy var profilePictureView = ProfilePictureView()
|
||||
|
||||
private lazy var moderatorIconImageView: UIImageView = {
|
||||
let result = UIImageView(image: #imageLiteral(resourceName: "Crown"))
|
||||
|
@ -82,12 +97,19 @@ private extension MentionCandidateSelectionView {
|
|||
|
||||
private lazy var displayNameLabel: UILabel = {
|
||||
let result = UILabel()
|
||||
result.textColor = Theme.primaryColor
|
||||
result.font = UIFont.ows_dynamicTypeSubheadlineClamped
|
||||
result.textColor = Colors.text
|
||||
result.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
result.lineBreakMode = .byTruncatingTail
|
||||
return result
|
||||
}()
|
||||
|
||||
lazy var separator: UIView = {
|
||||
let result = UIView()
|
||||
result.backgroundColor = Colors.separator
|
||||
result.set(.height, to: Values.separatorThickness)
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: Initialization
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
|
@ -100,36 +122,47 @@ private extension MentionCandidateSelectionView {
|
|||
}
|
||||
|
||||
private func setUpViewHierarchy() {
|
||||
// Make the cell transparent
|
||||
backgroundColor = .clear
|
||||
// Set the cell background color
|
||||
backgroundColor = Colors.cellBackground
|
||||
// Set up the highlight color
|
||||
let selectedBackgroundView = UIView()
|
||||
selectedBackgroundView.backgroundColor = Colors.cellBackground // Intentionally not Colors.cellSelected
|
||||
self.selectedBackgroundView = selectedBackgroundView
|
||||
// Set up the profile picture image view
|
||||
profilePictureImageView.set(.width, to: 36)
|
||||
profilePictureImageView.set(.height, to: 36)
|
||||
let profilePictureViewSize = Values.verySmallProfilePictureSize
|
||||
profilePictureView.set(.width, to: profilePictureViewSize)
|
||||
profilePictureView.set(.height, to: profilePictureViewSize)
|
||||
profilePictureView.size = profilePictureViewSize
|
||||
// Set up the main stack view
|
||||
let stackView = UIStackView(arrangedSubviews: [ profilePictureImageView, displayNameLabel ])
|
||||
let stackView = UIStackView(arrangedSubviews: [ profilePictureView, displayNameLabel ])
|
||||
stackView.axis = .horizontal
|
||||
stackView.alignment = .center
|
||||
stackView.spacing = 16
|
||||
stackView.set(.height, to: 36)
|
||||
stackView.spacing = Values.mediumSpacing
|
||||
stackView.set(.height, to: profilePictureViewSize)
|
||||
contentView.addSubview(stackView)
|
||||
stackView.pin(.leading, to: .leading, of: contentView, withInset: 16)
|
||||
stackView.pin(.top, to: .top, of: contentView, withInset: 8)
|
||||
contentView.pin(.trailing, to: .trailing, of: stackView, withInset: 16)
|
||||
contentView.pin(.bottom, to: .bottom, of: stackView, withInset: 8)
|
||||
stackView.set(.width, to: UIScreen.main.bounds.width - 2 * 16)
|
||||
stackView.pin(.leading, to: .leading, of: contentView, withInset: Values.mediumSpacing)
|
||||
stackView.pin(.top, to: .top, of: contentView, withInset: Values.smallSpacing)
|
||||
contentView.pin(.trailing, to: .trailing, of: stackView, withInset: Values.mediumSpacing)
|
||||
contentView.pin(.bottom, to: .bottom, of: stackView, withInset: Values.smallSpacing)
|
||||
stackView.set(.width, to: UIScreen.main.bounds.width - 2 * Values.mediumSpacing)
|
||||
// Set up the moderator icon image view
|
||||
moderatorIconImageView.set(.width, to: 20)
|
||||
moderatorIconImageView.set(.height, to: 20)
|
||||
contentView.addSubview(moderatorIconImageView)
|
||||
moderatorIconImageView.pin(.trailing, to: .trailing, of: profilePictureImageView)
|
||||
moderatorIconImageView.pin(.bottom, to: .bottom, of: profilePictureImageView, withInset: 3.5)
|
||||
moderatorIconImageView.pin(.trailing, to: .trailing, of: profilePictureView)
|
||||
moderatorIconImageView.pin(.bottom, to: .bottom, of: profilePictureView, withInset: 3.5)
|
||||
// Set up the separator
|
||||
addSubview(separator)
|
||||
separator.pin(.leading, to: .leading, of: self)
|
||||
separator.pin(.trailing, to: .trailing, of: self)
|
||||
separator.pin(.bottom, to: .bottom, of: self)
|
||||
}
|
||||
|
||||
// MARK: Updating
|
||||
private func update() {
|
||||
displayNameLabel.text = mentionCandidate.displayName
|
||||
let profilePicture = OWSContactAvatarBuilder(signalId: mentionCandidate.hexEncodedPublicKey, colorName: .blue, diameter: 36).build()
|
||||
profilePictureImageView.image = profilePicture
|
||||
profilePictureView.hexEncodedPublicKey = mentionCandidate.hexEncodedPublicKey
|
||||
profilePictureView.update()
|
||||
if let server = publicChatServer, let channel = publicChatChannel {
|
||||
let isUserModerator = LokiPublicChatAPI.isUserModerator(mentionCandidate.hexEncodedPublicKey, for: channel, on: server)
|
||||
moderatorIconImageView.isHidden = !isUserModerator
|
|
@ -0,0 +1,97 @@
|
|||
|
||||
@objc(LKProfilePictureView)
|
||||
final class ProfilePictureView : UIView {
|
||||
private var imageViewWidthConstraint: NSLayoutConstraint!
|
||||
private var imageViewHeightConstraint: NSLayoutConstraint!
|
||||
@objc var size: CGFloat = 0 // Not an implicitly unwrapped optional due to Obj-C limitations
|
||||
@objc var isRSSFeed = false
|
||||
@objc var hexEncodedPublicKey: String!
|
||||
@objc var additionalHexEncodedPublicKey: String?
|
||||
|
||||
// MARK: Components
|
||||
private lazy var imageView = getImageView()
|
||||
private lazy var additionalImageView = getImageView()
|
||||
|
||||
private lazy var rssLabel: UILabel = {
|
||||
let result = UILabel()
|
||||
result.textColor = Colors.text
|
||||
result.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
result.textAlignment = .center
|
||||
result.text = "RSS"
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: Lifecycle
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
private func setUpViewHierarchy() {
|
||||
// Set up image view
|
||||
addSubview(imageView)
|
||||
imageView.pin(.leading, to: .leading, of: self)
|
||||
imageView.pin(.top, to: .top, of: self)
|
||||
// Set up additional image view
|
||||
addSubview(additionalImageView)
|
||||
additionalImageView.pin(.trailing, to: .trailing, of: self)
|
||||
additionalImageView.pin(.bottom, to: .bottom, of: self)
|
||||
let additionalImageViewSize = Values.smallProfilePictureSize
|
||||
additionalImageView.set(.width, to: additionalImageViewSize)
|
||||
additionalImageView.set(.height, to: additionalImageViewSize)
|
||||
additionalImageView.layer.cornerRadius = additionalImageViewSize / 2
|
||||
// Set up RSS label
|
||||
addSubview(rssLabel)
|
||||
rssLabel.pin(.leading, to: .leading, of: self)
|
||||
rssLabel.pin(.top, to: .top, of: self)
|
||||
rssLabel.autoPinWidth(toWidthOf: imageView)
|
||||
rssLabel.autoPinHeight(toHeightOf: imageView)
|
||||
}
|
||||
|
||||
// MARK: Updating
|
||||
@objc func update() {
|
||||
if let imageViewWidthConstraint = imageViewWidthConstraint, let imageViewHeightConstraint = imageViewHeightConstraint {
|
||||
imageView.removeConstraint(imageViewWidthConstraint)
|
||||
imageView.removeConstraint(imageViewHeightConstraint)
|
||||
}
|
||||
func getProfilePicture(of size: CGFloat, for hexEncodedPublicKey: String) -> UIImage? {
|
||||
guard !hexEncodedPublicKey.isEmpty else { return nil }
|
||||
return OWSProfileManager.shared().profileAvatar(forRecipientId: hexEncodedPublicKey) ?? Identicon.generateIcon(string: hexEncodedPublicKey, size: size)
|
||||
}
|
||||
let size: CGFloat
|
||||
if let additionalHexEncodedPublicKey = additionalHexEncodedPublicKey, !isRSSFeed {
|
||||
size = Values.smallProfilePictureSize
|
||||
imageViewWidthConstraint = imageView.set(.width, to: size)
|
||||
imageViewHeightConstraint = imageView.set(.height, to: size)
|
||||
additionalImageView.isHidden = false
|
||||
additionalImageView.image = getProfilePicture(of: size, for: additionalHexEncodedPublicKey)
|
||||
} else {
|
||||
size = self.size
|
||||
imageViewWidthConstraint = imageView.pin(.trailing, to: .trailing, of: self)
|
||||
imageViewHeightConstraint = imageView.pin(.bottom, to: .bottom, of: self)
|
||||
additionalImageView.isHidden = true
|
||||
additionalImageView.image = nil
|
||||
}
|
||||
imageView.image = isRSSFeed ? nil : getProfilePicture(of: size, for: hexEncodedPublicKey)
|
||||
imageView.backgroundColor = isRSSFeed ? UIColor(hex: 0x353535) : Colors.unimportant
|
||||
imageView.layer.cornerRadius = size / 2
|
||||
rssLabel.isHidden = !isRSSFeed
|
||||
rssLabel.font = size == (Values.largeProfilePictureSize) ? .systemFont(ofSize: Values.largeFontSize) : .systemFont(ofSize: Values.smallFontSize)
|
||||
}
|
||||
|
||||
// MARK: Convenience
|
||||
private func getImageView() -> UIImageView {
|
||||
let result = UIImageView()
|
||||
result.layer.masksToBounds = true
|
||||
result.backgroundColor = Colors.unimportant
|
||||
result.layer.borderColor = Colors.border.cgColor
|
||||
result.layer.borderWidth = Values.borderThickness
|
||||
result.contentMode = .scaleAspectFit
|
||||
return result
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
|
||||
final class SearchBar : UISearchBar {
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
setUpStyle()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
setUpStyle()
|
||||
}
|
||||
|
||||
private func setUpStyle() {
|
||||
searchBarStyle = .minimal // Hide the border around the search bar
|
||||
barStyle = .black // Use Apple's black design as a base
|
||||
tintColor = Colors.accent // The cursor color
|
||||
let searchImage = #imageLiteral(resourceName: "searchbar_search").asTintedImage(color: Colors.searchBarPlaceholder)!
|
||||
setImage(searchImage, for: .search, state: .normal)
|
||||
let clearImage = #imageLiteral(resourceName: "searchbar_clear").asTintedImage(color: Colors.searchBarPlaceholder)!
|
||||
setImage(clearImage, for: .clear, state: .normal)
|
||||
let searchTextField: UITextField
|
||||
if #available(iOS 13, *) {
|
||||
searchTextField = self.searchTextField
|
||||
} else {
|
||||
searchTextField = self.value(forKey: "_searchField") as! UITextField
|
||||
}
|
||||
searchTextField.backgroundColor = Colors.searchBarBackground // The search bar background color
|
||||
searchTextField.textColor = Colors.text
|
||||
searchTextField.attributedPlaceholder = NSAttributedString(string: NSLocalizedString("Search", comment: ""), attributes: [ .foregroundColor : Colors.searchBarPlaceholder ])
|
||||
searchTextField.keyboardAppearance = .dark
|
||||
setPositionAdjustment(UIOffset(horizontal: 4, vertical: 0), for: UISearchBar.Icon.search)
|
||||
searchTextPositionAdjustment = UIOffset(horizontal: 2, vertical: 0)
|
||||
setPositionAdjustment(UIOffset(horizontal: -4, vertical: 0), for: UISearchBar.Icon.clear)
|
||||
searchTextField.removeConstraints(searchTextField.constraints)
|
||||
searchTextField.pin(.leading, to: .leading, of: searchTextField.superview!, withInset: Values.mediumSpacing + 3)
|
||||
searchTextField.pin(.top, to: .top, of: searchTextField.superview!, withInset: 10)
|
||||
searchTextField.superview!.pin(.trailing, to: .trailing, of: searchTextField, withInset: Values.mediumSpacing + 3)
|
||||
searchTextField.superview!.pin(.bottom, to: .bottom, of: searchTextField, withInset: 10)
|
||||
searchTextField.set(.height, to: Values.searchBarHeight)
|
||||
searchTextField.set(.width, to: UIScreen.main.bounds.width - 2 * Values.mediumSpacing)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
|
||||
final class SeedReminderView : UIView {
|
||||
private let hasContinueButton: Bool
|
||||
var title = NSAttributedString(string: "") { didSet { titleLabel.attributedText = title } }
|
||||
var subtitle = "" { didSet { subtitleLabel.text = subtitle } }
|
||||
var delegate: SeedReminderViewDelegate?
|
||||
|
||||
// MARK: Components
|
||||
private lazy var progressIndicatorView: UIProgressView = {
|
||||
let result = UIProgressView()
|
||||
result.progressViewStyle = .bar
|
||||
result.progressTintColor = Colors.accent
|
||||
result.backgroundColor = UIColor(hex: 0xFFFFFF).withAlphaComponent(0.1)
|
||||
result.set(.height, to: Values.progressBarThickness)
|
||||
return result
|
||||
}()
|
||||
|
||||
lazy var titleLabel: UILabel = {
|
||||
let result = UILabel()
|
||||
result.textColor = Colors.text
|
||||
result.font = .boldSystemFont(ofSize: Values.smallFontSize)
|
||||
result.lineBreakMode = .byTruncatingTail
|
||||
return result
|
||||
}()
|
||||
|
||||
lazy var subtitleLabel: UILabel = {
|
||||
let result = UILabel()
|
||||
result.textColor = Colors.text.withAlphaComponent(Values.unimportantElementOpacity)
|
||||
result.font = .systemFont(ofSize: Values.verySmallFontSize)
|
||||
result.lineBreakMode = .byTruncatingTail
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: Lifecycle
|
||||
init(hasContinueButton: Bool) {
|
||||
self.hasContinueButton = hasContinueButton
|
||||
super.init(frame: CGRect.zero)
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
preconditionFailure("Use init(hasContinueButton:) instead.")
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
preconditionFailure("Use init(hasContinueButton:) instead.")
|
||||
}
|
||||
|
||||
private func setUpViewHierarchy() {
|
||||
// Set background color
|
||||
backgroundColor = Colors.cellBackground
|
||||
// Set up label stack view
|
||||
let labelStackView = UIStackView(arrangedSubviews: [ titleLabel, subtitleLabel ])
|
||||
labelStackView.axis = .vertical
|
||||
labelStackView.spacing = 4
|
||||
// Set up button
|
||||
let button = Button(style: .prominentOutline, size: .small)
|
||||
button.setTitle(NSLocalizedString("Continue", comment: ""), for: UIControl.State.normal)
|
||||
button.set(.width, to: 80)
|
||||
button.addTarget(self, action: #selector(handleContinueButtonTapped), for: UIControl.Event.touchUpInside)
|
||||
// Set up content stack view
|
||||
let contentStackView = UIStackView(arrangedSubviews: [ labelStackView ])
|
||||
if hasContinueButton {
|
||||
contentStackView.addArrangedSubview(UIView.hStretchingSpacer())
|
||||
contentStackView.addArrangedSubview(button)
|
||||
}
|
||||
contentStackView.axis = .horizontal
|
||||
contentStackView.spacing = 4
|
||||
contentStackView.alignment = .center
|
||||
contentStackView.layoutMargins = UIEdgeInsets(top: 0, leading: Values.mediumSpacing + Values.accentLineThickness, bottom: 0, trailing: Values.mediumSpacing)
|
||||
contentStackView.isLayoutMarginsRelativeArrangement = true
|
||||
// Set up separator
|
||||
let separator = UIView()
|
||||
separator.set(.height, to: Values.separatorThickness)
|
||||
separator.backgroundColor = Colors.separator
|
||||
// Set up stack view
|
||||
let stackView = UIStackView(arrangedSubviews: [ progressIndicatorView, contentStackView, separator ])
|
||||
stackView.axis = .vertical
|
||||
stackView.spacing = Values.mediumSpacing
|
||||
addSubview(stackView)
|
||||
stackView.pin(to: self)
|
||||
}
|
||||
|
||||
// MARK: Updating
|
||||
func setProgress(_ progress: Float, animated isAnimated: Bool) {
|
||||
progressIndicatorView.setProgress(progress, animated: isAnimated)
|
||||
}
|
||||
|
||||
// MARK: Updating
|
||||
@objc private func handleContinueButtonTapped() {
|
||||
delegate?.handleContinueButtonTapped(from: self)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
|
||||
protocol SeedReminderViewDelegate {
|
||||
|
||||
func handleContinueButtonTapped(from seedReminderView: SeedReminderView)
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
|
||||
final class Separator : UIView {
|
||||
private let title: String
|
||||
|
||||
// MARK: Components
|
||||
private lazy var titleLabel: UILabel = {
|
||||
let result = UILabel()
|
||||
result.textColor = Colors.text.withAlphaComponent(Values.unimportantElementOpacity)
|
||||
result.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
result.textAlignment = .center
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var lineLayer: CAShapeLayer = {
|
||||
let result = CAShapeLayer()
|
||||
result.lineWidth = Values.separatorThickness
|
||||
result.strokeColor = Colors.separator.cgColor
|
||||
result.fillColor = UIColor.clear.cgColor
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: Initialization
|
||||
init(title: String) {
|
||||
self.title = title
|
||||
super.init(frame: CGRect.zero)
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
preconditionFailure("Use init(title:) instead.")
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
preconditionFailure("Use init(title:) instead.")
|
||||
}
|
||||
|
||||
private func setUpViewHierarchy() {
|
||||
titleLabel.text = title
|
||||
addSubview(titleLabel)
|
||||
titleLabel.center(.horizontal, in: self)
|
||||
titleLabel.center(.vertical, in: self)
|
||||
layer.insertSublayer(lineLayer, at: 0)
|
||||
set(.height, to: Values.separatorLabelHeight)
|
||||
}
|
||||
|
||||
// MARK: Updating
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
updateLineLayer()
|
||||
}
|
||||
|
||||
private func updateLineLayer() {
|
||||
let w = width()
|
||||
let h = height()
|
||||
let path = UIBezierPath()
|
||||
path.move(to: CGPoint(x: 0, y: h / 2))
|
||||
let titleLabelFrame = titleLabel.frame.insetBy(dx: -10, dy: -6)
|
||||
path.addLine(to: CGPoint(x: titleLabelFrame.origin.x, y: h / 2))
|
||||
let oval = UIBezierPath(roundedRect: titleLabelFrame, cornerRadius: Values.separatorLabelHeight / 2)
|
||||
path.append(oval)
|
||||
path.move(to: CGPoint(x: titleLabelFrame.origin.x + titleLabelFrame.width, y: h / 2))
|
||||
path.addLine(to: CGPoint(x: w, y: h / 2))
|
||||
path.close()
|
||||
lineLayer.path = path.cgPath
|
||||
}
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
|
||||
final class TabBar : UIView {
|
||||
private let tabs: [Tab]
|
||||
private var accentLineViewHorizontalCenteringConstraint: NSLayoutConstraint!
|
||||
private var accentLineViewWidthConstraint: NSLayoutConstraint!
|
||||
|
||||
// MARK: Components
|
||||
private lazy var tabLabels: [UILabel] = tabs.map { tab in
|
||||
let result = UILabel()
|
||||
result.font = .boldSystemFont(ofSize: Values.mediumFontSize)
|
||||
result.textColor = Colors.text.withAlphaComponent(Values.unimportantElementOpacity)
|
||||
result.textAlignment = .center
|
||||
result.text = tab.title
|
||||
result.set(.height, to: Values.tabBarHeight - Values.separatorThickness - Values.accentLineThickness)
|
||||
return result
|
||||
}
|
||||
|
||||
private lazy var accentLineView: UIView = {
|
||||
let result = UIView()
|
||||
result.backgroundColor = Colors.accent
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: Types
|
||||
struct Tab {
|
||||
let title: String
|
||||
let onTap: () -> Void
|
||||
}
|
||||
|
||||
// MARK: Lifecycle
|
||||
init(tabs: [Tab]) {
|
||||
self.tabs = tabs
|
||||
super.init(frame: CGRect.zero)
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
preconditionFailure("Use init(tabs:) instead.")
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
preconditionFailure("Use init(tabs:) instead.")
|
||||
}
|
||||
|
||||
private func setUpViewHierarchy() {
|
||||
set(.height, to: Values.tabBarHeight)
|
||||
tabLabels.forEach { tabLabel in
|
||||
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTabLabelTapped(_:)))
|
||||
tabLabel.addGestureRecognizer(tapGestureRecognizer)
|
||||
}
|
||||
let tabLabelStackView = UIStackView(arrangedSubviews: tabLabels)
|
||||
tabLabelStackView.axis = .horizontal
|
||||
tabLabelStackView.distribution = .fillEqually
|
||||
tabLabelStackView.spacing = Values.mediumSpacing
|
||||
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTabLabelTapped(_:)))
|
||||
tabLabelStackView.addGestureRecognizer(tapGestureRecognizer)
|
||||
tabLabelStackView.set(.height, to: Values.tabBarHeight - Values.separatorThickness - Values.accentLineThickness)
|
||||
addSubview(tabLabelStackView)
|
||||
let separator = UIView()
|
||||
separator.backgroundColor = Colors.separator
|
||||
separator.set(.height, to: Values.separatorThickness)
|
||||
addSubview(separator)
|
||||
accentLineView.set(.height, to: Values.accentLineThickness)
|
||||
addSubview(accentLineView)
|
||||
tabLabelStackView.pin(.leading, to: .leading, of: self)
|
||||
tabLabelStackView.pin(.top, to: .top, of: self)
|
||||
pin(.trailing, to: .trailing, of: tabLabelStackView)
|
||||
separator.pin(.leading, to: .leading, of: self)
|
||||
separator.pin(.top, to: .bottom, of: tabLabelStackView)
|
||||
pin(.trailing, to: .trailing, of: separator)
|
||||
accentLineView.translatesAutoresizingMaskIntoConstraints = false
|
||||
selectTab(at: 0, withAnimatedTransition: false)
|
||||
accentLineView.pin(.top, to: .bottom, of: separator)
|
||||
pin(.bottom, to: .bottom, of: accentLineView)
|
||||
}
|
||||
|
||||
// MARK: Updating
|
||||
func selectTab(at index: Int, withAnimatedTransition isAnimated: Bool = true) {
|
||||
let tabLabel = tabLabels[index]
|
||||
accentLineViewHorizontalCenteringConstraint?.isActive = false
|
||||
accentLineViewHorizontalCenteringConstraint = accentLineView.centerXAnchor.constraint(equalTo: tabLabel.centerXAnchor)
|
||||
accentLineViewHorizontalCenteringConstraint.isActive = true
|
||||
accentLineViewWidthConstraint?.isActive = false
|
||||
accentLineViewWidthConstraint = accentLineView.widthAnchor.constraint(equalTo: tabLabel.widthAnchor)
|
||||
accentLineViewWidthConstraint.isActive = true
|
||||
var tabLabelsCopy = tabLabels
|
||||
tabLabelsCopy.remove(at: index)
|
||||
UIView.animate(withDuration: isAnimated ? 0.25 : 0) {
|
||||
tabLabel.textColor = Colors.text
|
||||
tabLabelsCopy.forEach { $0.textColor = Colors.text.withAlphaComponent(Values.unimportantElementOpacity) }
|
||||
self.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Interaction
|
||||
@objc private func handleTabLabelTapped(_ sender: UITapGestureRecognizer) {
|
||||
guard let tabLabel = tabLabels.first(where: { $0.bounds.contains(sender.location(in: $0)) }), let index = tabLabels.firstIndex(of: tabLabel) else { return }
|
||||
selectTab(at: index)
|
||||
let tab = tabs[index]
|
||||
tab.onTap()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
|
||||
final class TextField : UITextField {
|
||||
private let usesDefaultHeight: Bool
|
||||
|
||||
init(placeholder: String, usesDefaultHeight: Bool = true) {
|
||||
self.usesDefaultHeight = usesDefaultHeight
|
||||
super.init(frame: CGRect.zero)
|
||||
self.placeholder = placeholder
|
||||
setUpStyle()
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
preconditionFailure("Use init(placeholder:) instead.")
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
preconditionFailure("Use init(placeholder:) instead.")
|
||||
}
|
||||
|
||||
private func setUpStyle() {
|
||||
textColor = Colors.text
|
||||
font = .systemFont(ofSize: Values.smallFontSize)
|
||||
let placeholder = NSMutableAttributedString(string: self.placeholder!)
|
||||
let placeholderColor = Colors.text.withAlphaComponent(Values.unimportantElementOpacity)
|
||||
placeholder.addAttribute(.foregroundColor, value: placeholderColor, range: NSRange(location: 0, length: placeholder.length))
|
||||
attributedPlaceholder = placeholder
|
||||
tintColor = Colors.accent
|
||||
keyboardAppearance = .dark
|
||||
if usesDefaultHeight {
|
||||
set(.height, to: Values.textFieldHeight)
|
||||
}
|
||||
layer.borderColor = Colors.border.withAlphaComponent(Values.textFieldBorderOpacity).cgColor
|
||||
layer.borderWidth = Values.borderThickness
|
||||
layer.cornerRadius = Values.textFieldCornerRadius
|
||||
}
|
||||
|
||||
override func textRect(forBounds bounds: CGRect) -> CGRect {
|
||||
if usesDefaultHeight {
|
||||
return bounds.insetBy(dx: Values.largeSpacing, dy: Values.largeSpacing)
|
||||
} else {
|
||||
return bounds.insetBy(dx: Values.mediumSpacing, dy: Values.smallSpacing)
|
||||
}
|
||||
}
|
||||
|
||||
override func editingRect(forBounds bounds: CGRect) -> CGRect {
|
||||
if usesDefaultHeight {
|
||||
return bounds.insetBy(dx: Values.largeSpacing, dy: Values.largeSpacing)
|
||||
} else {
|
||||
return bounds.insetBy(dx: Values.mediumSpacing, dy: Values.smallSpacing)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,117 +0,0 @@
|
|||
|
||||
@objc(LKNewConversationVC)
|
||||
final class NewConversationVC : OWSViewController, OWSQRScannerDelegate {
|
||||
|
||||
// MARK: Components
|
||||
private lazy var publicKeyTextField: UITextField = {
|
||||
let result = UITextField()
|
||||
result.textColor = Theme.primaryColor
|
||||
result.font = .ows_dynamicTypeBodyClamped
|
||||
let placeholder = NSMutableAttributedString(string: NSLocalizedString("Enter a Public Key", comment: ""))
|
||||
placeholder.addAttribute(.foregroundColor, value: Theme.placeholderColor, range: NSRange(location: 0, length: placeholder.length))
|
||||
result.attributedPlaceholder = placeholder
|
||||
result.tintColor = .lokiGreen()
|
||||
result.keyboardAppearance = .dark
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: Lifecycle
|
||||
override func viewDidLoad() {
|
||||
// Background color & margins
|
||||
view.backgroundColor = Theme.backgroundColor
|
||||
view.layoutMargins = .zero
|
||||
// Navigation bar
|
||||
navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .stop, target: self, action: #selector(close))
|
||||
title = NSLocalizedString("New Conversation", comment: "")
|
||||
// Separator
|
||||
let separator = UIView()
|
||||
separator.autoSetDimension(.height, toSize: 1 / UIScreen.main.scale)
|
||||
separator.backgroundColor = Theme.hairlineColor
|
||||
// Explanation label
|
||||
let explanationLabel = UILabel()
|
||||
explanationLabel.textColor = Theme.primaryColor
|
||||
explanationLabel.font = UIFont.ows_dynamicTypeSubheadlineClamped
|
||||
explanationLabel.text = NSLocalizedString("Enter the public key of the person you'd like to securely message. They can share their public key with you by going into Loki Messenger's in-app settings and clicking \"Share Public Key\".", comment: "")
|
||||
explanationLabel.numberOfLines = 0
|
||||
explanationLabel.lineBreakMode = .byWordWrapping
|
||||
// QR code button
|
||||
let qrCodeButtonFont = UIFont.ows_dynamicTypeBodyClamped.ows_mediumWeight()
|
||||
let qrCodeButtonHeight = qrCodeButtonFont.pointSize * 48 / 17
|
||||
let qrCodeButton = OWSFlatButton.button(title: NSLocalizedString("Scan a QR Code Instead", comment: ""), font: qrCodeButtonFont, titleColor: .lokiGreen(), backgroundColor: .clear, target: self, selector: #selector(scanQRCode))
|
||||
qrCodeButton.setBackgroundColors(upColor: .clear, downColor: .clear)
|
||||
qrCodeButton.autoSetDimension(.height, toSize: qrCodeButtonHeight)
|
||||
qrCodeButton.button.contentHorizontalAlignment = .left
|
||||
// Next button
|
||||
let nextButtonFont = UIFont.ows_dynamicTypeBodyClamped.ows_mediumWeight()
|
||||
let nextButtonHeight = nextButtonFont.pointSize * 48 / 17
|
||||
let nextButton = OWSFlatButton.button(title: NSLocalizedString("Next", comment: ""), font: nextButtonFont, titleColor: .white, backgroundColor: .lokiGreen(), target: self, selector: #selector(handleNextButtonTapped))
|
||||
nextButton.autoSetDimension(.height, toSize: nextButtonHeight)
|
||||
// Stack view
|
||||
let stackView = UIStackView(arrangedSubviews: [
|
||||
publicKeyTextField,
|
||||
UIView.spacer(withHeight: 8),
|
||||
separator,
|
||||
UIView.spacer(withHeight: 24),
|
||||
explanationLabel,
|
||||
UIView.spacer(withHeight: 8),
|
||||
qrCodeButton,
|
||||
UIView.vStretchingSpacer(),
|
||||
nextButton
|
||||
])
|
||||
stackView.axis = .vertical
|
||||
stackView.alignment = .fill
|
||||
stackView.layoutMargins = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16)
|
||||
stackView.isLayoutMarginsRelativeArrangement = true
|
||||
view.addSubview(stackView)
|
||||
stackView.autoPinWidthToSuperview()
|
||||
stackView.autoPin(toTopLayoutGuideOf: self, withInset: 0)
|
||||
autoPinView(toBottomOfViewControllerOrKeyboard: stackView, avoidNotch: true)
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
publicKeyTextField.becomeFirstResponder()
|
||||
}
|
||||
|
||||
// MARK: Interaction
|
||||
@objc private func close() {
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@objc private func scanQRCode() {
|
||||
ows_ask(forCameraPermissions: { [weak self] hasCameraAccess in
|
||||
if hasCameraAccess {
|
||||
let message = NSLocalizedString("Scan the QR code of the person you'd like to securely message. They can find their QR code by going into Loki Messenger's in-app settings and clicking \"Show QR Code\".", comment: "")
|
||||
let scanQRCodeWrapperVC = ScanQRCodeWrapperVC(message: message)
|
||||
scanQRCodeWrapperVC.delegate = self
|
||||
self?.navigationController!.pushViewController(scanQRCodeWrapperVC, animated: true)
|
||||
} else {
|
||||
// Do nothing
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func controller(_ controller: OWSQRCodeScanningViewController, didDetectQRCodeWith string: String) {
|
||||
Analytics.shared.track("QR Code Scanned")
|
||||
let hexEncodedPublicKey = string
|
||||
startNewConversationIfPossible(with: hexEncodedPublicKey)
|
||||
}
|
||||
|
||||
@objc private func handleNextButtonTapped() {
|
||||
let hexEncodedPublicKey = publicKeyTextField.text?.trimmingCharacters(in: .whitespaces) ?? ""
|
||||
startNewConversationIfPossible(with: hexEncodedPublicKey)
|
||||
}
|
||||
|
||||
private func startNewConversationIfPossible(with hexEncodedPublicKey: String) {
|
||||
if !ECKeyPair.isValidHexEncodedPublicKey(candidate: hexEncodedPublicKey) {
|
||||
let alert = UIAlertController(title: NSLocalizedString("Invalid Public Key", comment: ""), message: NSLocalizedString("Please check the public key you entered and try again.", comment: ""), preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
|
||||
presentAlert(alert)
|
||||
} else {
|
||||
let thread = TSContactThread.getOrCreateThread(contactId: hexEncodedPublicKey)
|
||||
Analytics.shared.track("New Conversation Started")
|
||||
SignalApp.shared().presentConversation(for: thread, action: .compose, animated: false)
|
||||
presentingViewController!.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,100 +0,0 @@
|
|||
|
||||
@objc(LKNewPublicChatVC)
|
||||
final class NewPublicChatVC : OWSViewController {
|
||||
|
||||
// MARK: Components
|
||||
private lazy var urlTextField: UITextField = {
|
||||
let result = UITextField()
|
||||
result.textColor = Theme.primaryColor
|
||||
result.font = .ows_dynamicTypeBodyClamped
|
||||
let placeholder = NSMutableAttributedString(string: NSLocalizedString("Enter a URL", comment: ""))
|
||||
placeholder.addAttribute(.foregroundColor, value: Theme.placeholderColor, range: NSRange(location: 0, length: placeholder.length))
|
||||
result.attributedPlaceholder = placeholder
|
||||
result.tintColor = .lokiGreen()
|
||||
result.keyboardAppearance = .dark
|
||||
result.keyboardType = .URL
|
||||
result.autocapitalizationType = .none
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var addButton = OWSFlatButton.button(title: NSLocalizedString("Add", comment: ""), font: UIFont.ows_dynamicTypeBodyClamped.ows_mediumWeight(), titleColor: .white, backgroundColor: .lokiGreen(), target: self, selector: #selector(handleAddButtonTapped))
|
||||
|
||||
// MARK: Lifecycle
|
||||
override func viewDidLoad() {
|
||||
// Background color & margins
|
||||
view.backgroundColor = Theme.backgroundColor
|
||||
view.layoutMargins = .zero
|
||||
// Navigation bar
|
||||
navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .stop, target: self, action: #selector(close))
|
||||
title = NSLocalizedString("Add Public Chat", comment: "")
|
||||
// Separator
|
||||
let separator = UIView()
|
||||
separator.autoSetDimension(.height, toSize: 1 / UIScreen.main.scale)
|
||||
separator.backgroundColor = Theme.hairlineColor
|
||||
// Explanation label
|
||||
let explanationLabel = UILabel()
|
||||
explanationLabel.textColor = Theme.primaryColor
|
||||
explanationLabel.font = UIFont.ows_dynamicTypeSubheadlineClamped
|
||||
explanationLabel.text = NSLocalizedString("Enter the URL of the public chat you'd like to join. The Loki Public Chat URL is https://chat.lokinet.org.", comment: "")
|
||||
explanationLabel.numberOfLines = 0
|
||||
explanationLabel.lineBreakMode = .byWordWrapping
|
||||
// Add button
|
||||
let addButtonHeight = addButton.button.titleLabel!.font.pointSize * 48 / 17
|
||||
addButton.autoSetDimension(.height, toSize: addButtonHeight)
|
||||
updateAddButton(isConnecting: false)
|
||||
// Stack view
|
||||
let stackView = UIStackView(arrangedSubviews: [ urlTextField, UIView.spacer(withHeight: 8), separator, UIView.spacer(withHeight: 24), explanationLabel, UIView.vStretchingSpacer(), addButton ])
|
||||
stackView.axis = .vertical
|
||||
stackView.alignment = .fill
|
||||
stackView.layoutMargins = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16)
|
||||
stackView.isLayoutMarginsRelativeArrangement = true
|
||||
view.addSubview(stackView)
|
||||
stackView.autoPinWidthToSuperview()
|
||||
stackView.autoPin(toTopLayoutGuideOf: self, withInset: 0)
|
||||
autoPinView(toBottomOfViewControllerOrKeyboard: stackView, avoidNotch: true)
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
urlTextField.becomeFirstResponder()
|
||||
}
|
||||
|
||||
// MARK: Updating
|
||||
private func updateAddButton(isConnecting: Bool) {
|
||||
addButton.setEnabled(!isConnecting)
|
||||
addButton.setTitle(isConnecting ? NSLocalizedString("Connecting...", comment: "") : NSLocalizedString("Add", comment: ""))
|
||||
}
|
||||
|
||||
// MARK: General
|
||||
private func showError(title: String, message: String = "") {
|
||||
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
|
||||
presentAlert(alert)
|
||||
}
|
||||
|
||||
// MARK: Interaction
|
||||
@objc private func close() {
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@objc private func handleAddButtonTapped() {
|
||||
let uncheckedURL = (urlTextField.text?.trimmingCharacters(in: .whitespaces) ?? "").lowercased().replacingOccurrences(of: "http://", with: "https://")
|
||||
guard let url = URL(string: uncheckedURL), let scheme = url.scheme, scheme == "https", url.host != nil else {
|
||||
return showError(title: NSLocalizedString("Invalid URL", comment: ""), message: NSLocalizedString("Please check the URL you entered and try again.", comment: ""))
|
||||
}
|
||||
updateAddButton(isConnecting: true)
|
||||
let channelID: UInt64 = 1
|
||||
let urlAsString = url.absoluteString
|
||||
let displayName = OWSProfileManager.shared().localProfileName()
|
||||
LokiPublicChatManager.shared.addChat(server: urlAsString, channel: channelID)
|
||||
.done(on: .main) { [weak self] _ in
|
||||
let _ = LokiPublicChatAPI.getMessages(for: channelID, on: urlAsString)
|
||||
let _ = LokiPublicChatAPI.setDisplayName(to: displayName, on: urlAsString)
|
||||
self?.presentingViewController!.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
.catch(on: .main) { [weak self] _ in
|
||||
self?.updateAddButton(isConnecting: false)
|
||||
self?.showError(title: NSLocalizedString("Couldn't Connect", comment: ""))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,77 +0,0 @@
|
|||
|
||||
final class DisplayNameVC : OnboardingBaseViewController {
|
||||
|
||||
private lazy var userNameTextField: UITextField = {
|
||||
let result = UITextField()
|
||||
result.textColor = Theme.primaryColor
|
||||
result.font = .ows_dynamicTypeBodyClamped
|
||||
result.textAlignment = .center
|
||||
let placeholder = NSMutableAttributedString(string: NSLocalizedString("Display Name", comment: ""))
|
||||
placeholder.addAttribute(.foregroundColor, value: Theme.placeholderColor, range: NSRange(location: 0, length: placeholder.length))
|
||||
result.attributedPlaceholder = placeholder
|
||||
result.tintColor = .lokiGreen()
|
||||
result.accessibilityIdentifier = "onboarding.accountDetailsStep.userNameTextField"
|
||||
result.keyboardAppearance = .dark
|
||||
return result
|
||||
}()
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.backgroundColor = Theme.backgroundColor
|
||||
view.layoutMargins = .zero
|
||||
let titleLabel = self.createTitleLabel(text: NSLocalizedString("Create Your Loki Messenger Account", comment: ""))
|
||||
titleLabel.accessibilityIdentifier = "onboarding.accountDetailsStep.titleLabel"
|
||||
let topSpacer = UIView.vStretchingSpacer()
|
||||
let displayNameLabel = createExplanationLabel(text: NSLocalizedString("Enter a name to be shown to your contacts", comment: ""))
|
||||
displayNameLabel.accessibilityIdentifier = "onboarding.accountDetailsStep.displayNameLabel"
|
||||
let bottomSpacer = UIView.vStretchingSpacer()
|
||||
let nextButton = createButton(title: NSLocalizedString("Next", comment: ""), selector: #selector(handleNextButtonPressed))
|
||||
nextButton.accessibilityIdentifier = "onboarding.accountDetailsStep.nextButton"
|
||||
let stackView = UIStackView(arrangedSubviews: [
|
||||
titleLabel,
|
||||
topSpacer,
|
||||
displayNameLabel,
|
||||
UIView.spacer(withHeight: 8),
|
||||
userNameTextField,
|
||||
bottomSpacer,
|
||||
nextButton
|
||||
])
|
||||
stackView.axis = .vertical
|
||||
stackView.alignment = .fill
|
||||
stackView.layoutMargins = UIEdgeInsets(top: 32, left: 32, bottom: 32, right: 32)
|
||||
stackView.isLayoutMarginsRelativeArrangement = true
|
||||
view.addSubview(stackView)
|
||||
stackView.autoPinWidthToSuperview()
|
||||
stackView.autoPin(toTopLayoutGuideOf: self, withInset: 0)
|
||||
autoPinView(toBottomOfViewControllerOrKeyboard: stackView, avoidNotch: true)
|
||||
topSpacer.autoMatch(.height, to: .height, of: bottomSpacer)
|
||||
Analytics.shared.track("Display Name Screen Viewed")
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
userNameTextField.becomeFirstResponder()
|
||||
}
|
||||
|
||||
@objc private func handleNextButtonPressed() {
|
||||
let displayName = userNameTextField.text!.ows_stripped()
|
||||
guard !displayName.isEmpty else {
|
||||
return OWSAlerts.showErrorAlert(message: NSLocalizedString("Please pick a display name", comment: ""))
|
||||
}
|
||||
guard displayName.allSatisfy({ "0"..."9" ~= $0 || "a"..."z" ~= $0 || "A"..."Z" ~= $0 || $0 == "_" || $0 == " " }) else {
|
||||
return OWSAlerts.showErrorAlert(message: NSLocalizedString("Please pick a display name that consists of only a-z, A-Z, 0-9 and _ characters", comment: ""))
|
||||
}
|
||||
guard !OWSProfileManager.shared().isProfileNameTooLong(displayName) else {
|
||||
return OWSAlerts.showErrorAlert(message: NSLocalizedString("Please pick a shorter display name", comment: ""))
|
||||
}
|
||||
TSAccountManager.sharedInstance().didRegister()
|
||||
UserDefaults.standard.set(true, forKey: "didUpdateForMainnet")
|
||||
onboardingController.verificationDidComplete(fromView: self)
|
||||
let appDelegate = UIApplication.shared.delegate as! AppDelegate
|
||||
appDelegate.setUpDefaultPublicChatsIfNeeded()
|
||||
appDelegate.createRSSFeedsIfNeeded()
|
||||
LokiPublicChatManager.shared.startPollersIfNeeded()
|
||||
appDelegate.startRSSFeedPollersIfNeeded()
|
||||
OWSProfileManager.shared().updateLocalProfileName(displayName, avatarImage: nil, success: { }, failure: { }) // Try to save the user name but ignore the result
|
||||
}
|
||||
}
|
|
@ -1,434 +0,0 @@
|
|||
|
||||
final class SeedVC : OnboardingBaseViewController, DeviceLinkingModalDelegate, OWSQRScannerDelegate {
|
||||
private var mode: Mode = .register { didSet { if mode != oldValue { handleModeChanged() } } }
|
||||
private var seed: Data! { didSet { updateMnemonic() } }
|
||||
private var mnemonic: String! { didSet { handleMnemonicChanged() } }
|
||||
|
||||
// MARK: Components
|
||||
private lazy var registerStackView: UIStackView = {
|
||||
let result = UIStackView(arrangedSubviews: [ explanationLabel1, UIView.spacer(withHeight: 32), mnemonicLabel, UIView.spacer(withHeight: 24), copyButton, restoreButton1, linkButton1 ])
|
||||
result.accessibilityIdentifier = "onboarding.keyPairStep.registerStackView"
|
||||
result.axis = .vertical
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var explanationLabel1: UILabel = {
|
||||
let result = createExplanationLabel(text: NSLocalizedString("Please save the seed below in a safe location. It can be used to restore your account if you lose access, or to migrate your account to a new device.", comment: ""))
|
||||
result.accessibilityIdentifier = "onboarding.keyPairStep.explanationLabel1"
|
||||
result.textColor = Theme.primaryColor
|
||||
var fontTraits = result.font.fontDescriptor.symbolicTraits
|
||||
fontTraits.insert(.traitBold)
|
||||
result.font = UIFont(descriptor: result.font.fontDescriptor.withSymbolicTraits(fontTraits)!, size: result.font.pointSize)
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var mnemonicLabel: UILabel = {
|
||||
let result = createExplanationLabel(text: "")
|
||||
result.accessibilityIdentifier = "onboarding.keyPairStep.mnemonicLabel"
|
||||
result.alpha = 0.8
|
||||
var fontTraits = result.font.fontDescriptor.symbolicTraits
|
||||
fontTraits.insert(.traitItalic)
|
||||
result.font = UIFont(descriptor: result.font.fontDescriptor.withSymbolicTraits(fontTraits)!, size: result.font.pointSize)
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var copyButton: OWSFlatButton = {
|
||||
let result = createLinkButton(title: NSLocalizedString("Copy", comment: ""), selector: #selector(copyMnemonic))
|
||||
result.accessibilityIdentifier = "onboarding.keyPairStep.copyButton"
|
||||
result.setBackgroundColors(upColor: .clear, downColor: .clear)
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var restoreButton1: OWSFlatButton = {
|
||||
let result = createLinkButton(title: NSLocalizedString("Restore Using Seed", comment: ""), selector: #selector(handleSwitchModeButton1Tapped))
|
||||
result.accessibilityIdentifier = "onboarding.keyPairStep.restoreButton1"
|
||||
result.setBackgroundColors(upColor: .clear, downColor: .clear)
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var linkButton1: OWSFlatButton = {
|
||||
let result = createLinkButton(title: NSLocalizedString("Link Device", comment: ""), selector: #selector(handleSwitchModeButton2Tapped))
|
||||
result.accessibilityIdentifier = "onboarding.keyPairStep.linkButton1"
|
||||
result.setBackgroundColors(upColor: .clear, downColor: .clear)
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var restoreStackView: UIStackView = {
|
||||
let result = UIStackView(arrangedSubviews: [ explanationLabel2, UIView.spacer(withHeight: 32), errorLabel1, errorLabel1Spacer, mnemonicTextField, UIView.spacer(withHeight: 24), registerButton1, linkButton2 ])
|
||||
result.accessibilityIdentifier = "onboarding.keyPairStep.restoreStackView"
|
||||
result.axis = .vertical
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var explanationLabel2: UILabel = {
|
||||
let result = createExplanationLabel(text: NSLocalizedString("Restore your account by entering your seed below.", comment: ""))
|
||||
result.accessibilityIdentifier = "onboarding.keyPairStep.explanationLabel2"
|
||||
result.textColor = Theme.primaryColor
|
||||
var fontTraits = result.font.fontDescriptor.symbolicTraits
|
||||
fontTraits.insert(.traitBold)
|
||||
result.font = UIFont(descriptor: result.font.fontDescriptor.withSymbolicTraits(fontTraits)!, size: result.font.pointSize)
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var errorLabel1: UILabel = {
|
||||
let result = createExplanationLabel(text: "")
|
||||
result.accessibilityIdentifier = "onboarding.keyPairStep.errorLabel1"
|
||||
result.textColor = UIColor.red
|
||||
var fontTraits = result.font.fontDescriptor.symbolicTraits
|
||||
fontTraits.insert(.traitBold)
|
||||
result.font = UIFont(descriptor: result.font.fontDescriptor.withSymbolicTraits(fontTraits)!, size: 12)
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var errorLabel1Spacer: UIView = {
|
||||
let result = UIView.spacer(withHeight: 32)
|
||||
result.isHidden = true
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var mnemonicTextField: UITextField = {
|
||||
let result = UITextField(frame: CGRect.zero)
|
||||
result.textColor = Theme.primaryColor
|
||||
result.font = UIFont.ows_dynamicTypeBodyClamped
|
||||
result.textAlignment = .center
|
||||
let placeholder = NSMutableAttributedString(string: NSLocalizedString("Enter Your Seed", comment: ""))
|
||||
placeholder.addAttribute(.foregroundColor, value: Theme.placeholderColor, range: NSRange(location: 0, length: placeholder.length))
|
||||
result.attributedPlaceholder = placeholder
|
||||
result.tintColor = UIColor.lokiGreen()
|
||||
result.accessibilityIdentifier = "onboarding.keyPairStep.mnemonicTextField"
|
||||
result.keyboardAppearance = .dark
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var registerButton1: OWSFlatButton = {
|
||||
let result = createLinkButton(title: NSLocalizedString("Register a New Account", comment: ""), selector: #selector(handleSwitchModeButton1Tapped))
|
||||
result.accessibilityIdentifier = "onboarding.keyPairStep.registerButton1"
|
||||
result.setBackgroundColors(upColor: .clear, downColor: .clear)
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var linkButton2: OWSFlatButton = {
|
||||
let result = createLinkButton(title: NSLocalizedString("Link Device", comment: ""), selector: #selector(handleSwitchModeButton2Tapped))
|
||||
result.accessibilityIdentifier = "onboarding.keyPairStep.linkButton2"
|
||||
result.setBackgroundColors(upColor: .clear, downColor: .clear)
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var linkStackView: UIStackView = {
|
||||
let result = UIStackView(arrangedSubviews: [ explanationLabel3, UIView.spacer(withHeight: 32), errorLabel2, errorLabel2Spacer, masterHexEncodedPublicKeyTextField, UIView.spacer(withHeight: 24), registerButton2, restoreButton2 ])
|
||||
result.accessibilityIdentifier = "onboarding.keyPairStep.linkStackView"
|
||||
result.axis = .vertical
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var explanationLabel3: UILabel = {
|
||||
let result = createExplanationLabel(text: NSLocalizedString("Link to an existing device by going into its in-app settings and clicking \"Link Device\".", comment: ""))
|
||||
result.accessibilityIdentifier = "onboarding.keyPairStep.explanationLabel3"
|
||||
result.textColor = Theme.primaryColor
|
||||
var fontTraits = result.font.fontDescriptor.symbolicTraits
|
||||
fontTraits.insert(.traitBold)
|
||||
result.font = UIFont(descriptor: result.font.fontDescriptor.withSymbolicTraits(fontTraits)!, size: result.font.pointSize)
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var errorLabel2: UILabel = {
|
||||
let result = createExplanationLabel(text: "")
|
||||
result.accessibilityIdentifier = "onboarding.keyPairStep.errorLabel2"
|
||||
result.textColor = UIColor.red
|
||||
var fontTraits = result.font.fontDescriptor.symbolicTraits
|
||||
fontTraits.insert(.traitBold)
|
||||
result.font = UIFont(descriptor: result.font.fontDescriptor.withSymbolicTraits(fontTraits)!, size: 12)
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var errorLabel2Spacer: UIView = {
|
||||
let result = UIView.spacer(withHeight: 32)
|
||||
result.isHidden = true
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var masterHexEncodedPublicKeyTextField: UITextField = {
|
||||
let result = UITextField(frame: CGRect.zero)
|
||||
result.textColor = Theme.primaryColor
|
||||
result.font = UIFont.ows_dynamicTypeBodyClamped
|
||||
result.textAlignment = .center
|
||||
let placeholder = NSMutableAttributedString(string: NSLocalizedString("Enter the Other Device's Public Key", comment: ""))
|
||||
placeholder.addAttribute(.foregroundColor, value: Theme.placeholderColor, range: NSRange(location: 0, length: placeholder.length))
|
||||
result.attributedPlaceholder = placeholder
|
||||
result.tintColor = UIColor.lokiGreen()
|
||||
result.accessibilityIdentifier = "onboarding.keyPairStep.masterHexEncodedPublicKeyTextField"
|
||||
result.keyboardAppearance = .dark
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var registerButton2: OWSFlatButton = {
|
||||
let result = createLinkButton(title: NSLocalizedString("Register a New Account", comment: ""), selector: #selector(handleSwitchModeButton1Tapped))
|
||||
result.accessibilityIdentifier = "onboarding.keyPairStep.registerButton2"
|
||||
result.setBackgroundColors(upColor: .clear, downColor: .clear)
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var restoreButton2: OWSFlatButton = {
|
||||
let result = createLinkButton(title: NSLocalizedString("Restore Using Seed", comment: ""), selector: #selector(handleSwitchModeButton2Tapped))
|
||||
result.accessibilityIdentifier = "onboarding.keyPairStep.restoreButton2"
|
||||
result.setBackgroundColors(upColor: .clear, downColor: .clear)
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var mainButton: OWSFlatButton = {
|
||||
let result = createButton(title: "", selector: #selector(objc_proceed))
|
||||
result.accessibilityIdentifier = "onboarding.keyPairStep.mainButton"
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: Types
|
||||
enum Mode { case register, restore, link }
|
||||
|
||||
// MARK: Lifecycle
|
||||
override func viewDidLoad() {
|
||||
super.loadView()
|
||||
setUpViewHierarchy()
|
||||
handleModeChanged() // Perform initial update
|
||||
updateSeed()
|
||||
Analytics.shared.track("Seed Screen Viewed")
|
||||
}
|
||||
|
||||
private func setUpViewHierarchy() {
|
||||
// Prepare
|
||||
view.backgroundColor = Theme.backgroundColor
|
||||
view.layoutMargins = .zero
|
||||
// Set up view hierarchy
|
||||
let titleLabel = createTitleLabel(text: NSLocalizedString("Create Your Loki Messenger Account", comment: ""))
|
||||
titleLabel.accessibilityIdentifier = "onboarding.keyPairStep.titleLabel"
|
||||
titleLabel.setContentHuggingPriority(.required, for: NSLayoutConstraint.Axis.vertical)
|
||||
let mainView = UIView(frame: CGRect.zero)
|
||||
mainView.addSubview(restoreStackView)
|
||||
mainView.addSubview(registerStackView)
|
||||
mainView.addSubview(linkStackView)
|
||||
let mainStackView = UIStackView(arrangedSubviews: [ titleLabel, mainView, mainButton ])
|
||||
mainStackView.axis = .vertical
|
||||
mainStackView.layoutMargins = UIEdgeInsets(top: 32, left: 32, bottom: 32, right: 32)
|
||||
mainStackView.isLayoutMarginsRelativeArrangement = true
|
||||
view.addSubview(mainStackView)
|
||||
// Set up constraints
|
||||
mainStackView.autoPinWidthToSuperview()
|
||||
mainStackView.autoPin(toTopLayoutGuideOf: self, withInset: 0)
|
||||
autoPinView(toBottomOfViewControllerOrKeyboard: mainStackView, avoidNotch: true)
|
||||
registerStackView.autoPinWidthToSuperview()
|
||||
registerStackView.autoVCenterInSuperview()
|
||||
restoreStackView.autoPinWidthToSuperview()
|
||||
restoreStackView.autoVCenterInSuperview()
|
||||
linkStackView.autoPinWidthToSuperview()
|
||||
linkStackView.autoVCenterInSuperview()
|
||||
}
|
||||
|
||||
// MARK: General
|
||||
@objc private func enableCopyButton() {
|
||||
copyButton.isUserInteractionEnabled = true
|
||||
UIView.transition(with: copyButton, duration: 0.25, options: .transitionCrossDissolve, animations: {
|
||||
self.copyButton.setTitle(NSLocalizedString("Copy", comment: ""))
|
||||
}, completion: nil)
|
||||
}
|
||||
|
||||
// MARK: Updating
|
||||
private func handleModeChanged() {
|
||||
let (activeStackView, otherStackViews) = { () -> (UIStackView, [UIStackView]) in
|
||||
switch mode {
|
||||
case .register: return (registerStackView, [ restoreStackView, linkStackView ])
|
||||
case .restore: return (restoreStackView, [ registerStackView, linkStackView ])
|
||||
case .link: return (linkStackView, [ registerStackView, restoreStackView ])
|
||||
}
|
||||
}()
|
||||
UIView.animate(withDuration: 0.25) {
|
||||
activeStackView.alpha = 1
|
||||
otherStackViews.forEach { $0.alpha = 0 }
|
||||
}
|
||||
let mainButtonTitle: String = {
|
||||
switch mode {
|
||||
case .register: return NSLocalizedString("Register", comment: "")
|
||||
case .restore: return NSLocalizedString("Restore", comment: "")
|
||||
case .link: return NSLocalizedString("Link", comment: "")
|
||||
}
|
||||
}()
|
||||
UIView.transition(with: mainButton, duration: 0.25, options: .transitionCrossDissolve, animations: {
|
||||
self.mainButton.setTitle(mainButtonTitle)
|
||||
}, completion: nil)
|
||||
if mode != .restore { mnemonicTextField.resignFirstResponder() }
|
||||
if mode != .link { masterHexEncodedPublicKeyTextField.resignFirstResponder() }
|
||||
if mode == .link {
|
||||
showQRCodeScanner()
|
||||
}
|
||||
}
|
||||
|
||||
private func updateSeed() {
|
||||
seed = Randomness.generateRandomBytes(16)
|
||||
}
|
||||
|
||||
private func updateMnemonic() {
|
||||
let hexEncodedSeed = seed!.toHexString()
|
||||
mnemonic = Mnemonic.encode(hexEncodedString: hexEncodedSeed)
|
||||
}
|
||||
|
||||
private func handleMnemonicChanged() {
|
||||
mnemonicLabel.text = mnemonic
|
||||
}
|
||||
|
||||
// MARK: Interaction
|
||||
@objc private func copyMnemonic() {
|
||||
UIPasteboard.general.string = mnemonic
|
||||
copyButton.isUserInteractionEnabled = false
|
||||
UIView.transition(with: copyButton, duration: 0.25, options: .transitionCrossDissolve, animations: {
|
||||
self.copyButton.setTitle(NSLocalizedString("Copied ✓", comment: ""))
|
||||
}, completion: nil)
|
||||
Timer.scheduledTimer(timeInterval: 4, target: self, selector: #selector(enableCopyButton), userInfo: nil, repeats: false)
|
||||
}
|
||||
|
||||
@objc private func handleSwitchModeButton1Tapped() {
|
||||
switch mode {
|
||||
case .register: mode = .restore
|
||||
case .restore: mode = .register
|
||||
case .link: mode = .register
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func handleSwitchModeButton2Tapped() {
|
||||
switch mode {
|
||||
case .register: mode = .link
|
||||
case .restore: mode = .link
|
||||
case .link: mode = .restore
|
||||
}
|
||||
}
|
||||
|
||||
private func showQRCodeScanner() {
|
||||
ows_ask(forCameraPermissions: { [weak self] hasCameraAccess in
|
||||
guard let self = self else { return }
|
||||
if hasCameraAccess {
|
||||
let message = NSLocalizedString("Link to an existing device by going into its in-app settings and clicking \"Link Device\".", comment: "")
|
||||
let scanQRCodeWrapperVC = ScanQRCodeWrapperVC(message: message)
|
||||
scanQRCodeWrapperVC.delegate = self
|
||||
scanQRCodeWrapperVC.isPresentedModally = true
|
||||
let navigationVC = OWSNavigationController(rootViewController: scanQRCodeWrapperVC)
|
||||
self.present(navigationVC, animated: true, completion: nil)
|
||||
} else {
|
||||
// Do nothing
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func controller(_ controller: OWSQRCodeScanningViewController, didDetectQRCodeWith string: String) {
|
||||
dismiss(animated: true, completion: nil)
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.proceed(with: string)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func objc_proceed() {
|
||||
proceed()
|
||||
}
|
||||
|
||||
private func proceed(with masterHexEncodedPublicKey: String? = nil) {
|
||||
var seed: Data
|
||||
let mode = self.mode
|
||||
switch mode {
|
||||
case .register: seed = self.seed
|
||||
case .restore:
|
||||
let mnemonic = mnemonicTextField.text!
|
||||
do {
|
||||
let hexEncodedSeed = try Mnemonic.decode(mnemonic: mnemonic)
|
||||
seed = Data(hex: hexEncodedSeed)
|
||||
} catch let error {
|
||||
let error = error as? Mnemonic.DecodingError ?? Mnemonic.DecodingError.generic
|
||||
errorLabel1Spacer.isHidden = false
|
||||
return errorLabel1.text = error.errorDescription
|
||||
}
|
||||
case .link:
|
||||
seed = self.seed
|
||||
let isUsingQRCode = masterHexEncodedPublicKey != nil
|
||||
let masterHexEncodedPublicKey = masterHexEncodedPublicKey ?? masterHexEncodedPublicKeyTextField.text!.trimmingCharacters(in: CharacterSet.whitespaces)
|
||||
if !isUsingQRCode && masterHexEncodedPublicKey.isEmpty {
|
||||
return showQRCodeScanner()
|
||||
}
|
||||
if !ECKeyPair.isValidHexEncodedPublicKey(candidate: masterHexEncodedPublicKey) {
|
||||
if isUsingQRCode {
|
||||
let alert = UIAlertController(title: NSLocalizedString("Invalid QR Code", comment: ""), message: NSLocalizedString("Please make sure the QR code you scanned is correct and try again.", comment: ""), preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), accessibilityIdentifier: nil, style: .default, handler: nil))
|
||||
return present(alert, animated: true, completion: nil)
|
||||
} else {
|
||||
errorLabel2Spacer.isHidden = false
|
||||
return errorLabel2.text = NSLocalizedString("Invalid public key", comment: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
// Use KVC to access dbConnection even though it's private
|
||||
let identityManager = OWSIdentityManager.shared()
|
||||
let databaseConnection = identityManager.value(forKey: "dbConnection") as! YapDatabaseConnection
|
||||
databaseConnection.setObject(seed.toHexString(), forKey: "LKLokiSeed", inCollection: OWSPrimaryStorageIdentityKeyStoreCollection)
|
||||
if seed.count == 16 { seed = seed + seed }
|
||||
identityManager.generateNewIdentityKeyPair(fromSeed: seed) // This also stores it
|
||||
let keyPair = identityManager.identityKeyPair()!
|
||||
let hexEncodedPublicKey = keyPair.hexEncodedPublicKey
|
||||
let accountManager = TSAccountManager.sharedInstance()
|
||||
accountManager.phoneNumberAwaitingVerification = hexEncodedPublicKey
|
||||
switch mode {
|
||||
case .register: Analytics.shared.track("Seed Created")
|
||||
case .restore: Analytics.shared.track("Seed Restored")
|
||||
case .link: Analytics.shared.track("Device Linking Attempted")
|
||||
}
|
||||
if mode == .link {
|
||||
let isUsingQRCode = masterHexEncodedPublicKey != nil
|
||||
let masterHexEncodedPublicKey = masterHexEncodedPublicKey ?? masterHexEncodedPublicKeyTextField.text!.trimmingCharacters(in: CharacterSet.whitespaces)
|
||||
TSAccountManager.sharedInstance().didRegister()
|
||||
setUserInteractionEnabled(false)
|
||||
let _ = LokiStorageAPI.getDeviceLinks(associatedWith: masterHexEncodedPublicKey).done(on: DispatchQueue.main) { [weak self] deviceLinks in
|
||||
guard let self = self else { return }
|
||||
defer { self.setUserInteractionEnabled(true) }
|
||||
guard deviceLinks.count < 2 else {
|
||||
let alert = UIAlertController(title: "Multi Device Limit Reached", message: "It's currently not allowed to link more than one device.", preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: "OK", accessibilityIdentifier: nil, style: .default, handler: nil))
|
||||
return self.present(alert, animated: true, completion: nil)
|
||||
}
|
||||
let appDelegate = UIApplication.shared.delegate as! AppDelegate
|
||||
appDelegate.startLongPollerIfNeeded()
|
||||
let deviceLinkingModal = DeviceLinkingModal(mode: .slave, delegate: self)
|
||||
deviceLinkingModal.modalPresentationStyle = .overFullScreen
|
||||
self.present(deviceLinkingModal, animated: true, completion: nil)
|
||||
let linkingRequestMessage = DeviceLinkingUtilities.getLinkingRequestMessage(for: masterHexEncodedPublicKey)
|
||||
ThreadUtil.enqueue(linkingRequestMessage)
|
||||
}.catch(on: DispatchQueue.main) { [weak self] _ in
|
||||
DispatchQueue.main.async {
|
||||
// FIXME: For some reason resetForRegistration() complains about not being on the main queue
|
||||
// without this (even though the catch closure should be executed on the main queue)
|
||||
TSAccountManager.sharedInstance().resetForReregistration()
|
||||
}
|
||||
guard let self = self else { return }
|
||||
let alert = UIAlertController(title: NSLocalizedString("Couldn't Link Device", comment: ""), message: NSLocalizedString("Please check your internet connection and try again", comment: ""), preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: "OK", accessibilityIdentifier: nil, style: .default, handler: nil))
|
||||
self.present(alert, animated: true, completion: nil)
|
||||
self.setUserInteractionEnabled(true)
|
||||
}
|
||||
} else {
|
||||
onboardingController.pushDisplayNameVC(from: self)
|
||||
}
|
||||
}
|
||||
|
||||
func handleDeviceLinkAuthorized(_ deviceLink: DeviceLink) {
|
||||
let userDefaults = UserDefaults.standard
|
||||
userDefaults.set(true, forKey: "didUpdateForMainnet")
|
||||
userDefaults.set(deviceLink.master.hexEncodedPublicKey, forKey: "masterDeviceHexEncodedPublicKey")
|
||||
onboardingController.verificationDidComplete(fromView: self)
|
||||
Analytics.shared.track("Device Linked Successfully")
|
||||
}
|
||||
|
||||
func handleDeviceLinkingModalDismissed() {
|
||||
let appDelegate = UIApplication.shared.delegate as! AppDelegate
|
||||
appDelegate.stopLongPollerIfNeeded()
|
||||
TSAccountManager.sharedInstance().resetForReregistration()
|
||||
}
|
||||
|
||||
// MARK: Convenience
|
||||
private func setUserInteractionEnabled(_ isEnabled: Bool) {
|
||||
[ copyButton, restoreButton1, linkButton1, registerButton1, linkButton2, registerButton2, restoreButton2, mainButton ].forEach {
|
||||
$0.isUserInteractionEnabled = isEnabled
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,54 +0,0 @@
|
|||
|
||||
@objc(LKNukeDataModal)
|
||||
final class NukeDataModal : Modal {
|
||||
|
||||
// MARK: Lifecycle
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
Analytics.shared.track("Nuke Data Modal Shown")
|
||||
}
|
||||
|
||||
override func populateContentView() {
|
||||
// Label
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.textColor = Theme.primaryColor
|
||||
titleLabel.font = UIFont.ows_dynamicTypeHeadlineClamped
|
||||
titleLabel.text = NSLocalizedString("Clear All Data", comment: "")
|
||||
titleLabel.numberOfLines = 0
|
||||
titleLabel.lineBreakMode = .byWordWrapping
|
||||
titleLabel.textAlignment = .center
|
||||
// Explanation label
|
||||
let explanationLabel = UILabel()
|
||||
explanationLabel.font = UIFont.ows_dynamicTypeCaption1Clamped
|
||||
explanationLabel.text = NSLocalizedString("Are you sure you want to clear all your data? This will delete your entire account, including all conversations and your personal key pair.", comment: "")
|
||||
explanationLabel.numberOfLines = 0
|
||||
explanationLabel.textAlignment = .center
|
||||
explanationLabel.lineBreakMode = .byWordWrapping
|
||||
explanationLabel.textColor = UIColor.ows_white
|
||||
// Button stack view
|
||||
let nukeButton = OWSFlatButton.button(title: NSLocalizedString("OK", comment: ""), font: .ows_dynamicTypeBodyClamped, titleColor: .white, backgroundColor: .clear, target: self, selector: #selector(nuke))
|
||||
nukeButton.setBackgroundColors(upColor: .clear, downColor: .clear)
|
||||
let buttonStackView = UIStackView(arrangedSubviews: [ nukeButton, cancelButton ])
|
||||
buttonStackView.axis = .horizontal
|
||||
buttonStackView.distribution = .fillEqually
|
||||
let buttonHeight = cancelButton.button.titleLabel!.font.pointSize * 48 / 17
|
||||
nukeButton.set(.height, to: buttonHeight)
|
||||
cancelButton.set(.height, to: buttonHeight)
|
||||
// Stack view
|
||||
let stackView = UIStackView(arrangedSubviews: [ UIView.spacer(withHeight: 2), titleLabel, explanationLabel, buttonStackView ])
|
||||
stackView.axis = .vertical
|
||||
stackView.spacing = 16
|
||||
contentView.addSubview(stackView)
|
||||
stackView.pin(.leading, to: .leading, of: contentView, withInset: 16)
|
||||
stackView.pin(.top, to: .top, of: contentView, withInset: 16)
|
||||
contentView.pin(.trailing, to: .trailing, of: stackView, withInset: 16)
|
||||
contentView.pin(.bottom, to: .bottom, of: stackView, withInset: 16)
|
||||
}
|
||||
|
||||
// MARK: Interaction
|
||||
@objc private func nuke() {
|
||||
Analytics.shared.track("Data Nuked")
|
||||
UserDefaults.removeAll() // Not done in the nuke data implementation as unlinking requires this to happen later
|
||||
NotificationCenter.default.post(name: .dataNukeRequested, object: nil)
|
||||
}
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
|
||||
@objc(LKQRCodeModal)
|
||||
final class QRCodeModal : Modal {
|
||||
|
||||
override func populateContentView() {
|
||||
// Label
|
||||
let label = UILabel()
|
||||
label.font = UIFont.ows_dynamicTypeSubheadlineClamped
|
||||
label.text = NSLocalizedString("This is your QR code. Other people can scan it to start a secure conversation with you.", comment: "")
|
||||
label.numberOfLines = 0
|
||||
label.textAlignment = .center
|
||||
label.lineBreakMode = .byWordWrapping
|
||||
label.textColor = UIColor.ows_white
|
||||
// Image view
|
||||
let imageView = UIImageView()
|
||||
let hexEncodedPublicKey: String
|
||||
if let masterDeviceHexEncodedPublicKey = UserDefaults.standard.string(forKey: "masterDeviceHexEncodedPublicKey") {
|
||||
hexEncodedPublicKey = masterDeviceHexEncodedPublicKey
|
||||
} else {
|
||||
hexEncodedPublicKey = OWSIdentityManager.shared().identityKeyPair()!.hexEncodedPublicKey
|
||||
}
|
||||
let data = hexEncodedPublicKey.data(using: .utf8)
|
||||
let filter = CIFilter(name: "CIQRCodeGenerator")!
|
||||
filter.setValue(data, forKey: "inputMessage")
|
||||
let qrCodeAsCIImage = filter.outputImage!
|
||||
let scaledQRCodeAsCIImage = qrCodeAsCIImage.transformed(by: CGAffineTransform(scaleX: 4.8, y: 4.8))
|
||||
let qrCode = UIImage(ciImage: scaledQRCodeAsCIImage)
|
||||
imageView.image = qrCode
|
||||
// Cancel button
|
||||
let buttonHeight = cancelButton.button.titleLabel!.font.pointSize * 48 / 17
|
||||
cancelButton.set(.height, to: buttonHeight)
|
||||
// Stack view
|
||||
let stackView = UIStackView(arrangedSubviews: [ UIView.spacer(withHeight: 2), label, UIView.spacer(withHeight: 2), imageView, cancelButton ])
|
||||
stackView.axis = .vertical
|
||||
stackView.spacing = 16
|
||||
stackView.alignment = .center
|
||||
contentView.addSubview(stackView)
|
||||
stackView.pin(.leading, to: .leading, of: contentView, withInset: 16)
|
||||
stackView.pin(.top, to: .top, of: contentView, withInset: 16)
|
||||
contentView.pin(.trailing, to: .trailing, of: stackView, withInset: 16)
|
||||
contentView.pin(.bottom, to: .bottom, of: stackView, withInset: 16)
|
||||
}
|
||||
}
|
|
@ -1,73 +0,0 @@
|
|||
|
||||
@objc(LKSeedModal)
|
||||
final class SeedModal : Modal {
|
||||
|
||||
private let mnemonic: String = {
|
||||
let identityManager = OWSIdentityManager.shared()
|
||||
let databaseConnection = identityManager.value(forKey: "dbConnection") as! YapDatabaseConnection
|
||||
var hexEncodedSeed: String! = databaseConnection.object(forKey: "LKLokiSeed", inCollection: OWSPrimaryStorageIdentityKeyStoreCollection) as! String?
|
||||
if hexEncodedSeed == nil {
|
||||
hexEncodedSeed = identityManager.identityKeyPair()!.hexEncodedPrivateKey // Legacy account
|
||||
}
|
||||
return Mnemonic.encode(hexEncodedString: hexEncodedSeed)
|
||||
}()
|
||||
|
||||
// MARK: Lifecycle
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
Analytics.shared.track("Seed Modal Shown")
|
||||
}
|
||||
|
||||
override func populateContentView() {
|
||||
// Label
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.textColor = Theme.primaryColor
|
||||
titleLabel.font = UIFont.ows_dynamicTypeHeadlineClamped
|
||||
titleLabel.text = NSLocalizedString("Your Seed", comment: "")
|
||||
titleLabel.numberOfLines = 0
|
||||
titleLabel.lineBreakMode = .byWordWrapping
|
||||
titleLabel.textAlignment = .center
|
||||
// Subtitle label
|
||||
let subtitleLabel = UILabel()
|
||||
subtitleLabel.textColor = Theme.primaryColor
|
||||
subtitleLabel.font = UIFont.ows_dynamicTypeCaption1Clamped
|
||||
subtitleLabel.text = NSLocalizedString("This is your personal secret. It can be used to restore your account if you lose access, or to migrate your account to a new device.", comment: "")
|
||||
subtitleLabel.numberOfLines = 0
|
||||
subtitleLabel.lineBreakMode = .byWordWrapping
|
||||
subtitleLabel.textAlignment = .center
|
||||
// Mnemonic label
|
||||
let mnemonicLabel = UILabel()
|
||||
let font = UIFont.ows_dynamicTypeCaption1Clamped
|
||||
mnemonicLabel.font = UIFont(descriptor: font.fontDescriptor.withSymbolicTraits(.traitItalic)!, size: font.pointSize)
|
||||
mnemonicLabel.text = mnemonic
|
||||
mnemonicLabel.numberOfLines = 0
|
||||
mnemonicLabel.textAlignment = .center
|
||||
mnemonicLabel.lineBreakMode = .byWordWrapping
|
||||
mnemonicLabel.textColor = UIColor.ows_white
|
||||
mnemonicLabel.alpha = 0.8
|
||||
// Button stack view
|
||||
let copyButton = OWSFlatButton.button(title: NSLocalizedString("Copy", comment: ""), font: .ows_dynamicTypeBodyClamped, titleColor: .white, backgroundColor: .clear, target: self, selector: #selector(copySeed))
|
||||
copyButton.setBackgroundColors(upColor: .clear, downColor: .clear)
|
||||
let buttonStackView = UIStackView(arrangedSubviews: [ copyButton, cancelButton ])
|
||||
buttonStackView.axis = .horizontal
|
||||
buttonStackView.distribution = .fillEqually
|
||||
let buttonHeight = cancelButton.button.titleLabel!.font.pointSize * 48 / 17
|
||||
copyButton.set(.height, to: buttonHeight)
|
||||
cancelButton.set(.height, to: buttonHeight)
|
||||
// Stack view
|
||||
let stackView = UIStackView(arrangedSubviews: [ UIView.spacer(withHeight: 2), titleLabel, subtitleLabel, mnemonicLabel, buttonStackView ])
|
||||
stackView.axis = .vertical
|
||||
stackView.spacing = 16
|
||||
contentView.addSubview(stackView)
|
||||
stackView.pin(.leading, to: .leading, of: contentView, withInset: 16)
|
||||
stackView.pin(.top, to: .top, of: contentView, withInset: 16)
|
||||
contentView.pin(.trailing, to: .trailing, of: stackView, withInset: 16)
|
||||
contentView.pin(.bottom, to: .bottom, of: stackView, withInset: 16)
|
||||
}
|
||||
|
||||
// MARK: Interaction
|
||||
@objc private func copySeed() {
|
||||
UIPasteboard.general.string = mnemonic
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
|
||||
@objc extension UIColor {
|
||||
|
||||
@objc convenience init(hex value: UInt) {
|
||||
let red = CGFloat((value >> 16) & 0xff) / 255
|
||||
let green = CGFloat((value >> 8) & 0xff) / 255
|
||||
let blue = CGFloat((value >> 0) & 0xff) / 255
|
||||
self.init(red: red, green: green, blue: blue, alpha: 1)
|
||||
}
|
||||
}
|
||||
|
||||
@objc(LKColors)
|
||||
final class Colors : NSObject {
|
||||
|
||||
@objc static let accent = UIColor(hex: 0x00F782)
|
||||
@objc static let text = UIColor(hex: 0xFFFFFF)
|
||||
@objc static let destructive = UIColor(hex: 0xFF453A)
|
||||
@objc static let unimportant = UIColor(hex: 0xD8D8D8)
|
||||
@objc static let border = UIColor(hex: 0x979797)
|
||||
@objc static let cellBackground = UIColor(hex: 0x1B1B1B)
|
||||
@objc static let cellSelected = UIColor(hex: 0x0C0C0C)
|
||||
@objc static let navigationBarBackground = UIColor(hex: 0x161616)
|
||||
@objc static let searchBarPlaceholder = UIColor(hex: 0x8E8E93) // Also used for the icons
|
||||
@objc static let searchBarBackground = UIColor(red: 142 / 255, green: 142 / 255, blue: 147 / 255, alpha: 0.12)
|
||||
@objc static let newConversationButtonShadow = UIColor(hex: 0x077C44)
|
||||
@objc static let separator = UIColor(hex: 0x36383C)
|
||||
@objc static let unimportantButtonBackground = UIColor(hex: 0x323232)
|
||||
@objc static let buttonBackground = UIColor(hex: 0x1B1B1B)
|
||||
@objc static let settingButtonSelected = UIColor(hex: 0x0C0C0C)
|
||||
@objc static let modalBackground = UIColor(hex: 0x101011)
|
||||
@objc static let modalBorder = UIColor(hex: 0x212121)
|
||||
@objc static let fakeChatBubbleBackground = UIColor(hex: 0x3F4146)
|
||||
@objc static let fakeChatBubbleText = UIColor(hex: 0x000000)
|
||||
@objc static let composeViewBackground = UIColor(hex: 0x1B1B1B)
|
||||
@objc static let composeViewTextFieldBackground = UIColor(hex: 0x141414)
|
||||
@objc static let receivedMessageBackgroundColor = UIColor(hex: 0x222325)
|
||||
@objc static let sentMessageBackgroundColor = UIColor(hex: 0x3F4146)
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
|
||||
@objc(LKFonts)
|
||||
final class Fonts : NSObject {
|
||||
|
||||
@objc static func spaceMono(ofSize size: CGFloat) -> UIFont {
|
||||
return UIFont(name: "SpaceMono-Regular", size: size)!
|
||||
}
|
||||
|
||||
@objc static func boldSpaceMono(ofSize size: CGFloat) -> UIFont {
|
||||
return UIFont(name: "SpaceMono-Bold", size: size)!
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
|
||||
@objc(LKGradient)
|
||||
final class Gradient : NSObject {
|
||||
let start: UIColor
|
||||
let end: UIColor
|
||||
|
||||
private override init() { preconditionFailure("Use init(start:end:) instead.") }
|
||||
|
||||
@objc init(start: UIColor, end: UIColor) {
|
||||
self.start = start
|
||||
self.end = end
|
||||
super.init()
|
||||
}
|
||||
}
|
||||
|
||||
@objc extension UIView {
|
||||
|
||||
@objc func setGradient(_ gradient: Gradient) {
|
||||
let layer = CAGradientLayer()
|
||||
layer.frame = UIScreen.main.bounds
|
||||
layer.colors = [ gradient.start.cgColor, gradient.end.cgColor ]
|
||||
self.layer.insertSublayer(layer, at: 0)
|
||||
}
|
||||
}
|
||||
|
||||
@objc(LKGradients)
|
||||
final class Gradients : NSObject {
|
||||
|
||||
@objc static let defaultLokiBackground = Gradient(start: UIColor(hex: 0x171717), end: UIColor(hex:0x121212))
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
|
||||
@objc(LKValues)
|
||||
final class Values : NSObject {
|
||||
|
||||
// MARK: - Alpha Values
|
||||
@objc static let unimportantElementOpacity = CGFloat(0.6)
|
||||
@objc static let conversationCellTimestampOpacity = CGFloat(0.4)
|
||||
@objc static let textFieldBorderOpacity = CGFloat(0.4)
|
||||
@objc static let modalBackgroundOpacity = CGFloat(0.75)
|
||||
@objc static let composeViewTextFieldBorderOpacity = CGFloat(0.12)
|
||||
@objc static let composeViewTextFieldPlaceholderOpacity = CGFloat(0.4)
|
||||
|
||||
// MARK: - Font Sizes
|
||||
@objc static let verySmallFontSize = CGFloat(10)
|
||||
@objc static let smallFontSize = CGFloat(13)
|
||||
@objc static let mediumFontSize = CGFloat(15)
|
||||
@objc static let largeFontSize = CGFloat(20)
|
||||
@objc static let veryLargeFontSize = CGFloat(25)
|
||||
@objc static let massiveFontSize = CGFloat(50)
|
||||
|
||||
// MARK: - Element Sizes
|
||||
@objc static let smallButtonHeight = CGFloat(27)
|
||||
@objc static let mediumButtonHeight = CGFloat(34)
|
||||
@objc static let largeButtonHeight = CGFloat(45)
|
||||
@objc static let accentLineThickness = CGFloat(4)
|
||||
@objc static let verySmallProfilePictureSize = CGFloat(26)
|
||||
@objc static let smallProfilePictureSize = CGFloat(35)
|
||||
@objc static let mediumProfilePictureSize = CGFloat(45)
|
||||
@objc static let largeProfilePictureSize = CGFloat(75)
|
||||
@objc static let borderThickness = CGFloat(1)
|
||||
@objc static let conversationCellStatusIndicatorSize = CGFloat(14)
|
||||
@objc static let searchBarHeight = CGFloat(36)
|
||||
@objc static let newConversationButtonSize = CGFloat(45)
|
||||
@objc static let textFieldHeight = CGFloat(80)
|
||||
@objc static let textFieldCornerRadius = CGFloat(8)
|
||||
@objc static let separatorLabelHeight = CGFloat(24)
|
||||
@objc static var separatorThickness: CGFloat { return 1 / UIScreen.main.scale }
|
||||
@objc static let tabBarHeight = CGFloat(48)
|
||||
@objc static let settingButtonHeight = CGFloat(75)
|
||||
@objc static let modalCornerRadius = CGFloat(10)
|
||||
@objc static let modalButtonCornerRadius = CGFloat(5)
|
||||
@objc static let fakeChatBubbleWidth = CGFloat(224)
|
||||
@objc static let fakeChatBubbleCornerRadius = CGFloat(10)
|
||||
@objc static let fakeChatViewHeight = CGFloat(234)
|
||||
@objc static var composeViewTextFieldBorderThickness: CGFloat { return 1 / UIScreen.main.scale }
|
||||
@objc static let messageBubbleCornerRadius: CGFloat = 10
|
||||
@objc static let progressBarThickness: CGFloat = 2
|
||||
|
||||
// MARK: - Distances
|
||||
@objc static let verySmallSpacing = CGFloat(4)
|
||||
@objc static let smallSpacing = CGFloat(8)
|
||||
@objc static let mediumSpacing = CGFloat(16)
|
||||
@objc static let largeSpacing = CGFloat(24)
|
||||
@objc static let veryLargeSpacing = CGFloat(35)
|
||||
@objc static let massiveSpacing = CGFloat(64)
|
||||
@objc static let newConversationButtonBottomOffset = CGFloat(52)
|
||||
@objc static let onboardingButtonBottomOffset = CGFloat(72)
|
||||
|
||||
// MARK: - Animation Values
|
||||
@objc static let fakeChatStartDelay: TimeInterval = 2
|
||||
@objc static let fakeChatAnimationDuration: TimeInterval = 0.4
|
||||
@objc static let fakeChatDelay: TimeInterval = 2.5
|
||||
@objc static let fakeChatMessagePopAnimationStartScale: CGFloat = 0.6
|
||||
}
|
|
@ -9,33 +9,36 @@ public final class MentionUtilities : NSObject {
|
|||
}
|
||||
|
||||
@objc public static func highlightMentions(in string: String, isOutgoingMessage: Bool, threadID: String, attributes: [NSAttributedString.Key:Any]) -> NSAttributedString {
|
||||
let userHexEncodedPublicKey = OWSIdentityManager.shared().identityKeyPair()!.hexEncodedPublicKey
|
||||
var publicChat: LokiPublicChat?
|
||||
var userLinkedDeviceHexEncodedPublicKeys: Set<String>!
|
||||
OWSPrimaryStorage.shared().dbReadConnection.read { transaction in
|
||||
publicChat = LokiDatabaseUtilities.getPublicChat(for: threadID, in: transaction)
|
||||
userLinkedDeviceHexEncodedPublicKeys = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: userHexEncodedPublicKey, in: transaction)
|
||||
}
|
||||
var string = string
|
||||
let regex = try! NSRegularExpression(pattern: "@[0-9a-fA-F]*", options: [])
|
||||
let knownUserHexEncodedPublicKeys = LokiAPI.userHexEncodedPublicKeyCache[threadID] ?? [] // Should always be populated at this point
|
||||
var mentions: [NSRange] = []
|
||||
let knownHexEncodedPublicKeys = LokiAPI.userHexEncodedPublicKeyCache[threadID] ?? [] // Should always be populated at this point
|
||||
var mentions: [(range: NSRange, hexEncodedPublicKey: String)] = []
|
||||
var outerMatch = regex.firstMatch(in: string, options: .withoutAnchoringBounds, range: NSRange(location: 0, length: string.count))
|
||||
while let match = outerMatch {
|
||||
let hexEncodedPublicKey = String((string as NSString).substring(with: match.range).dropFirst()) // Drop the @
|
||||
let matchEnd: Int
|
||||
if knownUserHexEncodedPublicKeys.contains(hexEncodedPublicKey) {
|
||||
var userDisplayName: String?
|
||||
if hexEncodedPublicKey == OWSIdentityManager.shared().identityKeyPair()!.hexEncodedPublicKey {
|
||||
userDisplayName = OWSProfileManager.shared().localProfileName()
|
||||
if knownHexEncodedPublicKeys.contains(hexEncodedPublicKey) {
|
||||
var displayName: String?
|
||||
if hexEncodedPublicKey == userHexEncodedPublicKey {
|
||||
displayName = OWSProfileManager.shared().localProfileName()
|
||||
} else {
|
||||
if let publicChat = publicChat {
|
||||
userDisplayName = DisplayNameUtilities.getPublicChatDisplayName(for: hexEncodedPublicKey, in: publicChat.channel, on: publicChat.server)
|
||||
displayName = DisplayNameUtilities.getPublicChatDisplayName(for: hexEncodedPublicKey, in: publicChat.channel, on: publicChat.server)
|
||||
} else {
|
||||
userDisplayName = DisplayNameUtilities.getPrivateChatDisplayName(for: hexEncodedPublicKey)
|
||||
displayName = DisplayNameUtilities.getPrivateChatDisplayName(for: hexEncodedPublicKey)
|
||||
}
|
||||
}
|
||||
if let userDisplayName = userDisplayName {
|
||||
string = (string as NSString).replacingCharacters(in: match.range, with: "@\(userDisplayName)")
|
||||
mentions.append(NSRange(location: match.range.location, length: userDisplayName.count + 1)) // + 1 to include the @
|
||||
matchEnd = match.range.location + userDisplayName.count
|
||||
if let displayName = displayName {
|
||||
string = (string as NSString).replacingCharacters(in: match.range, with: "@\(displayName)")
|
||||
mentions.append((range: NSRange(location: match.range.location, length: displayName.count + 1), hexEncodedPublicKey: hexEncodedPublicKey)) // + 1 to include the @
|
||||
matchEnd = match.range.location + displayName.count
|
||||
} else {
|
||||
matchEnd = match.range.location + match.range.length
|
||||
}
|
||||
|
@ -46,8 +49,9 @@ public final class MentionUtilities : NSObject {
|
|||
}
|
||||
let result = NSMutableAttributedString(string: string, attributes: attributes)
|
||||
mentions.forEach { mention in
|
||||
let color: UIColor = isOutgoingMessage ? .lokiDarkGray() : .lokiGreen()
|
||||
result.addAttribute(.backgroundColor, value: color, range: mention)
|
||||
guard userLinkedDeviceHexEncodedPublicKeys.contains(mention.hexEncodedPublicKey) else { return }
|
||||
result.addAttribute(.foregroundColor, value: Colors.accent, range: mention.range)
|
||||
result.addAttribute(.font, value: UIFont.boldSystemFont(ofSize: Values.mediumFontSize), range: mention.range)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
|
|
@ -1,58 +0,0 @@
|
|||
|
||||
@objc(LKModal)
|
||||
internal class Modal : UIViewController {
|
||||
private(set) var verticalCenteringConstraint: NSLayoutConstraint!
|
||||
|
||||
// MARK: Components
|
||||
lazy var contentView: UIView = {
|
||||
let result = UIView()
|
||||
result.backgroundColor = .lokiDarkGray()
|
||||
result.layer.cornerRadius = 4
|
||||
result.layer.masksToBounds = false
|
||||
result.layer.shadowColor = UIColor.black.cgColor
|
||||
result.layer.shadowRadius = 8
|
||||
result.layer.shadowOpacity = 0.64
|
||||
return result
|
||||
}()
|
||||
|
||||
lazy var cancelButton: OWSFlatButton = {
|
||||
let result = OWSFlatButton.button(title: NSLocalizedString("Cancel", comment: ""), font: .ows_dynamicTypeBodyClamped, titleColor: .white, backgroundColor: .clear, target: self, selector: #selector(cancel))
|
||||
result.setBackgroundColors(upColor: .clear, downColor: .clear)
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: Lifecycle
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
private func setUpViewHierarchy() {
|
||||
view.backgroundColor = .clear
|
||||
view.addSubview(contentView)
|
||||
contentView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 32).isActive = true
|
||||
view.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: 32).isActive = true
|
||||
verticalCenteringConstraint = contentView.center(.vertical, in: view)
|
||||
populateContentView()
|
||||
}
|
||||
|
||||
/// To be overridden by subclasses.
|
||||
func populateContentView() {
|
||||
preconditionFailure("populateContentView() is abstract and must be overridden.")
|
||||
}
|
||||
|
||||
// MARK: Interaction
|
||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
let touch = touches.first!
|
||||
let location = touch.location(in: view)
|
||||
if contentView.frame.contains(location) {
|
||||
super.touchesBegan(touches, with: event)
|
||||
} else {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func cancel() {
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
|
||||
enum QRCode {
|
||||
|
||||
static func generate(for string: String, hasBackground: Bool = false) -> UIImage {
|
||||
let data = string.data(using: .utf8)
|
||||
var qrCodeAsCIImage: CIImage
|
||||
let filter1 = CIFilter(name: "CIQRCodeGenerator")!
|
||||
filter1.setValue(data, forKey: "inputMessage")
|
||||
qrCodeAsCIImage = filter1.outputImage!
|
||||
if hasBackground {
|
||||
let filter2 = CIFilter(name: "CIFalseColor")!
|
||||
filter2.setValue(qrCodeAsCIImage, forKey: "inputImage")
|
||||
filter2.setValue(CIColor(color: UIColor(hex: 0xFFFFFF)), forKey: "inputColor0")
|
||||
filter2.setValue(CIColor(color: UIColor(hex: 0x1B1B1B)), forKey: "inputColor1")
|
||||
qrCodeAsCIImage = filter2.outputImage!
|
||||
} else {
|
||||
let filter2 = CIFilter(name: "CIColorInvert")!
|
||||
filter2.setValue(qrCodeAsCIImage, forKey: "inputImage")
|
||||
qrCodeAsCIImage = filter2.outputImage!
|
||||
let filter3 = CIFilter(name: "CIMaskToAlpha")!
|
||||
filter3.setValue(qrCodeAsCIImage, forKey: "inputImage")
|
||||
qrCodeAsCIImage = filter3.outputImage!
|
||||
}
|
||||
let scaledQRCodeAsCIImage = qrCodeAsCIImage.transformed(by: CGAffineTransform(scaleX: 6.4, y: 6.4))
|
||||
return UIImage(ciImage: scaledQRCodeAsCIImage)
|
||||
}
|
||||
}
|
|
@ -43,6 +43,13 @@ extension UIView {
|
|||
[ VerticalEdge.top, VerticalEdge.bottom ].forEach { pin($0, to: $0, of: view) }
|
||||
}
|
||||
|
||||
func pin(to view: UIView, withInset inset: CGFloat) {
|
||||
pin(.leading, to: .leading, of: view, withInset: inset)
|
||||
pin(.top, to: .top, of: view, withInset: inset)
|
||||
view.pin(.trailing, to: .trailing, of: self, withInset: inset)
|
||||
view.pin(.bottom, to: .bottom, of: self, withInset: inset)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func center(_ direction: Direction, in view: UIView) -> NSLayoutConstraint {
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
|
@ -10,9 +10,7 @@ final class DeviceLinkingModal : Modal, DeviceLinkingSessionDelegate {
|
|||
enum Mode : String { case master, slave }
|
||||
|
||||
// MARK: Components
|
||||
private lazy var topSpacer = UIView.spacer(withHeight: 8)
|
||||
|
||||
private lazy var spinner = NVActivityIndicatorView(frame: CGRect.zero, type: .circleStrokeSpin, color: .white, padding: nil)
|
||||
private lazy var spinner = NVActivityIndicatorView(frame: CGRect.zero, type: .circleStrokeSpin, color: Colors.text, padding: nil)
|
||||
|
||||
private lazy var qrCodeImageView: UIImageView = {
|
||||
let result = UIImageView()
|
||||
|
@ -22,8 +20,8 @@ final class DeviceLinkingModal : Modal, DeviceLinkingSessionDelegate {
|
|||
|
||||
private lazy var titleLabel: UILabel = {
|
||||
let result = UILabel()
|
||||
result.textColor = Theme.primaryColor
|
||||
result.font = UIFont.ows_dynamicTypeHeadlineClamped
|
||||
result.textColor = Colors.text
|
||||
result.font = .boldSystemFont(ofSize: Values.mediumFontSize)
|
||||
result.numberOfLines = 0
|
||||
result.lineBreakMode = .byWordWrapping
|
||||
result.textAlignment = .center
|
||||
|
@ -32,8 +30,8 @@ final class DeviceLinkingModal : Modal, DeviceLinkingSessionDelegate {
|
|||
|
||||
private lazy var subtitleLabel: UILabel = {
|
||||
let result = UILabel()
|
||||
result.textColor = Theme.primaryColor
|
||||
result.font = UIFont.ows_dynamicTypeCaption1Clamped
|
||||
result.textColor = Colors.text.withAlphaComponent(Values.unimportantElementOpacity)
|
||||
result.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
result.numberOfLines = 0
|
||||
result.lineBreakMode = .byWordWrapping
|
||||
result.textAlignment = .center
|
||||
|
@ -42,8 +40,8 @@ final class DeviceLinkingModal : Modal, DeviceLinkingSessionDelegate {
|
|||
|
||||
private lazy var mnemonicLabel: UILabel = {
|
||||
let result = UILabel()
|
||||
result.textColor = Theme.primaryColor
|
||||
result.font = UIFont.ows_dynamicTypeCaption1Clamped
|
||||
result.textColor = Colors.text
|
||||
result.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
result.numberOfLines = 0
|
||||
result.lineBreakMode = .byWordWrapping
|
||||
result.textAlignment = .center
|
||||
|
@ -51,19 +49,23 @@ final class DeviceLinkingModal : Modal, DeviceLinkingSessionDelegate {
|
|||
}()
|
||||
|
||||
private lazy var buttonStackView: UIStackView = {
|
||||
let result = UIStackView(arrangedSubviews: [ authorizeButton, cancelButton ])
|
||||
let result = UIStackView(arrangedSubviews: [ cancelButton, authorizeButton ])
|
||||
result.axis = .horizontal
|
||||
result.spacing = Values.mediumSpacing
|
||||
result.distribution = .fillEqually
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var authorizeButton: OWSFlatButton = {
|
||||
let result = OWSFlatButton.button(title: NSLocalizedString("Authorize", comment: ""), font: .ows_dynamicTypeBodyClamped, titleColor: .white, backgroundColor: .clear, target: self, selector: #selector(authorizeDeviceLink))
|
||||
result.setBackgroundColors(upColor: .clear, downColor: .clear)
|
||||
private lazy var authorizeButton: UIButton = {
|
||||
let result = UIButton()
|
||||
result.set(.height, to: Values.mediumButtonHeight)
|
||||
result.layer.cornerRadius = Values.modalButtonCornerRadius
|
||||
result.backgroundColor = Colors.accent
|
||||
result.titleLabel!.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
result.setTitleColor(Colors.text, for: UIControl.State.normal)
|
||||
result.setTitle(NSLocalizedString("Authorize", comment: ""), for: UIControl.State.normal)
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var bottomSpacer = UIView.spacer(withHeight: 8)
|
||||
|
||||
// MARK: Lifecycle
|
||||
init(mode: Mode, delegate: DeviceLinkingModalDelegate?) {
|
||||
|
@ -93,29 +95,19 @@ final class DeviceLinkingModal : Modal, DeviceLinkingSessionDelegate {
|
|||
}
|
||||
|
||||
override func populateContentView() {
|
||||
let stackView = UIStackView(arrangedSubviews: [ topSpacer, titleLabel, subtitleLabel, mnemonicLabel, buttonStackView, bottomSpacer ])
|
||||
let stackView = UIStackView(arrangedSubviews: [ titleLabel, subtitleLabel, mnemonicLabel, buttonStackView ])
|
||||
switch mode {
|
||||
case .master:
|
||||
stackView.insertArrangedSubview(qrCodeImageView, at: 1)
|
||||
stackView.insertArrangedSubview(UIView.spacer(withHeight: 2), at: 2)
|
||||
case .slave:
|
||||
stackView.insertArrangedSubview(spinner, at: 1)
|
||||
stackView.insertArrangedSubview(UIView.spacer(withHeight: 8), at: 2)
|
||||
case .master: stackView.insertArrangedSubview(qrCodeImageView, at: 0)
|
||||
case .slave: stackView.insertArrangedSubview(spinner, at: 0)
|
||||
}
|
||||
contentView.addSubview(stackView)
|
||||
stackView.spacing = 16
|
||||
stackView.spacing = Values.largeSpacing
|
||||
stackView.axis = .vertical
|
||||
switch mode {
|
||||
case .master:
|
||||
qrCodeImageView.set(.height, to: 128)
|
||||
let hexEncodedPublicKey = OWSIdentityManager.shared().identityKeyPair()!.hexEncodedPublicKey
|
||||
let data = hexEncodedPublicKey.data(using: .utf8)
|
||||
let filter = CIFilter(name: "CIQRCodeGenerator")!
|
||||
filter.setValue(data, forKey: "inputMessage")
|
||||
let qrCodeAsCIImage = filter.outputImage!
|
||||
let scaledQRCodeAsCIImage = qrCodeAsCIImage.transformed(by: CGAffineTransform(scaleX: 4.8, y: 4.8))
|
||||
let qrCode = UIImage(ciImage: scaledQRCodeAsCIImage)
|
||||
qrCodeImageView.image = qrCode
|
||||
qrCodeImageView.image = QRCode.generate(for: hexEncodedPublicKey)
|
||||
case .slave:
|
||||
spinner.set(.height, to: 64)
|
||||
spinner.startAnimating()
|
||||
|
@ -128,7 +120,7 @@ final class DeviceLinkingModal : Modal, DeviceLinkingSessionDelegate {
|
|||
}()
|
||||
subtitleLabel.text = {
|
||||
switch mode {
|
||||
case .master: return NSLocalizedString("Create a new account on your other device and click \"Link Device\" when you're at the \"Create Your Loki Messenger Account\" step to start the linking process", comment: "")
|
||||
case .master: return NSLocalizedString("Create a new account on your other device and click \"Link to an existing account\" to start the linking process", comment: "")
|
||||
case .slave: return NSLocalizedString("Please check that the words below match the ones shown on your other device", comment: "")
|
||||
}
|
||||
}()
|
||||
|
@ -137,21 +129,17 @@ final class DeviceLinkingModal : Modal, DeviceLinkingSessionDelegate {
|
|||
let hexEncodedPublicKey = OWSIdentityManager.shared().identityKeyPair()!.hexEncodedPublicKey.removing05PrefixIfNeeded()
|
||||
mnemonicLabel.text = Mnemonic.hash(hexEncodedString: hexEncodedPublicKey)
|
||||
}
|
||||
let buttonHeight = cancelButton.button.titleLabel!.font.pointSize * 48 / 17
|
||||
authorizeButton.set(.height, to: buttonHeight)
|
||||
cancelButton.set(.height, to: buttonHeight)
|
||||
authorizeButton.addTarget(self, action: #selector(authorizeDeviceLink), for: UIControl.Event.touchUpInside)
|
||||
authorizeButton.isHidden = true
|
||||
bottomSpacer.isHidden = true
|
||||
stackView.pin(.leading, to: .leading, of: contentView, withInset: 16)
|
||||
stackView.pin(.top, to: .top, of: contentView, withInset: 16)
|
||||
contentView.pin(.trailing, to: .trailing, of: stackView, withInset: 16)
|
||||
contentView.pin(.bottom, to: .bottom, of: stackView, withInset: 16)
|
||||
stackView.pin(.leading, to: .leading, of: contentView, withInset: Values.largeSpacing)
|
||||
stackView.pin(.top, to: .top, of: contentView, withInset: Values.largeSpacing)
|
||||
contentView.pin(.trailing, to: .trailing, of: stackView, withInset: Values.largeSpacing)
|
||||
contentView.pin(.bottom, to: .bottom, of: stackView, withInset: Values.largeSpacing)
|
||||
}
|
||||
|
||||
// MARK: Device Linking
|
||||
func requestUserAuthorization(for deviceLink: DeviceLink) {
|
||||
self.deviceLink = deviceLink
|
||||
topSpacer.isHidden = true
|
||||
qrCodeImageView.isHidden = true
|
||||
titleLabel.text = NSLocalizedString("Linking Request Received", comment: "")
|
||||
subtitleLabel.text = NSLocalizedString("Please check that the words below match the ones shown on your other device", comment: "")
|
||||
|
@ -186,24 +174,23 @@ final class DeviceLinkingModal : Modal, DeviceLinkingSessionDelegate {
|
|||
func handleDeviceLinkAuthorized(_ deviceLink: DeviceLink) {
|
||||
let session = DeviceLinkingSession.current!
|
||||
session.stopListeningForLinkingAuthorization()
|
||||
topSpacer.isHidden = true
|
||||
spinner.stopAnimating()
|
||||
spinner.isHidden = true
|
||||
titleLabel.text = NSLocalizedString("Device Link Authorized", comment: "")
|
||||
subtitleLabel.text = NSLocalizedString("Your device has been linked successfully", comment: "")
|
||||
mnemonicLabel.isHidden = true
|
||||
buttonStackView.isHidden = true
|
||||
bottomSpacer.isHidden = false
|
||||
LokiStorageAPI.addDeviceLink(deviceLink).catch { error in
|
||||
print("[Loki] Failed to add device link due to error: \(error).")
|
||||
}
|
||||
Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { _ in
|
||||
self.delegate?.handleDeviceLinkAuthorized(deviceLink)
|
||||
self.dismiss(animated: true, completion: nil)
|
||||
self.dismiss(animated: true) {
|
||||
self.delegate?.handleDeviceLinkAuthorized(deviceLink)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc override func cancel() {
|
||||
@objc override func close() {
|
||||
guard let session = DeviceLinkingSession.current else {
|
||||
return print("[Loki] Device linking session missing.") // Should never occur
|
||||
}
|
|
@ -18,38 +18,53 @@ final class DeviceLinksVC : UIViewController, UITableViewDataSource, UITableView
|
|||
|
||||
private lazy var callToActionView : UIStackView = {
|
||||
let explanationLabel = UILabel()
|
||||
explanationLabel.textColor = Theme.primaryColor
|
||||
explanationLabel.font = UIFont.ows_dynamicTypeSubheadlineClamped
|
||||
explanationLabel.textColor = Colors.text
|
||||
explanationLabel.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
explanationLabel.numberOfLines = 0
|
||||
explanationLabel.lineBreakMode = .byWordWrapping
|
||||
explanationLabel.textAlignment = .center
|
||||
explanationLabel.text = NSLocalizedString("You don't have any linked devices yet", comment: "")
|
||||
let linkNewDeviceButtonFont = UIFont.ows_dynamicTypeBodyClamped.ows_mediumWeight()
|
||||
let linkNewDeviceButtonHeight = linkNewDeviceButtonFont.pointSize * 48 / 17
|
||||
let linkNewDeviceButton = OWSFlatButton.button(title: NSLocalizedString("Link a Device", comment: ""), font: linkNewDeviceButtonFont, titleColor: .lokiGreen(), backgroundColor: .clear, target: self, selector: #selector(linkNewDevice))
|
||||
linkNewDeviceButton.setBackgroundColors(upColor: .clear, downColor: .clear)
|
||||
linkNewDeviceButton.autoSetDimension(.height, toSize: linkNewDeviceButtonHeight)
|
||||
linkNewDeviceButton.button.contentHorizontalAlignment = .left
|
||||
let linkNewDeviceButton = Button(style: .prominentOutline, size: .medium)
|
||||
linkNewDeviceButton.setTitle(NSLocalizedString("Link a Device", comment: ""), for: UIControl.State.normal)
|
||||
linkNewDeviceButton.addTarget(self, action: #selector(linkNewDevice), for: UIControl.Event.touchUpInside)
|
||||
linkNewDeviceButton.set(.width, to: 160)
|
||||
let result = UIStackView(arrangedSubviews: [ explanationLabel, linkNewDeviceButton ])
|
||||
result.axis = .vertical
|
||||
result.spacing = 4
|
||||
result.spacing = Values.mediumSpacing
|
||||
result.alignment = .center
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: Lifecycle
|
||||
override func viewDidLoad() {
|
||||
title = NSLocalizedString("Linked Devices", comment: "")
|
||||
let masterDeviceHexEncodedPublicKey = UserDefaults.standard.string(forKey: "masterDeviceHexEncodedPublicKey")
|
||||
let isMasterDevice = (masterDeviceHexEncodedPublicKey == nil)
|
||||
if isMasterDevice {
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(linkNewDevice))
|
||||
}
|
||||
view.backgroundColor = Theme.backgroundColor
|
||||
// Set gradient background
|
||||
view.backgroundColor = .clear
|
||||
let gradient = Gradients.defaultLokiBackground
|
||||
view.setGradient(gradient)
|
||||
// Set navigation bar background color
|
||||
let navigationBar = navigationController!.navigationBar
|
||||
navigationBar.setBackgroundImage(UIImage(), for: UIBarMetrics.default)
|
||||
navigationBar.shadowImage = UIImage()
|
||||
navigationBar.isTranslucent = false
|
||||
navigationBar.barTintColor = Colors.navigationBarBackground
|
||||
// Customize title
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.text = NSLocalizedString("Linked Devices", comment: "")
|
||||
titleLabel.textColor = Colors.text
|
||||
titleLabel.font = .boldSystemFont(ofSize: Values.veryLargeFontSize)
|
||||
navigationItem.titleView = titleLabel
|
||||
// Set up link new device button
|
||||
let linkNewDeviceButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(linkNewDevice))
|
||||
linkNewDeviceButton.tintColor = Colors.text
|
||||
navigationItem.rightBarButtonItem = linkNewDeviceButton
|
||||
// Set up constraints
|
||||
view.addSubview(tableView)
|
||||
tableView.pin(to: view)
|
||||
view.addSubview(callToActionView)
|
||||
callToActionView.center(in: view)
|
||||
callToActionView.center(.horizontal, in: view)
|
||||
let verticalCenteringConstraint = callToActionView.center(.vertical, in: view)
|
||||
verticalCenteringConstraint.constant = -16 // Makes things appear centered visually
|
||||
// Perform initial update
|
||||
updateDeviceLinks()
|
||||
}
|
||||
|
||||
|
@ -61,7 +76,7 @@ final class DeviceLinksVC : UIViewController, UITableViewDataSource, UITableView
|
|||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as! Cell
|
||||
let selectedBackgroundView = UIView()
|
||||
selectedBackgroundView.backgroundColor = Theme.cellSelectedColor
|
||||
selectedBackgroundView.backgroundColor = Colors.cellSelected
|
||||
cell.selectedBackgroundView = selectedBackgroundView
|
||||
let device = deviceLinks[indexPath.row].other
|
||||
cell.device = device
|
||||
|
@ -102,6 +117,7 @@ final class DeviceLinksVC : UIViewController, UITableViewDataSource, UITableView
|
|||
if deviceLinks.isEmpty {
|
||||
let deviceLinkingModal = DeviceLinkingModal(mode: .master, delegate: self)
|
||||
deviceLinkingModal.modalPresentationStyle = .overFullScreen
|
||||
deviceLinkingModal.modalTransitionStyle = .crossDissolve
|
||||
present(deviceLinkingModal, animated: true, completion: nil)
|
||||
} else {
|
||||
let alert = UIAlertController(title: NSLocalizedString("Multi Device Limit Reached", comment: ""), message: NSLocalizedString("It's currently not allowed to link more than one device.", comment: ""), preferredStyle: .alert)
|
||||
|
@ -119,6 +135,8 @@ final class DeviceLinksVC : UIViewController, UITableViewDataSource, UITableView
|
|||
let deviceNameModal = DeviceNameModal()
|
||||
deviceNameModal.device = deviceLink.other
|
||||
deviceNameModal.delegate = self
|
||||
deviceNameModal.modalPresentationStyle = .overFullScreen
|
||||
deviceNameModal.modalTransitionStyle = .crossDissolve
|
||||
self.present(deviceNameModal, animated: true, completion: nil)
|
||||
})
|
||||
sheet.addAction(UIAlertAction(title: NSLocalizedString("Unlink", comment: ""), style: .destructive) { [weak self] _ in
|
||||
|
@ -169,17 +187,16 @@ private extension DeviceLinksVC {
|
|||
// MARK: Components
|
||||
private lazy var titleLabel: UILabel = {
|
||||
let result = UILabel()
|
||||
result.textColor = Theme.primaryColor
|
||||
let font = UIFont.ows_dynamicTypeSubheadlineClamped
|
||||
result.font = UIFont(descriptor: font.fontDescriptor.withSymbolicTraits(.traitBold)!, size: font.pointSize)
|
||||
result.textColor = Colors.text
|
||||
result.font = .boldSystemFont(ofSize: Values.mediumFontSize)
|
||||
result.lineBreakMode = .byTruncatingTail
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var subtitleLabel: UILabel = {
|
||||
let result = UILabel()
|
||||
result.textColor = Theme.primaryColor
|
||||
result.font = UIFont.ows_dynamicTypeCaption1Clamped
|
||||
result.textColor = Colors.text
|
||||
result.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
result.lineBreakMode = .byTruncatingTail
|
||||
return result
|
||||
}()
|
||||
|
@ -196,18 +213,18 @@ private extension DeviceLinksVC {
|
|||
}
|
||||
|
||||
private func setUpViewHierarchy() {
|
||||
backgroundColor = .clear
|
||||
backgroundColor = Colors.cellBackground
|
||||
let stackView = UIStackView(arrangedSubviews: [ titleLabel, subtitleLabel ])
|
||||
stackView.axis = .vertical
|
||||
stackView.distribution = .equalCentering
|
||||
stackView.spacing = 4
|
||||
stackView.spacing = Values.verySmallSpacing
|
||||
stackView.set(.height, to: 36)
|
||||
contentView.addSubview(stackView)
|
||||
stackView.pin(.leading, to: .leading, of: contentView, withInset: 16)
|
||||
stackView.pin(.top, to: .top, of: contentView, withInset: 8)
|
||||
contentView.pin(.trailing, to: .trailing, of: stackView, withInset: 16)
|
||||
contentView.pin(.bottom, to: .bottom, of: stackView, withInset: 8)
|
||||
stackView.set(.width, to: UIScreen.main.bounds.width - 2 * 16)
|
||||
stackView.pin(.leading, to: .leading, of: contentView, withInset: Values.largeSpacing)
|
||||
stackView.pin(.top, to: .top, of: contentView, withInset: Values.mediumSpacing)
|
||||
contentView.pin(.trailing, to: .trailing, of: stackView, withInset: Values.largeSpacing)
|
||||
contentView.pin(.bottom, to: .bottom, of: stackView, withInset: Values.mediumSpacing)
|
||||
stackView.set(.width, to: UIScreen.main.bounds.width - 2 * Values.largeSpacing)
|
||||
}
|
||||
|
||||
// MARK: Updating
|
|
@ -5,15 +5,15 @@ final class DeviceNameModal : Modal {
|
|||
@objc public var delegate: DeviceNameModalDelegate?
|
||||
|
||||
// MARK: Components
|
||||
private lazy var nameTextView: UITextField = {
|
||||
private lazy var nameTextField: UITextField = {
|
||||
let result = UITextField()
|
||||
result.textColor = Theme.primaryColor
|
||||
result.font = .ows_dynamicTypeBodyClamped
|
||||
result.textColor = Colors.text
|
||||
result.font = .systemFont(ofSize: Values.mediumFontSize)
|
||||
result.textAlignment = .center
|
||||
let placeholder = NSMutableAttributedString(string: NSLocalizedString("Enter a Name", comment: ""))
|
||||
placeholder.addAttribute(.foregroundColor, value: UIColor.white.withAlphaComponent(0.5), range: NSRange(location: 0, length: placeholder.length))
|
||||
placeholder.addAttribute(.foregroundColor, value: Colors.text.withAlphaComponent(Values.unimportantElementOpacity), range: NSRange(location: 0, length: placeholder.length))
|
||||
result.attributedPlaceholder = placeholder
|
||||
result.tintColor = .lokiGreen()
|
||||
result.tintColor = Colors.accent
|
||||
result.keyboardAppearance = .dark
|
||||
return result
|
||||
}()
|
||||
|
@ -21,44 +21,51 @@ final class DeviceNameModal : Modal {
|
|||
// MARK: Lifecycle
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(handleKeyboardWillChangeFrameNotification(_:)), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
|
||||
let notificationCenter = NotificationCenter.default
|
||||
notificationCenter.addObserver(self, selector: #selector(handleKeyboardWillChangeFrameNotification(_:)), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(handleKeyboardWillHideNotification(_:)), name: UIResponder.keyboardWillHideNotification, object: nil)
|
||||
}
|
||||
|
||||
override func populateContentView() {
|
||||
// Label
|
||||
// Set up title label
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.textColor = Theme.primaryColor
|
||||
titleLabel.font = UIFont.ows_dynamicTypeHeadlineClamped
|
||||
titleLabel.textColor = Colors.text
|
||||
titleLabel.font = .boldSystemFont(ofSize: Values.mediumFontSize)
|
||||
titleLabel.text = NSLocalizedString("Change Device Name", comment: "")
|
||||
titleLabel.numberOfLines = 0
|
||||
titleLabel.lineBreakMode = .byWordWrapping
|
||||
titleLabel.textAlignment = .center
|
||||
// Explanation label
|
||||
// Set up explanation label
|
||||
let explanationLabel = UILabel()
|
||||
explanationLabel.font = UIFont.ows_dynamicTypeCaption1Clamped
|
||||
explanationLabel.textColor = Colors.text
|
||||
explanationLabel.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
explanationLabel.text = NSLocalizedString("Enter the new display name for your device below", comment: "")
|
||||
explanationLabel.numberOfLines = 0
|
||||
explanationLabel.textAlignment = .center
|
||||
explanationLabel.lineBreakMode = .byWordWrapping
|
||||
explanationLabel.textColor = UIColor.ows_white
|
||||
// Button stack view
|
||||
let okButton = OWSFlatButton.button(title: NSLocalizedString("OK", comment: ""), font: .ows_dynamicTypeBodyClamped, titleColor: .white, backgroundColor: .clear, target: self, selector: #selector(changeName))
|
||||
okButton.setBackgroundColors(upColor: .clear, downColor: .clear)
|
||||
let buttonStackView = UIStackView(arrangedSubviews: [ okButton, cancelButton ])
|
||||
// Set up OK button
|
||||
let okButton = UIButton()
|
||||
okButton.set(.height, to: Values.mediumButtonHeight)
|
||||
okButton.layer.cornerRadius = Values.modalButtonCornerRadius
|
||||
okButton.backgroundColor = Colors.accent
|
||||
okButton.titleLabel!.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
okButton.setTitleColor(Colors.text, for: UIControl.State.normal)
|
||||
okButton.setTitle(NSLocalizedString("OK", comment: ""), for: UIControl.State.normal)
|
||||
okButton.addTarget(self, action: #selector(changeName), for: UIControl.Event.touchUpInside)
|
||||
// Set up button stack view
|
||||
let buttonStackView = UIStackView(arrangedSubviews: [ cancelButton, okButton ])
|
||||
buttonStackView.axis = .horizontal
|
||||
buttonStackView.spacing = Values.mediumSpacing
|
||||
buttonStackView.distribution = .fillEqually
|
||||
let buttonHeight = cancelButton.button.titleLabel!.font.pointSize * 48 / 17
|
||||
okButton.set(.height, to: buttonHeight)
|
||||
cancelButton.set(.height, to: buttonHeight)
|
||||
// Stack view
|
||||
let stackView = UIStackView(arrangedSubviews: [ UIView.spacer(withHeight: 2), titleLabel, explanationLabel, nameTextView, buttonStackView ])
|
||||
let stackView = UIStackView(arrangedSubviews: [ titleLabel, explanationLabel, nameTextField, buttonStackView ])
|
||||
stackView.axis = .vertical
|
||||
stackView.spacing = 16
|
||||
stackView.spacing = Values.largeSpacing
|
||||
contentView.addSubview(stackView)
|
||||
stackView.pin(.leading, to: .leading, of: contentView, withInset: 16)
|
||||
stackView.pin(.top, to: .top, of: contentView, withInset: 16)
|
||||
contentView.pin(.trailing, to: .trailing, of: stackView, withInset: 16)
|
||||
contentView.pin(.bottom, to: .bottom, of: stackView, withInset: 16)
|
||||
stackView.pin(.leading, to: .leading, of: contentView, withInset: Values.largeSpacing)
|
||||
stackView.pin(.top, to: .top, of: contentView, withInset: Values.largeSpacing)
|
||||
contentView.pin(.trailing, to: .trailing, of: stackView, withInset: Values.largeSpacing)
|
||||
contentView.pin(.bottom, to: .bottom, of: stackView, withInset: Values.largeSpacing)
|
||||
}
|
||||
|
||||
deinit {
|
||||
|
@ -74,9 +81,16 @@ final class DeviceNameModal : Modal {
|
|||
}
|
||||
}
|
||||
|
||||
@objc private func handleKeyboardWillHideNotification(_ notification: Notification) {
|
||||
verticalCenteringConstraint.constant = 0
|
||||
UIView.animate(withDuration: 0.25) {
|
||||
self.view.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Interaction
|
||||
@objc private func changeName() {
|
||||
let name = nameTextView.text!.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
|
||||
let name = nameTextField.text!.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
|
||||
if !name.isEmpty {
|
||||
UserDefaults.standard.set(name, forKey: "\(device.hexEncodedPublicKey)_display_name")
|
||||
delegate?.handleDeviceNameChanged(to: name, for: device)
|
|
@ -0,0 +1,162 @@
|
|||
|
||||
final class DisplayNameVC : UIViewController {
|
||||
private var spacer1HeightConstraint: NSLayoutConstraint!
|
||||
private var spacer2HeightConstraint: NSLayoutConstraint!
|
||||
private var registerButtonBottomOffsetConstraint: NSLayoutConstraint!
|
||||
private var bottomConstraint: NSLayoutConstraint!
|
||||
|
||||
// MARK: Components
|
||||
private lazy var displayNameTextField: TextField = {
|
||||
let result = TextField(placeholder: NSLocalizedString("Enter a display name", comment: ""))
|
||||
result.layer.borderColor = Colors.text.cgColor
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: Settings
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle { return .lightContent }
|
||||
|
||||
// MARK: Lifecycle
|
||||
override func viewDidLoad() {
|
||||
// Set gradient background
|
||||
view.backgroundColor = .clear
|
||||
let gradient = Gradients.defaultLokiBackground
|
||||
view.setGradient(gradient)
|
||||
// Set up navigation bar
|
||||
let navigationBar = navigationController!.navigationBar
|
||||
navigationBar.setBackgroundImage(UIImage(), for: UIBarMetrics.default)
|
||||
navigationBar.shadowImage = UIImage()
|
||||
navigationBar.isTranslucent = false
|
||||
navigationBar.barTintColor = Colors.navigationBarBackground
|
||||
// Set up logo image view
|
||||
let logoImageView = UIImageView()
|
||||
logoImageView.image = #imageLiteral(resourceName: "Session")
|
||||
logoImageView.contentMode = .scaleAspectFit
|
||||
logoImageView.set(.width, to: 32)
|
||||
logoImageView.set(.height, to: 32)
|
||||
navigationItem.titleView = logoImageView
|
||||
// Set up title label
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.textColor = Colors.text
|
||||
titleLabel.font = .boldSystemFont(ofSize: Values.veryLargeFontSize)
|
||||
titleLabel.text = NSLocalizedString("Pick your display name", comment: "")
|
||||
titleLabel.numberOfLines = 0
|
||||
titleLabel.lineBreakMode = .byWordWrapping
|
||||
// Set up explanation label
|
||||
let explanationLabel = UILabel()
|
||||
explanationLabel.textColor = Colors.text
|
||||
explanationLabel.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
explanationLabel.text = "This is how others will be able to recognize you."
|
||||
explanationLabel.numberOfLines = 0
|
||||
explanationLabel.lineBreakMode = .byWordWrapping
|
||||
// Set up spacers
|
||||
let topSpacer = UIView.vStretchingSpacer()
|
||||
let spacer1 = UIView()
|
||||
spacer1HeightConstraint = spacer1.set(.height, to: Values.veryLargeSpacing)
|
||||
let spacer2 = UIView()
|
||||
spacer2HeightConstraint = spacer2.set(.height, to: Values.veryLargeSpacing)
|
||||
let bottomSpacer = UIView.vStretchingSpacer()
|
||||
let registerButtonBottomOffsetSpacer = UIView()
|
||||
registerButtonBottomOffsetConstraint = registerButtonBottomOffsetSpacer.set(.height, to: Values.onboardingButtonBottomOffset)
|
||||
// Set up register button
|
||||
let registerButton = Button(style: .prominentFilled, size: .large)
|
||||
registerButton.setTitle(NSLocalizedString("Continue", comment: ""), for: UIControl.State.normal)
|
||||
registerButton.titleLabel!.font = .boldSystemFont(ofSize: Values.mediumFontSize)
|
||||
registerButton.addTarget(self, action: #selector(register), for: UIControl.Event.touchUpInside)
|
||||
// Set up register button container
|
||||
let registerButtonContainer = UIView()
|
||||
registerButtonContainer.addSubview(registerButton)
|
||||
registerButton.pin(.leading, to: .leading, of: registerButtonContainer, withInset: Values.massiveSpacing)
|
||||
registerButton.pin(.top, to: .top, of: registerButtonContainer)
|
||||
registerButtonContainer.pin(.trailing, to: .trailing, of: registerButton, withInset: Values.massiveSpacing)
|
||||
registerButtonContainer.pin(.bottom, to: .bottom, of: registerButton)
|
||||
// Set up top stack view
|
||||
let topStackView = UIStackView(arrangedSubviews: [ titleLabel, spacer1, explanationLabel, spacer2, displayNameTextField ])
|
||||
topStackView.axis = .vertical
|
||||
topStackView.alignment = .fill
|
||||
// Set up top stack view container
|
||||
let topStackViewContainer = UIView()
|
||||
topStackViewContainer.addSubview(topStackView)
|
||||
topStackView.pin(.leading, to: .leading, of: topStackViewContainer, withInset: Values.veryLargeSpacing)
|
||||
topStackView.pin(.top, to: .top, of: topStackViewContainer)
|
||||
topStackViewContainer.pin(.trailing, to: .trailing, of: topStackView, withInset: Values.veryLargeSpacing)
|
||||
topStackViewContainer.pin(.bottom, to: .bottom, of: topStackView)
|
||||
// Set up main stack view
|
||||
let mainStackView = UIStackView(arrangedSubviews: [ topSpacer, topStackViewContainer, bottomSpacer, registerButtonContainer, registerButtonBottomOffsetSpacer ])
|
||||
mainStackView.axis = .vertical
|
||||
mainStackView.alignment = .fill
|
||||
view.addSubview(mainStackView)
|
||||
mainStackView.pin(.leading, to: .leading, of: view)
|
||||
mainStackView.pin(.top, to: .top, of: view)
|
||||
mainStackView.pin(.trailing, to: .trailing, of: view)
|
||||
bottomConstraint = mainStackView.pin(.bottom, to: .bottom, of: view)
|
||||
topSpacer.heightAnchor.constraint(equalTo: bottomSpacer.heightAnchor, multiplier: 1).isActive = true
|
||||
// Dismiss keyboard on tap
|
||||
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
|
||||
view.addGestureRecognizer(tapGestureRecognizer)
|
||||
// Listen to keyboard notifications
|
||||
let notificationCenter = NotificationCenter.default
|
||||
notificationCenter.addObserver(self, selector: #selector(handleKeyboardWillChangeFrameNotification(_:)), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(handleKeyboardWillHideNotification(_:)), name: UIResponder.keyboardWillHideNotification, object: nil)
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
displayNameTextField.becomeFirstResponder()
|
||||
}
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
// MARK: General
|
||||
@objc private func dismissKeyboard() {
|
||||
displayNameTextField.resignFirstResponder()
|
||||
}
|
||||
|
||||
// MARK: Updating
|
||||
@objc private func handleKeyboardWillChangeFrameNotification(_ notification: Notification) {
|
||||
guard let newHeight = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue.size.height else { return }
|
||||
bottomConstraint.constant = -newHeight // Negative due to how the constraint is set up
|
||||
registerButtonBottomOffsetConstraint.constant = Values.largeSpacing
|
||||
spacer1HeightConstraint.constant = Values.mediumSpacing
|
||||
spacer2HeightConstraint.constant = Values.mediumSpacing
|
||||
UIView.animate(withDuration: 0.25) {
|
||||
self.view.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func handleKeyboardWillHideNotification(_ notification: Notification) {
|
||||
bottomConstraint.constant = 0
|
||||
registerButtonBottomOffsetConstraint.constant = Values.onboardingButtonBottomOffset
|
||||
spacer1HeightConstraint.constant = Values.veryLargeSpacing
|
||||
spacer2HeightConstraint.constant = Values.veryLargeSpacing
|
||||
UIView.animate(withDuration: 0.25) {
|
||||
self.view.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Interaction
|
||||
@objc private func register() {
|
||||
func showError(title: String, message: String = "") {
|
||||
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
|
||||
presentAlert(alert)
|
||||
}
|
||||
let displayName = displayNameTextField.text!.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
|
||||
guard !displayName.isEmpty else {
|
||||
return showError(title: NSLocalizedString("Please pick a display name", comment: ""))
|
||||
}
|
||||
let allowedCharacters = CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_ ")
|
||||
let hasInvalidCharacters = !displayName.allSatisfy { $0.unicodeScalars.allSatisfy { allowedCharacters.contains($0) } }
|
||||
guard !hasInvalidCharacters else {
|
||||
return showError(title: NSLocalizedString("Please pick a display name that consists of only a-z, A-Z, 0-9 and _ characters", comment: ""))
|
||||
}
|
||||
guard !OWSProfileManager.shared().isProfileNameTooLong(displayName) else {
|
||||
return showError(title: NSLocalizedString("Please pick a shorter display name", comment: ""))
|
||||
}
|
||||
TSAccountManager.sharedInstance().didRegister()
|
||||
OWSProfileManager.shared().updateLocalProfileName(displayName, avatarImage: nil, success: { }, failure: { }) // Try to save the user name but ignore the result
|
||||
let homeVC = HomeVC()
|
||||
navigationController!.setViewControllers([ homeVC ], animated: true)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,385 @@
|
|||
|
||||
final class HomeVC : UIViewController, UITableViewDataSource, UITableViewDelegate, UIScrollViewDelegate, UIViewControllerPreviewingDelegate, SeedReminderViewDelegate {
|
||||
private var threadViewModelCache: [String:ThreadViewModel] = [:]
|
||||
private var isObservingDatabase = true
|
||||
private var isViewVisible = false { didSet { updateIsObservingDatabase() } }
|
||||
private var tableViewTopConstraint: NSLayoutConstraint!
|
||||
|
||||
private var threads: YapDatabaseViewMappings = {
|
||||
let result = YapDatabaseViewMappings(groups: [ TSInboxGroup ], view: TSThreadDatabaseViewExtensionName)
|
||||
result.setIsReversed(true, forGroup: TSInboxGroup)
|
||||
return result
|
||||
}()
|
||||
|
||||
private let uiDatabaseConnection: YapDatabaseConnection = {
|
||||
let result = OWSPrimaryStorage.shared().newDatabaseConnection()
|
||||
result.objectCacheLimit = 500
|
||||
return result
|
||||
}()
|
||||
|
||||
private let editingDatabaseConnection = OWSPrimaryStorage.shared().newDatabaseConnection()
|
||||
|
||||
// MARK: Settings
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle { return .lightContent }
|
||||
|
||||
// MARK: Components
|
||||
private lazy var seedReminderView: SeedReminderView = {
|
||||
let result = SeedReminderView(hasContinueButton: true)
|
||||
let title = "You're almost finished! 80%"
|
||||
let attributedTitle = NSMutableAttributedString(string: title)
|
||||
attributedTitle.addAttribute(.foregroundColor, value: Colors.accent, range: (title as NSString).range(of: "80%"))
|
||||
result.title = attributedTitle
|
||||
result.subtitle = NSLocalizedString("Secure your account by saving your seed", comment: "")
|
||||
result.setProgress(0.8, animated: false)
|
||||
result.delegate = self
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var searchBar = SearchBar()
|
||||
|
||||
private lazy var tableView: UITableView = {
|
||||
let result = UITableView()
|
||||
result.backgroundColor = .clear
|
||||
result.separatorStyle = .none
|
||||
result.register(ConversationCell.self, forCellReuseIdentifier: ConversationCell.reuseIdentifier)
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var newConversationButton: UIButton = {
|
||||
let result = UIButton()
|
||||
result.setTitle("+", for: UIControl.State.normal)
|
||||
result.titleLabel!.font = .systemFont(ofSize: 35)
|
||||
result.setTitleColor(UIColor(hex: 0x121212), for: UIControl.State.normal)
|
||||
result.titleEdgeInsets = UIEdgeInsets(top: 0, left: 1, bottom: 4, right: 0) // Slight adjustment to make the plus exactly centered
|
||||
result.backgroundColor = Colors.accent
|
||||
let size = Values.newConversationButtonSize
|
||||
result.layer.cornerRadius = size / 2
|
||||
result.layer.shadowPath = UIBezierPath(ovalIn: CGRect(origin: CGPoint(x: 0, y: 0), size: CGSize(width: size, height: size))).cgPath
|
||||
result.layer.shadowColor = Colors.newConversationButtonShadow.cgColor
|
||||
result.layer.shadowOffset = CGSize(width: 0, height: 0.8)
|
||||
result.layer.shadowOpacity = 1
|
||||
result.layer.shadowRadius = 6
|
||||
result.layer.masksToBounds = false
|
||||
result.set(.width, to: size)
|
||||
result.set(.height, to: size)
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: Lifecycle
|
||||
override func viewDidLoad() {
|
||||
SignalApp.shared().homeViewController = self
|
||||
// Set gradient background
|
||||
view.backgroundColor = .clear
|
||||
let gradient = Gradients.defaultLokiBackground
|
||||
view.setGradient(gradient)
|
||||
// Set navigation bar background color
|
||||
if let navigationBar = navigationController?.navigationBar {
|
||||
navigationBar.setBackgroundImage(UIImage(), for: UIBarMetrics.default)
|
||||
navigationBar.shadowImage = UIImage()
|
||||
navigationBar.isTranslucent = false
|
||||
navigationBar.barTintColor = Colors.navigationBarBackground
|
||||
}
|
||||
// Set up navigation bar buttons
|
||||
updateNavigationBarButtons()
|
||||
// Customize title
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.text = NSLocalizedString("Messages", comment: "")
|
||||
titleLabel.textColor = Colors.text
|
||||
titleLabel.font = .boldSystemFont(ofSize: Values.veryLargeFontSize)
|
||||
navigationItem.titleView = titleLabel
|
||||
// Set up seed reminder view if needed
|
||||
let hasViewedSeed = UserDefaults.standard.bool(forKey: "hasViewedSeed")
|
||||
if !hasViewedSeed {
|
||||
view.addSubview(seedReminderView)
|
||||
seedReminderView.pin(.leading, to: .leading, of: view)
|
||||
seedReminderView.pin(.top, to: .top, of: view)
|
||||
seedReminderView.pin(.trailing, to: .trailing, of: view)
|
||||
}
|
||||
// Set up table view
|
||||
tableView.dataSource = self
|
||||
tableView.delegate = self
|
||||
view.addSubview(tableView)
|
||||
tableView.pin(.leading, to: .leading, of: view)
|
||||
if !hasViewedSeed {
|
||||
tableViewTopConstraint = tableView.pin(.top, to: .bottom, of: seedReminderView)
|
||||
} else {
|
||||
tableViewTopConstraint = tableView.pin(.top, to: .top, of: view)
|
||||
}
|
||||
tableView.pin(.trailing, to: .trailing, of: view)
|
||||
tableView.pin(.bottom, to: .bottom, of: view)
|
||||
// Set up search bar
|
||||
tableView.tableHeaderView = searchBar
|
||||
searchBar.sizeToFit()
|
||||
tableView.contentOffset = CGPoint(x: 0, y: searchBar.frame.height)
|
||||
// Set up new conversation button
|
||||
newConversationButton.addTarget(self, action: #selector(createPrivateChat), for: UIControl.Event.touchUpInside)
|
||||
view.addSubview(newConversationButton)
|
||||
newConversationButton.center(.horizontal, in: view)
|
||||
newConversationButton.pin(.bottom, to: .bottom, of: view, withInset: -Values.newConversationButtonBottomOffset) // Negative due to how the constraint is set up
|
||||
// Set up previewing
|
||||
if (traitCollection.forceTouchCapability == .available) {
|
||||
registerForPreviewing(with: self, sourceView: tableView)
|
||||
}
|
||||
// Listen for notifications
|
||||
let notificationCenter = NotificationCenter.default
|
||||
notificationCenter.addObserver(self, selector: #selector(handleYapDatabaseModifiedNotification(_:)), name: .YapDatabaseModified, object: OWSPrimaryStorage.shared().dbNotificationObject)
|
||||
notificationCenter.addObserver(self, selector: #selector(handleYapDatabaseModifiedExternallyNotification(_:)), name: .YapDatabaseModifiedExternally, object: OWSPrimaryStorage.shared().dbNotificationObject)
|
||||
notificationCenter.addObserver(self, selector: #selector(handleApplicationDidBecomeActiveNotification(_:)), name: .OWSApplicationDidBecomeActive, object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(handleApplicationWillResignActiveNotification(_:)), name: .OWSApplicationWillResignActive, object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(handleProfileDidChangeNotification(_:)), name: NSNotification.Name(rawValue: kNSNotificationName_OtherUsersProfileDidChange), object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(handleLocalProfileDidChangeNotification(_:)), name: Notification.Name(kNSNotificationName_LocalProfileDidChange), object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(handleSeedViewedNotification(_:)), name: .seedViewed, object: nil)
|
||||
// Set up public chats and RSS feeds if needed
|
||||
if OWSIdentityManager.shared().identityKeyPair() != nil {
|
||||
let appDelegate = UIApplication.shared.delegate as! AppDelegate
|
||||
appDelegate.setUpDefaultPublicChatsIfNeeded()
|
||||
appDelegate.createRSSFeedsIfNeeded()
|
||||
LokiPublicChatManager.shared.startPollersIfNeeded()
|
||||
appDelegate.startRSSFeedPollersIfNeeded()
|
||||
}
|
||||
// Do initial update
|
||||
reload()
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
isViewVisible = true
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
isViewVisible = false
|
||||
}
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
// MARK: Data
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return Int(threads.numberOfItems(inGroup: TSInboxGroup))
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: ConversationCell.reuseIdentifier) as! ConversationCell
|
||||
cell.threadViewModel = threadViewModel(at: indexPath.row)
|
||||
return cell
|
||||
}
|
||||
|
||||
// MARK: Updating
|
||||
private func updateIsObservingDatabase() {
|
||||
isObservingDatabase = isViewVisible && CurrentAppContext().isAppForegroundAndActive()
|
||||
}
|
||||
|
||||
private func reload() {
|
||||
uiDatabaseConnection.beginLongLivedReadTransaction()
|
||||
uiDatabaseConnection.read { transaction in
|
||||
self.threads.update(with: transaction)
|
||||
}
|
||||
tableView.reloadData()
|
||||
}
|
||||
|
||||
@objc private func handleYapDatabaseModifiedExternallyNotification(_ notification: Notification) {
|
||||
guard isObservingDatabase else { return }
|
||||
reload()
|
||||
}
|
||||
|
||||
@objc private func handleYapDatabaseModifiedNotification(_ notification: Notification) {
|
||||
guard isObservingDatabase else { return }
|
||||
let transaction = uiDatabaseConnection.beginLongLivedReadTransaction()
|
||||
let hasChanges = (uiDatabaseConnection.ext(TSThreadDatabaseViewExtensionName) as! YapDatabaseViewConnection).hasChanges(forGroup: TSInboxGroup, in: transaction)
|
||||
guard hasChanges else {
|
||||
uiDatabaseConnection.read { transaction in
|
||||
self.threads.update(with: transaction)
|
||||
}
|
||||
return
|
||||
}
|
||||
var sectionChanges = NSArray()
|
||||
var rowChanges = NSArray()
|
||||
(uiDatabaseConnection.ext(TSThreadDatabaseViewExtensionName) as! YapDatabaseViewConnection).getSectionChanges(§ionChanges, rowChanges: &rowChanges, for: transaction, with: threads)
|
||||
guard sectionChanges.count > 0 || rowChanges.count > 0 else { return }
|
||||
tableView.beginUpdates()
|
||||
rowChanges.forEach { rowChange in
|
||||
let rowChange = rowChange as! YapDatabaseViewRowChange
|
||||
let key = rowChange.collectionKey.key
|
||||
threadViewModelCache[key] = nil
|
||||
switch rowChange.type {
|
||||
case .delete: tableView.deleteRows(at: [ rowChange.indexPath! ], with: UITableView.RowAnimation.fade)
|
||||
case .insert: tableView.insertRows(at: [ rowChange.newIndexPath! ], with: UITableView.RowAnimation.fade)
|
||||
case .move:
|
||||
tableView.deleteRows(at: [ rowChange.indexPath! ], with: UITableView.RowAnimation.fade)
|
||||
tableView.insertRows(at: [ rowChange.newIndexPath! ], with: UITableView.RowAnimation.fade)
|
||||
case .update:
|
||||
tableView.reloadRows(at: [ rowChange.indexPath! ], with: UITableView.RowAnimation.none)
|
||||
default: break
|
||||
}
|
||||
}
|
||||
tableView.endUpdates()
|
||||
}
|
||||
|
||||
@objc private func handleApplicationDidBecomeActiveNotification(_ notification: Notification) {
|
||||
updateIsObservingDatabase()
|
||||
}
|
||||
|
||||
@objc private func handleApplicationWillResignActiveNotification(_ notification: Notification) {
|
||||
updateIsObservingDatabase()
|
||||
}
|
||||
|
||||
@objc private func handleProfileDidChangeNotification(_ notification: Notification) {
|
||||
tableView.reloadData() // TODO: Just reload the affected cell
|
||||
}
|
||||
|
||||
@objc private func handleLocalProfileDidChangeNotification(_ notification: Notification) {
|
||||
updateNavigationBarButtons()
|
||||
}
|
||||
|
||||
@objc private func handleSeedViewedNotification(_ notification: Notification) {
|
||||
tableViewTopConstraint.isActive = false
|
||||
tableViewTopConstraint = tableView.pin(.top, to: .top, of: view)
|
||||
seedReminderView.removeFromSuperview()
|
||||
}
|
||||
|
||||
private func updateNavigationBarButtons() {
|
||||
let profilePictureSize = Values.verySmallProfilePictureSize
|
||||
let profilePictureView = ProfilePictureView()
|
||||
profilePictureView.size = profilePictureSize
|
||||
let userHexEncodedPublicKey = OWSIdentityManager.shared().identityKeyPair()!.hexEncodedPublicKey
|
||||
profilePictureView.hexEncodedPublicKey = userHexEncodedPublicKey
|
||||
profilePictureView.update()
|
||||
profilePictureView.set(.width, to: profilePictureSize)
|
||||
profilePictureView.set(.height, to: profilePictureSize)
|
||||
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(openSettings))
|
||||
profilePictureView.addGestureRecognizer(tapGestureRecognizer)
|
||||
navigationItem.leftBarButtonItem = UIBarButtonItem(customView: profilePictureView)
|
||||
// let createPrivateGroupChatButton = UIBarButtonItem(image: #imageLiteral(resourceName: "People"), style: .plain, target: self, action: #selector(createPrivateGroupChat))
|
||||
// createPrivateGroupChatButton.tintColor = Colors.text
|
||||
let joinPublicChatButton = UIBarButtonItem(image: #imageLiteral(resourceName: "Globe"), style: .plain, target: self, action: #selector(joinPublicChat))
|
||||
joinPublicChatButton.tintColor = Colors.text
|
||||
navigationItem.rightBarButtonItems = [ /*createPrivateGroupChatButton,*/ joinPublicChatButton ]
|
||||
}
|
||||
|
||||
// MARK: Interaction
|
||||
func handleContinueButtonTapped(from seedReminderView: SeedReminderView) {
|
||||
let seedVC = SeedVCV2()
|
||||
let navigationController = OWSNavigationController(rootViewController: seedVC)
|
||||
present(navigationController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||||
searchBar.resignFirstResponder()
|
||||
}
|
||||
|
||||
func previewingContext(_ previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController? {
|
||||
guard let indexPath = tableView.indexPathForRow(at: location), let thread = self.thread(at: indexPath.row) else { return nil }
|
||||
previewingContext.sourceRect = tableView.rectForRow(at: indexPath)
|
||||
let conversationVC = ConversationViewController()
|
||||
conversationVC.configure(for: thread, action: .none, focusMessageId: nil)
|
||||
conversationVC.peekSetup()
|
||||
return conversationVC
|
||||
}
|
||||
|
||||
func previewingContext(_ previewingContext: UIViewControllerPreviewing, commit viewControllerToCommit: UIViewController) {
|
||||
guard let conversationVC = viewControllerToCommit as? ConversationViewController else { return }
|
||||
conversationVC.popped()
|
||||
navigationController?.pushViewController(conversationVC, animated: false)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
guard let thread = self.thread(at: indexPath.row) else { return }
|
||||
show(thread, with: ConversationViewAction.none, highlightedMessageID: nil, animated: true)
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
}
|
||||
|
||||
@objc func show(_ thread: TSThread, with action: ConversationViewAction, highlightedMessageID: String?, animated: Bool) {
|
||||
DispatchMainThreadSafe {
|
||||
let conversationVC = ConversationViewController()
|
||||
conversationVC.configure(for: thread, action: action, focusMessageId: highlightedMessageID)
|
||||
self.navigationController?.setViewControllers([ self, conversationVC ], animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
|
||||
guard let threadID = self.thread(at: indexPath.row)?.uniqueId else { return false }
|
||||
var publicChat: LokiPublicChat?
|
||||
OWSPrimaryStorage.shared().dbReadConnection.read { transaction in
|
||||
publicChat = LokiDatabaseUtilities.getPublicChat(for: threadID, in: transaction)
|
||||
}
|
||||
if let publicChat = publicChat {
|
||||
return publicChat.isDeletable
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
|
||||
guard let thread = self.thread(at: indexPath.row) else { return [] }
|
||||
var publicChat: LokiPublicChat?
|
||||
OWSPrimaryStorage.shared().dbReadConnection.read { transaction in
|
||||
publicChat = LokiDatabaseUtilities.getPublicChat(for: thread.uniqueId!, in: transaction)
|
||||
}
|
||||
let delete = UITableViewRowAction(style: .destructive, title: NSLocalizedString("Delete", comment: "")) { [weak self] action, indexPath in
|
||||
let alert = UIAlertController(title: NSLocalizedString("CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE", comment: ""), message: NSLocalizedString("CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE", comment: ""), preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: NSLocalizedString("TXT_DELETE_TITLE", comment: ""), style: .destructive) { _ in
|
||||
guard let self = self else { return }
|
||||
self.editingDatabaseConnection.readWrite { transaction in
|
||||
thread.remove(with: transaction)
|
||||
}
|
||||
NotificationCenter.default.post(name: .threadDeleted, object: nil, userInfo: [ "threadId" : thread.uniqueId! ])
|
||||
})
|
||||
alert.addAction(UIAlertAction(title: NSLocalizedString("TXT_CANCEL_TITLE", comment: ""), style: .default) { _ in })
|
||||
guard let self = self else { return }
|
||||
self.present(alert, animated: true, completion: nil)
|
||||
}
|
||||
delete.backgroundColor = Colors.destructive
|
||||
if let publicChat = publicChat {
|
||||
return publicChat.isDeletable ? [ delete ] : []
|
||||
} else {
|
||||
return [ delete ]
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func openSettings() {
|
||||
let settingsVC = SettingsVC()
|
||||
let navigationController = OWSNavigationController(rootViewController: settingsVC)
|
||||
present(navigationController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@objc private func joinPublicChat() {
|
||||
let joinPublicChatVC = JoinPublicChatVC()
|
||||
let navigationController = OWSNavigationController(rootViewController: joinPublicChatVC)
|
||||
present(navigationController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@objc private func createPrivateGroupChat() {
|
||||
// TODO: Implement
|
||||
}
|
||||
|
||||
@objc func createPrivateChat() {
|
||||
let newPrivateChatVC = NewPrivateChatVC()
|
||||
let navigationController = OWSNavigationController(rootViewController: newPrivateChatVC)
|
||||
present(navigationController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
// MARK: Convenience
|
||||
private func thread(at index: Int) -> TSThread? {
|
||||
var thread: TSThread? = nil
|
||||
uiDatabaseConnection.read { transaction in
|
||||
thread = ((transaction as YapDatabaseReadTransaction).ext(TSThreadDatabaseViewExtensionName) as! YapDatabaseViewTransaction).object(atRow: UInt(index), inSection: 0, with: self.threads) as! TSThread?
|
||||
}
|
||||
return thread
|
||||
}
|
||||
|
||||
private func threadViewModel(at index: Int) -> ThreadViewModel? {
|
||||
guard let thread = thread(at: index) else { return nil }
|
||||
if let cachedThreadViewModel = threadViewModelCache[thread.uniqueId!] {
|
||||
return cachedThreadViewModel
|
||||
} else {
|
||||
var threadViewModel: ThreadViewModel? = nil
|
||||
uiDatabaseConnection.read { transaction in
|
||||
threadViewModel = ThreadViewModel(thread: thread, transaction: transaction)
|
||||
}
|
||||
threadViewModelCache[thread.uniqueId!] = threadViewModel
|
||||
return threadViewModel
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,299 @@
|
|||
|
||||
final class JoinPublicChatVC : UIViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate, OWSQRScannerDelegate {
|
||||
private let pageVC = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
|
||||
private var pages: [UIViewController] = []
|
||||
private var isJoining = false
|
||||
private var targetVCIndex: Int?
|
||||
|
||||
// MARK: Settings
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle { return .lightContent }
|
||||
|
||||
// MARK: Components
|
||||
private lazy var tabBar: TabBar = {
|
||||
let tabs = [
|
||||
TabBar.Tab(title: NSLocalizedString("Enter Chat URL", comment: "")) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.pageVC.setViewControllers([ self.pages[0] ], direction: .forward, animated: false, completion: nil)
|
||||
},
|
||||
TabBar.Tab(title: NSLocalizedString("Scan QR Code", comment: "")) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.pageVC.setViewControllers([ self.pages[1] ], direction: .forward, animated: false, completion: nil)
|
||||
}
|
||||
]
|
||||
return TabBar(tabs: tabs)
|
||||
}()
|
||||
|
||||
private lazy var enterChatURLVC: EnterChatURLVC = {
|
||||
let result = EnterChatURLVC()
|
||||
result.joinPublicChatVC = self
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var scanQRCodePlaceholderVC: ScanQRCodePlaceholderVC = {
|
||||
let result = ScanQRCodePlaceholderVC()
|
||||
result.joinPublicChatVC = self
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var scanQRCodeWrapperVC: ScanQRCodeWrapperVC = {
|
||||
let message = NSLocalizedString("Scan the QR code of the public chat you'd like to join", comment: "")
|
||||
let result = ScanQRCodeWrapperVC(message: message)
|
||||
result.delegate = self
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: Lifecycle
|
||||
override func viewDidLoad() {
|
||||
// Set gradient background
|
||||
view.backgroundColor = .clear
|
||||
let gradient = Gradients.defaultLokiBackground
|
||||
view.setGradient(gradient)
|
||||
// Set navigation bar background color
|
||||
let navigationBar = navigationController!.navigationBar
|
||||
navigationBar.setBackgroundImage(UIImage(), for: UIBarMetrics.default)
|
||||
navigationBar.shadowImage = UIImage()
|
||||
navigationBar.isTranslucent = false
|
||||
navigationBar.barTintColor = Colors.navigationBarBackground
|
||||
// Set up navigation bar buttons
|
||||
let closeButton = UIBarButtonItem(image: #imageLiteral(resourceName: "X"), style: .plain, target: self, action: #selector(close))
|
||||
closeButton.tintColor = Colors.text
|
||||
navigationItem.leftBarButtonItem = closeButton
|
||||
// Customize title
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.text = NSLocalizedString("Join Public Chat", comment: "")
|
||||
titleLabel.textColor = Colors.text
|
||||
titleLabel.font = .boldSystemFont(ofSize: Values.veryLargeFontSize)
|
||||
navigationItem.titleView = titleLabel
|
||||
// Set up page VC
|
||||
let hasCameraAccess = (AVCaptureDevice.authorizationStatus(for: .video) == .authorized)
|
||||
pages = [ enterChatURLVC, (hasCameraAccess ? scanQRCodeWrapperVC : scanQRCodePlaceholderVC) ]
|
||||
pageVC.dataSource = self
|
||||
pageVC.delegate = self
|
||||
pageVC.setViewControllers([ enterChatURLVC ], direction: .forward, animated: false, completion: nil)
|
||||
// Set up tab bar
|
||||
view.addSubview(tabBar)
|
||||
tabBar.pin(.leading, to: .leading, of: view)
|
||||
tabBar.pin(.top, to: .top, of: view, withInset: navigationBar.height())
|
||||
view.pin(.trailing, to: .trailing, of: tabBar)
|
||||
// Set up page VC constraints
|
||||
let pageVCView = pageVC.view!
|
||||
view.addSubview(pageVCView)
|
||||
pageVCView.pin(.leading, to: .leading, of: view)
|
||||
pageVCView.pin(.top, to: .bottom, of: tabBar)
|
||||
view.pin(.trailing, to: .trailing, of: pageVCView)
|
||||
view.pin(.bottom, to: .bottom, of: pageVCView)
|
||||
let screen = UIScreen.main.bounds
|
||||
pageVCView.set(.width, to: screen.width)
|
||||
let height = navigationController!.view.bounds.height - navigationBar.height() - Values.tabBarHeight
|
||||
pageVCView.set(.height, to: height)
|
||||
enterChatURLVC.constrainHeight(to: height)
|
||||
scanQRCodePlaceholderVC.constrainHeight(to: height)
|
||||
}
|
||||
|
||||
// MARK: General
|
||||
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
|
||||
guard let index = pages.firstIndex(of: viewController), index != 0 else { return nil }
|
||||
return pages[index - 1]
|
||||
}
|
||||
|
||||
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
|
||||
guard let index = pages.firstIndex(of: viewController), index != (pages.count - 1) else { return nil }
|
||||
return pages[index + 1]
|
||||
}
|
||||
|
||||
fileprivate func handleCameraAccessGranted() {
|
||||
pages[1] = scanQRCodeWrapperVC
|
||||
pageVC.setViewControllers([ scanQRCodeWrapperVC ], direction: .forward, animated: false, completion: nil)
|
||||
}
|
||||
|
||||
// MARK: Updating
|
||||
func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) {
|
||||
guard let targetVC = pendingViewControllers.first, let index = pages.firstIndex(of: targetVC) else { return }
|
||||
targetVCIndex = index
|
||||
}
|
||||
|
||||
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating isFinished: Bool, previousViewControllers: [UIViewController], transitionCompleted isCompleted: Bool) {
|
||||
guard isCompleted, let index = targetVCIndex else { return }
|
||||
tabBar.selectTab(at: index)
|
||||
}
|
||||
|
||||
// MARK: Interaction
|
||||
@objc private func close() {
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
func controller(_ controller: OWSQRCodeScanningViewController, didDetectQRCodeWith string: String) {
|
||||
let chatURL = string
|
||||
joinPublicChatIfPossible(with: chatURL)
|
||||
}
|
||||
|
||||
fileprivate func joinPublicChatIfPossible(with chatURL: String) {
|
||||
guard !isJoining else { return }
|
||||
guard let url = URL(string: chatURL), let scheme = url.scheme, scheme == "https", url.host != nil else {
|
||||
return showError(title: NSLocalizedString("Invalid URL", comment: ""), message: NSLocalizedString("Please check the URL you entered and try again", comment: ""))
|
||||
}
|
||||
isJoining = true
|
||||
let channelID: UInt64 = 1
|
||||
let urlAsString = url.absoluteString
|
||||
let displayName = OWSProfileManager.shared().localProfileName()
|
||||
// TODO: Profile picture & profile key
|
||||
LokiPublicChatManager.shared.addChat(server: urlAsString, channel: channelID)
|
||||
.done(on: .main) { [weak self] _ in
|
||||
let _ = LokiPublicChatAPI.getMessages(for: channelID, on: urlAsString)
|
||||
let _ = LokiPublicChatAPI.setDisplayName(to: displayName, on: urlAsString)
|
||||
self?.presentingViewController!.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
.catch(on: .main) { [weak self] _ in
|
||||
self?.isJoining = false
|
||||
self?.showError(title: NSLocalizedString("Couldn't Join", comment: ""))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Convenience
|
||||
private func showError(title: String, message: String = "") {
|
||||
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
|
||||
presentAlert(alert)
|
||||
}
|
||||
}
|
||||
|
||||
private final class EnterChatURLVC : UIViewController {
|
||||
weak var joinPublicChatVC: JoinPublicChatVC!
|
||||
private var bottomConstraint: NSLayoutConstraint!
|
||||
|
||||
// MARK: Components
|
||||
private lazy var chatURLTextField: TextField = {
|
||||
let result = TextField(placeholder: "https://chat.lokinet.org")
|
||||
result.keyboardType = .URL
|
||||
result.autocapitalizationType = .none
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: Lifecycle
|
||||
override func viewDidLoad() {
|
||||
// Remove background color
|
||||
view.backgroundColor = .clear
|
||||
// Set up explanation label
|
||||
let explanationLabel = UILabel()
|
||||
explanationLabel.textColor = Colors.text.withAlphaComponent(Values.unimportantElementOpacity)
|
||||
explanationLabel.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
explanationLabel.text = NSLocalizedString("Enter the URL of the public chat you'd like to join", comment: "")
|
||||
explanationLabel.numberOfLines = 0
|
||||
explanationLabel.textAlignment = .center
|
||||
explanationLabel.lineBreakMode = .byWordWrapping
|
||||
// Next button
|
||||
let nextButton = Button(style: .prominentOutline, size: .large)
|
||||
nextButton.setTitle(NSLocalizedString("Next", comment: ""), for: UIControl.State.normal)
|
||||
nextButton.addTarget(self, action: #selector(joinPublicChatIfPossible), for: UIControl.Event.touchUpInside)
|
||||
let nextButtonContainer = UIView()
|
||||
nextButtonContainer.addSubview(nextButton)
|
||||
nextButton.pin(.leading, to: .leading, of: nextButtonContainer, withInset: 80)
|
||||
nextButton.pin(.top, to: .top, of: nextButtonContainer)
|
||||
nextButtonContainer.pin(.trailing, to: .trailing, of: nextButton, withInset: 80)
|
||||
nextButtonContainer.pin(.bottom, to: .bottom, of: nextButton)
|
||||
// Set up stack view
|
||||
let stackView = UIStackView(arrangedSubviews: [ chatURLTextField, UIView.spacer(withHeight: Values.smallSpacing), explanationLabel, UIView.vStretchingSpacer(), nextButtonContainer ])
|
||||
stackView.axis = .vertical
|
||||
stackView.alignment = .fill
|
||||
stackView.layoutMargins = UIEdgeInsets(top: Values.mediumSpacing, left: Values.largeSpacing, bottom: Values.mediumSpacing, right: Values.largeSpacing)
|
||||
stackView.isLayoutMarginsRelativeArrangement = true
|
||||
view.addSubview(stackView)
|
||||
stackView.pin(.leading, to: .leading, of: view)
|
||||
stackView.pin(.top, to: .top, of: view)
|
||||
view.pin(.trailing, to: .trailing, of: stackView)
|
||||
bottomConstraint = view.pin(.bottom, to: .bottom, of: stackView)
|
||||
// Set up width constraint
|
||||
view.set(.width, to: UIScreen.main.bounds.width)
|
||||
// Dismiss keyboard on tap
|
||||
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
|
||||
view.addGestureRecognizer(tapGestureRecognizer)
|
||||
// Listen to keyboard notifications
|
||||
let notificationCenter = NotificationCenter.default
|
||||
notificationCenter.addObserver(self, selector: #selector(handleKeyboardWillChangeFrameNotification(_:)), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(handleKeyboardWillHideNotification(_:)), name: UIResponder.keyboardWillHideNotification, object: nil)
|
||||
}
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
// MARK: General
|
||||
func constrainHeight(to height: CGFloat) {
|
||||
view.set(.height, to: height)
|
||||
}
|
||||
|
||||
@objc private func dismissKeyboard() {
|
||||
chatURLTextField.resignFirstResponder()
|
||||
}
|
||||
|
||||
// MARK: Updating
|
||||
@objc private func handleKeyboardWillChangeFrameNotification(_ notification: Notification) {
|
||||
guard let newHeight = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue.size.height else { return }
|
||||
bottomConstraint.constant = newHeight
|
||||
UIView.animate(withDuration: 0.25) {
|
||||
self.view.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func handleKeyboardWillHideNotification(_ notification: Notification) {
|
||||
bottomConstraint.constant = 0
|
||||
UIView.animate(withDuration: 0.25) {
|
||||
self.view.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Interaction
|
||||
@objc private func joinPublicChatIfPossible() {
|
||||
let chatURL = chatURLTextField.text?.trimmingCharacters(in: .whitespaces) ?? ""
|
||||
joinPublicChatVC.joinPublicChatIfPossible(with: chatURL)
|
||||
}
|
||||
}
|
||||
|
||||
private final class ScanQRCodePlaceholderVC : UIViewController {
|
||||
weak var joinPublicChatVC: JoinPublicChatVC!
|
||||
|
||||
override func viewDidLoad() {
|
||||
// Remove background color
|
||||
view.backgroundColor = .clear
|
||||
// Set up explanation label
|
||||
let explanationLabel = UILabel()
|
||||
explanationLabel.textColor = Colors.text
|
||||
explanationLabel.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
explanationLabel.text = NSLocalizedString("Loki Messenger needs camera access to scan QR codes", comment: "")
|
||||
explanationLabel.numberOfLines = 0
|
||||
explanationLabel.textAlignment = .center
|
||||
explanationLabel.lineBreakMode = .byWordWrapping
|
||||
// Set up call to action button
|
||||
let callToActionButton = UIButton()
|
||||
callToActionButton.titleLabel!.font = .boldSystemFont(ofSize: Values.mediumFontSize)
|
||||
callToActionButton.setTitleColor(Colors.accent, for: UIControl.State.normal)
|
||||
callToActionButton.setTitle(NSLocalizedString("Enable Camera Access", comment: ""), for: UIControl.State.normal)
|
||||
callToActionButton.addTarget(self, action: #selector(requestCameraAccess), for: UIControl.Event.touchUpInside)
|
||||
// Set up stack view
|
||||
let stackView = UIStackView(arrangedSubviews: [ explanationLabel, callToActionButton ])
|
||||
stackView.axis = .vertical
|
||||
stackView.spacing = Values.mediumSpacing
|
||||
stackView.alignment = .center
|
||||
// Set up constraints
|
||||
view.set(.width, to: UIScreen.main.bounds.width)
|
||||
view.addSubview(stackView)
|
||||
stackView.pin(.leading, to: .leading, of: view, withInset: Values.massiveSpacing)
|
||||
view.pin(.trailing, to: .trailing, of: stackView, withInset: Values.massiveSpacing)
|
||||
let verticalCenteringConstraint = stackView.center(.vertical, in: view)
|
||||
verticalCenteringConstraint.constant = -16 // Makes things appear centered visually
|
||||
}
|
||||
|
||||
func constrainHeight(to height: CGFloat) {
|
||||
view.set(.height, to: height)
|
||||
}
|
||||
|
||||
@objc private func requestCameraAccess() {
|
||||
ows_ask(forCameraPermissions: { [weak self] hasCameraAccess in
|
||||
if hasCameraAccess {
|
||||
self?.joinPublicChatVC.handleCameraAccessGranted()
|
||||
} else {
|
||||
// Do nothing
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,213 @@
|
|||
|
||||
final class LandingVC : UIViewController, LinkDeviceVCDelegate, DeviceLinkingModalDelegate {
|
||||
private var fakeChatViewContentOffset: CGPoint!
|
||||
|
||||
// MARK: Components
|
||||
private lazy var fakeChatView: FakeChatView = {
|
||||
let result = FakeChatView()
|
||||
result.set(.height, to: Values.fakeChatViewHeight)
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var registerButton: Button = {
|
||||
let result = Button(style: .prominentFilled, size: .large)
|
||||
result.setTitle(NSLocalizedString("Create Account", comment: ""), for: UIControl.State.normal)
|
||||
result.titleLabel!.font = .boldSystemFont(ofSize: Values.mediumFontSize)
|
||||
result.addTarget(self, action: #selector(register), for: UIControl.Event.touchUpInside)
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var restoreButton: Button = {
|
||||
let result = Button(style: .prominentOutline, size: .large)
|
||||
result.setTitle(NSLocalizedString("Continue your Session", comment: ""), for: UIControl.State.normal)
|
||||
result.titleLabel!.font = .boldSystemFont(ofSize: Values.mediumFontSize)
|
||||
result.addTarget(self, action: #selector(restore), for: UIControl.Event.touchUpInside)
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var linkButton: Button = {
|
||||
let result = Button(style: .regularBorderless, size: .small)
|
||||
result.setTitle(NSLocalizedString("Link to an existing account", comment: ""), for: UIControl.State.normal)
|
||||
result.titleLabel!.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
result.addTarget(self, action: #selector(linkDevice), for: UIControl.Event.touchUpInside)
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: Settings
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle { return .lightContent }
|
||||
|
||||
// MARK: Lifecycle
|
||||
override func viewDidLoad() {
|
||||
// Set gradient background
|
||||
view.backgroundColor = .clear
|
||||
let gradient = Gradients.defaultLokiBackground
|
||||
view.setGradient(gradient)
|
||||
// Set up navigation bar
|
||||
let navigationBar = navigationController!.navigationBar
|
||||
navigationBar.setBackgroundImage(UIImage(), for: UIBarMetrics.default)
|
||||
navigationBar.shadowImage = UIImage()
|
||||
navigationBar.isTranslucent = false
|
||||
navigationBar.barTintColor = Colors.navigationBarBackground
|
||||
// Set up logo image view
|
||||
let logoImageView = UIImageView()
|
||||
logoImageView.image = #imageLiteral(resourceName: "Session")
|
||||
logoImageView.contentMode = .scaleAspectFit
|
||||
logoImageView.set(.width, to: 32)
|
||||
logoImageView.set(.height, to: 32)
|
||||
navigationItem.titleView = logoImageView
|
||||
// Set up title label
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.textColor = Colors.text
|
||||
titleLabel.font = .boldSystemFont(ofSize: Values.veryLargeFontSize)
|
||||
titleLabel.text = NSLocalizedString("Your Session begins here...", comment: "")
|
||||
titleLabel.numberOfLines = 0
|
||||
titleLabel.lineBreakMode = .byWordWrapping
|
||||
// Set up title label container
|
||||
let titleLabelContainer = UIView()
|
||||
titleLabelContainer.addSubview(titleLabel)
|
||||
titleLabel.pin(.leading, to: .leading, of: titleLabelContainer, withInset: Values.veryLargeSpacing)
|
||||
titleLabel.pin(.top, to: .top, of: titleLabelContainer)
|
||||
titleLabelContainer.pin(.trailing, to: .trailing, of: titleLabel, withInset: Values.veryLargeSpacing)
|
||||
titleLabelContainer.pin(.bottom, to: .bottom, of: titleLabel)
|
||||
// Set up spacers
|
||||
let topSpacer = UIView.vStretchingSpacer()
|
||||
let bottomSpacer = UIView.vStretchingSpacer()
|
||||
// Set up link button container
|
||||
let linkButtonContainer = UIView()
|
||||
linkButtonContainer.set(.height, to: Values.onboardingButtonBottomOffset)
|
||||
linkButtonContainer.addSubview(linkButton)
|
||||
linkButton.pin(.leading, to: .leading, of: linkButtonContainer, withInset: Values.massiveSpacing)
|
||||
linkButton.pin(.top, to: .top, of: linkButtonContainer)
|
||||
linkButtonContainer.pin(.trailing, to: .trailing, of: linkButton, withInset: Values.massiveSpacing)
|
||||
linkButtonContainer.pin(.bottom, to: .bottom, of: linkButton, withInset: 10)
|
||||
// Set up button stack view
|
||||
let buttonStackView = UIStackView(arrangedSubviews: [ registerButton, restoreButton ])
|
||||
buttonStackView.axis = .vertical
|
||||
buttonStackView.spacing = Values.mediumSpacing
|
||||
buttonStackView.alignment = .fill
|
||||
// Set up button stack view container
|
||||
let buttonStackViewContainer = UIView()
|
||||
buttonStackViewContainer.addSubview(buttonStackView)
|
||||
buttonStackView.pin(.leading, to: .leading, of: buttonStackViewContainer, withInset: Values.massiveSpacing)
|
||||
buttonStackView.pin(.top, to: .top, of: buttonStackViewContainer)
|
||||
buttonStackViewContainer.pin(.trailing, to: .trailing, of: buttonStackView, withInset: Values.massiveSpacing)
|
||||
buttonStackViewContainer.pin(.bottom, to: .bottom, of: buttonStackView)
|
||||
// Set up main stack view
|
||||
let mainStackView = UIStackView(arrangedSubviews: [ topSpacer, titleLabelContainer, UIView.spacer(withHeight: Values.mediumSpacing), fakeChatView, bottomSpacer, buttonStackViewContainer, linkButtonContainer ])
|
||||
mainStackView.axis = .vertical
|
||||
mainStackView.alignment = .fill
|
||||
view.addSubview(mainStackView)
|
||||
mainStackView.pin(to: view)
|
||||
topSpacer.heightAnchor.constraint(equalTo: bottomSpacer.heightAnchor, multiplier: 1).isActive = true
|
||||
// Show device unlinked alert if needed
|
||||
if UserDefaults.standard.bool(forKey: "wasUnlinked") {
|
||||
let alert = UIAlertController(title: NSLocalizedString("Device Unlinked", comment: ""), message: NSLocalizedString("Your device was unlinked successfully", comment: ""), preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), accessibilityIdentifier: nil, style: .default, handler: nil))
|
||||
present(alert, animated: true, completion: nil)
|
||||
UserDefaults.removeAll()
|
||||
}
|
||||
}
|
||||
|
||||
override func viewDidDisappear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
fakeChatView.contentOffset = fakeChatViewContentOffset
|
||||
}
|
||||
|
||||
// MARK: Interaction
|
||||
@objc private func register() {
|
||||
fakeChatViewContentOffset = fakeChatView.contentOffset
|
||||
DispatchQueue.main.async {
|
||||
self.fakeChatView.contentOffset = self.fakeChatViewContentOffset
|
||||
}
|
||||
let registerVC = RegisterVC()
|
||||
navigationController!.pushViewController(registerVC, animated: true)
|
||||
}
|
||||
|
||||
@objc private func restore() {
|
||||
fakeChatViewContentOffset = fakeChatView.contentOffset
|
||||
DispatchQueue.main.async {
|
||||
self.fakeChatView.contentOffset = self.fakeChatViewContentOffset
|
||||
}
|
||||
let restoreVC = RestoreVC()
|
||||
navigationController!.pushViewController(restoreVC, animated: true)
|
||||
}
|
||||
|
||||
@objc private func linkDevice() {
|
||||
let linkDeviceVC = LinkDeviceVC()
|
||||
linkDeviceVC.delegate = self
|
||||
let navigationController = OWSNavigationController(rootViewController: linkDeviceVC)
|
||||
present(navigationController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
// MARK: Device Linking
|
||||
func requestDeviceLink(with hexEncodedPublicKey: String) {
|
||||
guard ECKeyPair.isValidHexEncodedPublicKey(candidate: hexEncodedPublicKey) else {
|
||||
let alert = UIAlertController(title: NSLocalizedString("Invalid Public Key", comment: ""), message: NSLocalizedString("Please make sure the public key you entered is correct and try again.", comment: ""), preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), accessibilityIdentifier: nil, style: .default, handler: nil))
|
||||
return present(alert, animated: true, completion: nil)
|
||||
}
|
||||
let seed = Randomness.generateRandomBytes(16)!
|
||||
let keyPair = Curve25519.generateKeyPair(fromSeed: seed + seed)
|
||||
let identityManager = OWSIdentityManager.shared()
|
||||
let databaseConnection = identityManager.value(forKey: "dbConnection") as! YapDatabaseConnection
|
||||
databaseConnection.setObject(seed.toHexString(), forKey: "LKLokiSeed", inCollection: OWSPrimaryStorageIdentityKeyStoreCollection)
|
||||
databaseConnection.setObject(keyPair, forKey: OWSPrimaryStorageIdentityKeyStoreIdentityKey, inCollection: OWSPrimaryStorageIdentityKeyStoreCollection)
|
||||
TSAccountManager.sharedInstance().phoneNumberAwaitingVerification = keyPair.hexEncodedPublicKey
|
||||
TSAccountManager.sharedInstance().didRegister()
|
||||
setUserInteractionEnabled(false)
|
||||
let _ = LokiStorageAPI.getDeviceLinks(associatedWith: hexEncodedPublicKey).done(on: DispatchQueue.main) { [weak self] deviceLinks in
|
||||
guard let self = self else { return }
|
||||
defer { self.setUserInteractionEnabled(true) }
|
||||
guard deviceLinks.count < 2 else {
|
||||
let alert = UIAlertController(title: "Multi Device Limit Reached", message: "It's currently not allowed to link more than one device.", preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: "OK", accessibilityIdentifier: nil, style: .default, handler: nil))
|
||||
return self.present(alert, animated: true, completion: nil)
|
||||
}
|
||||
let appDelegate = UIApplication.shared.delegate as! AppDelegate
|
||||
appDelegate.startLongPollerIfNeeded()
|
||||
let deviceLinkingModal = DeviceLinkingModal(mode: .slave, delegate: self)
|
||||
deviceLinkingModal.modalPresentationStyle = .overFullScreen
|
||||
deviceLinkingModal.modalTransitionStyle = .crossDissolve
|
||||
self.present(deviceLinkingModal, animated: true, completion: nil)
|
||||
let linkingRequestMessage = DeviceLinkingUtilities.getLinkingRequestMessage(for: hexEncodedPublicKey)
|
||||
ThreadUtil.enqueue(linkingRequestMessage)
|
||||
}.catch(on: DispatchQueue.main) { [weak self] _ in
|
||||
let appDelegate = UIApplication.shared.delegate as! AppDelegate
|
||||
appDelegate.stopLongPollerIfNeeded()
|
||||
DispatchQueue.main.async {
|
||||
// FIXME: For some reason resetForRegistration() complains about not being on the main queue
|
||||
// without this (even though the catch closure should be executed on the main queue)
|
||||
TSAccountManager.sharedInstance().resetForReregistration()
|
||||
}
|
||||
guard let self = self else { return }
|
||||
let alert = UIAlertController(title: NSLocalizedString("Couldn't Link Device", comment: ""), message: NSLocalizedString("Please check your internet connection and try again", comment: ""), preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: "OK", accessibilityIdentifier: nil, style: .default, handler: nil))
|
||||
self.present(alert, animated: true, completion: nil)
|
||||
self.setUserInteractionEnabled(true)
|
||||
}
|
||||
}
|
||||
|
||||
func handleDeviceLinkAuthorized(_ deviceLink: DeviceLink) {
|
||||
let userDefaults = UserDefaults.standard
|
||||
userDefaults.set(deviceLink.master.hexEncodedPublicKey, forKey: "masterDeviceHexEncodedPublicKey")
|
||||
fakeChatViewContentOffset = fakeChatView.contentOffset
|
||||
DispatchQueue.main.async {
|
||||
self.fakeChatView.contentOffset = self.fakeChatViewContentOffset
|
||||
}
|
||||
let homeVC = HomeVC()
|
||||
navigationController!.setViewControllers([ homeVC ], animated: true)
|
||||
}
|
||||
|
||||
func handleDeviceLinkingModalDismissed() {
|
||||
let appDelegate = UIApplication.shared.delegate as! AppDelegate
|
||||
appDelegate.stopLongPollerIfNeeded()
|
||||
TSAccountManager.sharedInstance().resetForReregistration()
|
||||
}
|
||||
|
||||
// MARK: Convenience
|
||||
private func setUserInteractionEnabled(_ isEnabled: Bool) {
|
||||
[ registerButton, restoreButton, linkButton ].forEach {
|
||||
$0.isUserInteractionEnabled = isEnabled
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,287 @@
|
|||
|
||||
final class LinkDeviceVC : UIViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate, OWSQRScannerDelegate {
|
||||
private let pageVC = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
|
||||
private var pages: [UIViewController] = []
|
||||
private var targetVCIndex: Int?
|
||||
var delegate: LinkDeviceVCDelegate?
|
||||
|
||||
// MARK: Settings
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle { return .lightContent }
|
||||
|
||||
// MARK: Components
|
||||
private lazy var tabBar: TabBar = {
|
||||
let tabs = [
|
||||
TabBar.Tab(title: NSLocalizedString("Enter Public Key", comment: "")) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.pageVC.setViewControllers([ self.pages[0] ], direction: .forward, animated: false, completion: nil)
|
||||
},
|
||||
TabBar.Tab(title: NSLocalizedString("Scan QR Code", comment: "")) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.pageVC.setViewControllers([ self.pages[1] ], direction: .forward, animated: false, completion: nil)
|
||||
}
|
||||
]
|
||||
return TabBar(tabs: tabs)
|
||||
}()
|
||||
|
||||
private lazy var enterPublicKeyVC: EnterPublicKeyVC = {
|
||||
let result = EnterPublicKeyVC()
|
||||
result.linkDeviceVC = self
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var scanQRCodePlaceholderVC: ScanQRCodePlaceholderVC = {
|
||||
let result = ScanQRCodePlaceholderVC()
|
||||
result.linkDeviceVC = self
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var scanQRCodeWrapperVC: ScanQRCodeWrapperVC = {
|
||||
let message = NSLocalizedString("Link to your existing account by going into your in-app settings and clicking \"Linked Devices\".", comment: "")
|
||||
let result = ScanQRCodeWrapperVC(message: message)
|
||||
result.delegate = self
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: Lifecycle
|
||||
override func viewDidLoad() {
|
||||
// Set gradient background
|
||||
view.backgroundColor = .clear
|
||||
let gradient = Gradients.defaultLokiBackground
|
||||
view.setGradient(gradient)
|
||||
// Set navigation bar background color
|
||||
let navigationBar = navigationController!.navigationBar
|
||||
navigationBar.setBackgroundImage(UIImage(), for: UIBarMetrics.default)
|
||||
navigationBar.shadowImage = UIImage()
|
||||
navigationBar.isTranslucent = false
|
||||
navigationBar.barTintColor = Colors.navigationBarBackground
|
||||
// Set up navigation bar buttons
|
||||
let closeButton = UIBarButtonItem(image: #imageLiteral(resourceName: "X"), style: .plain, target: self, action: #selector(close))
|
||||
closeButton.tintColor = Colors.text
|
||||
navigationItem.leftBarButtonItem = closeButton
|
||||
// Customize title
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.text = NSLocalizedString("Link Device", comment: "")
|
||||
titleLabel.textColor = Colors.text
|
||||
titleLabel.font = .boldSystemFont(ofSize: Values.veryLargeFontSize)
|
||||
navigationItem.titleView = titleLabel
|
||||
// Set up page VC
|
||||
let hasCameraAccess = (AVCaptureDevice.authorizationStatus(for: .video) == .authorized)
|
||||
pages = [ enterPublicKeyVC, (hasCameraAccess ? scanQRCodeWrapperVC : scanQRCodePlaceholderVC) ]
|
||||
pageVC.dataSource = self
|
||||
pageVC.delegate = self
|
||||
pageVC.setViewControllers([ enterPublicKeyVC ], direction: .forward, animated: false, completion: nil)
|
||||
// Set up tab bar
|
||||
view.addSubview(tabBar)
|
||||
tabBar.pin(.leading, to: .leading, of: view)
|
||||
tabBar.pin(.top, to: .top, of: view, withInset: navigationBar.height())
|
||||
view.pin(.trailing, to: .trailing, of: tabBar)
|
||||
// Set up page VC constraints
|
||||
let pageVCView = pageVC.view!
|
||||
view.addSubview(pageVCView)
|
||||
pageVCView.pin(.leading, to: .leading, of: view)
|
||||
pageVCView.pin(.top, to: .bottom, of: tabBar)
|
||||
view.pin(.trailing, to: .trailing, of: pageVCView)
|
||||
view.pin(.bottom, to: .bottom, of: pageVCView)
|
||||
let screen = UIScreen.main.bounds
|
||||
pageVCView.set(.width, to: screen.width)
|
||||
let height = navigationController!.view.bounds.height - navigationBar.height() - Values.tabBarHeight
|
||||
pageVCView.set(.height, to: height)
|
||||
enterPublicKeyVC.constrainHeight(to: height)
|
||||
scanQRCodePlaceholderVC.constrainHeight(to: height)
|
||||
}
|
||||
|
||||
// MARK: General
|
||||
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
|
||||
guard let index = pages.firstIndex(of: viewController), index != 0 else { return nil }
|
||||
return pages[index - 1]
|
||||
}
|
||||
|
||||
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
|
||||
guard let index = pages.firstIndex(of: viewController), index != (pages.count - 1) else { return nil }
|
||||
return pages[index + 1]
|
||||
}
|
||||
|
||||
fileprivate func handleCameraAccessGranted() {
|
||||
pages[1] = scanQRCodeWrapperVC
|
||||
pageVC.setViewControllers([ scanQRCodeWrapperVC ], direction: .forward, animated: false, completion: nil)
|
||||
}
|
||||
|
||||
// MARK: Updating
|
||||
func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) {
|
||||
guard let targetVC = pendingViewControllers.first, let index = pages.firstIndex(of: targetVC) else { return }
|
||||
targetVCIndex = index
|
||||
}
|
||||
|
||||
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating isFinished: Bool, previousViewControllers: [UIViewController], transitionCompleted isCompleted: Bool) {
|
||||
guard isCompleted, let index = targetVCIndex else { return }
|
||||
tabBar.selectTab(at: index)
|
||||
}
|
||||
|
||||
// MARK: Interaction
|
||||
@objc private func close() {
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
func controller(_ controller: OWSQRCodeScanningViewController, didDetectQRCodeWith string: String) {
|
||||
let hexEncodedPublicKey = string
|
||||
requestDeviceLink(with: hexEncodedPublicKey)
|
||||
}
|
||||
|
||||
fileprivate func requestDeviceLink(with hexEncodedPublicKey: String) {
|
||||
delegate?.requestDeviceLink(with: hexEncodedPublicKey)
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
private final class EnterPublicKeyVC : UIViewController {
|
||||
weak var linkDeviceVC: LinkDeviceVC!
|
||||
private var bottomConstraint: NSLayoutConstraint!
|
||||
private var linkButtonBottomConstraint: NSLayoutConstraint!
|
||||
|
||||
// MARK: Components
|
||||
private lazy var publicKeyTextField = TextField(placeholder: NSLocalizedString("Enter your public key", comment: ""))
|
||||
|
||||
// MARK: Lifecycle
|
||||
override func viewDidLoad() {
|
||||
// Remove background color
|
||||
view.backgroundColor = .clear
|
||||
// Set up title label
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.textColor = Colors.text
|
||||
titleLabel.font = .boldSystemFont(ofSize: Values.veryLargeFontSize)
|
||||
titleLabel.text = NSLocalizedString("Enter your public key", comment: "")
|
||||
titleLabel.numberOfLines = 0
|
||||
titleLabel.lineBreakMode = .byWordWrapping
|
||||
// Set up explanation label
|
||||
let explanationLabel = UILabel()
|
||||
explanationLabel.textColor = Colors.text
|
||||
explanationLabel.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
explanationLabel.text = "Enter your account's public key to link your device."
|
||||
explanationLabel.numberOfLines = 0
|
||||
explanationLabel.lineBreakMode = .byWordWrapping
|
||||
// Link button
|
||||
let linkButton = Button(style: .prominentOutline, size: .large)
|
||||
linkButton.setTitle(NSLocalizedString("Continue", comment: ""), for: UIControl.State.normal)
|
||||
linkButton.addTarget(self, action: #selector(requestDeviceLink), for: UIControl.Event.touchUpInside)
|
||||
let linkButtonContainer = UIView()
|
||||
linkButtonContainer.addSubview(linkButton)
|
||||
linkButton.pin(.leading, to: .leading, of: linkButtonContainer, withInset: 80)
|
||||
linkButton.pin(.top, to: .top, of: linkButtonContainer)
|
||||
linkButtonContainer.pin(.trailing, to: .trailing, of: linkButton, withInset: 80)
|
||||
linkButtonBottomConstraint = linkButtonContainer.pin(.bottom, to: .bottom, of: linkButton, withInset: Values.veryLargeSpacing)
|
||||
// Set up top stack view
|
||||
let topStackView = UIStackView(arrangedSubviews: [ titleLabel, explanationLabel, publicKeyTextField ])
|
||||
topStackView.axis = .vertical
|
||||
topStackView.spacing = Values.largeSpacing
|
||||
// Set up spacers
|
||||
let topSpacer = UIView.vStretchingSpacer()
|
||||
let bottomSpacer = UIView.vStretchingSpacer()
|
||||
// Set up stack view
|
||||
let stackView = UIStackView(arrangedSubviews: [ topSpacer, topStackView, bottomSpacer, linkButtonContainer ])
|
||||
stackView.axis = .vertical
|
||||
stackView.alignment = .fill
|
||||
stackView.layoutMargins = UIEdgeInsets(top: 0, left: Values.veryLargeSpacing, bottom: 0, right: Values.veryLargeSpacing)
|
||||
stackView.isLayoutMarginsRelativeArrangement = true
|
||||
view.addSubview(stackView)
|
||||
stackView.pin(.leading, to: .leading, of: view)
|
||||
stackView.pin(.top, to: .top, of: view)
|
||||
stackView.pin(.trailing, to: .trailing, of: view)
|
||||
bottomConstraint = stackView.pin(.bottom, to: .bottom, of: view)
|
||||
topSpacer.heightAnchor.constraint(equalTo: bottomSpacer.heightAnchor, multiplier: 1).isActive = true
|
||||
// Set up width constraint
|
||||
view.set(.width, to: UIScreen.main.bounds.width)
|
||||
// Dismiss keyboard on tap
|
||||
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
|
||||
view.addGestureRecognizer(tapGestureRecognizer)
|
||||
// Listen to keyboard notifications
|
||||
let notificationCenter = NotificationCenter.default
|
||||
notificationCenter.addObserver(self, selector: #selector(handleKeyboardWillChangeFrameNotification(_:)), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(handleKeyboardWillHideNotification(_:)), name: UIResponder.keyboardWillHideNotification, object: nil)
|
||||
}
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
// MARK: General
|
||||
func constrainHeight(to height: CGFloat) {
|
||||
view.set(.height, to: height)
|
||||
}
|
||||
|
||||
@objc private func dismissKeyboard() {
|
||||
publicKeyTextField.resignFirstResponder()
|
||||
}
|
||||
|
||||
// MARK: Updating
|
||||
@objc private func handleKeyboardWillChangeFrameNotification(_ notification: Notification) {
|
||||
guard let newHeight = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue.size.height else { return }
|
||||
bottomConstraint.constant = -newHeight
|
||||
linkButtonBottomConstraint.constant = Values.mediumSpacing
|
||||
UIView.animate(withDuration: 0.25) {
|
||||
self.view.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func handleKeyboardWillHideNotification(_ notification: Notification) {
|
||||
bottomConstraint.constant = 0
|
||||
linkButtonBottomConstraint.constant = Values.veryLargeSpacing
|
||||
UIView.animate(withDuration: 0.25) {
|
||||
self.view.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Interaction
|
||||
@objc private func requestDeviceLink() {
|
||||
let hexEncodedPublicKey = publicKeyTextField.text?.trimmingCharacters(in: .whitespaces) ?? ""
|
||||
linkDeviceVC.requestDeviceLink(with: hexEncodedPublicKey)
|
||||
}
|
||||
}
|
||||
|
||||
private final class ScanQRCodePlaceholderVC : UIViewController {
|
||||
weak var linkDeviceVC: LinkDeviceVC!
|
||||
|
||||
override func viewDidLoad() {
|
||||
// Remove background color
|
||||
view.backgroundColor = .clear
|
||||
// Set up explanation label
|
||||
let explanationLabel = UILabel()
|
||||
explanationLabel.textColor = Colors.text
|
||||
explanationLabel.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
explanationLabel.text = NSLocalizedString("Loki Messenger needs camera access to scan QR codes", comment: "")
|
||||
explanationLabel.numberOfLines = 0
|
||||
explanationLabel.textAlignment = .center
|
||||
explanationLabel.lineBreakMode = .byWordWrapping
|
||||
// Set up call to action button
|
||||
let callToActionButton = UIButton()
|
||||
callToActionButton.titleLabel!.font = .boldSystemFont(ofSize: Values.mediumFontSize)
|
||||
callToActionButton.setTitleColor(Colors.accent, for: UIControl.State.normal)
|
||||
callToActionButton.setTitle(NSLocalizedString("Enable Camera Access", comment: ""), for: UIControl.State.normal)
|
||||
callToActionButton.addTarget(self, action: #selector(requestCameraAccess), for: UIControl.Event.touchUpInside)
|
||||
// Set up stack view
|
||||
let stackView = UIStackView(arrangedSubviews: [ explanationLabel, callToActionButton ])
|
||||
stackView.axis = .vertical
|
||||
stackView.spacing = Values.mediumSpacing
|
||||
stackView.alignment = .center
|
||||
// Set up constraints
|
||||
view.set(.width, to: UIScreen.main.bounds.width)
|
||||
view.addSubview(stackView)
|
||||
stackView.pin(.leading, to: .leading, of: view, withInset: Values.massiveSpacing)
|
||||
view.pin(.trailing, to: .trailing, of: stackView, withInset: Values.massiveSpacing)
|
||||
let verticalCenteringConstraint = stackView.center(.vertical, in: view)
|
||||
verticalCenteringConstraint.constant = -16 // Makes things appear centered visually
|
||||
}
|
||||
|
||||
func constrainHeight(to height: CGFloat) {
|
||||
view.set(.height, to: height)
|
||||
}
|
||||
|
||||
@objc private func requestCameraAccess() {
|
||||
ows_ask(forCameraPermissions: { [weak self] hasCameraAccess in
|
||||
if hasCameraAccess {
|
||||
self?.linkDeviceVC.handleCameraAccessGranted()
|
||||
} else {
|
||||
// Do nothing
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
|
||||
protocol LinkDeviceVCDelegate {
|
||||
|
||||
func requestDeviceLink(with hexEncodedPublicKey: String)
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
|
||||
@objc(LKModal)
|
||||
internal class Modal : UIViewController {
|
||||
private(set) var verticalCenteringConstraint: NSLayoutConstraint!
|
||||
|
||||
// MARK: Settings
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle { return .lightContent }
|
||||
|
||||
// MARK: Components
|
||||
lazy var contentView: UIView = {
|
||||
let result = UIView()
|
||||
result.backgroundColor = Colors.modalBackground
|
||||
result.layer.cornerRadius = Values.modalCornerRadius
|
||||
result.layer.masksToBounds = false
|
||||
result.layer.borderColor = Colors.modalBorder.cgColor
|
||||
result.layer.borderWidth = Values.borderThickness
|
||||
result.layer.shadowColor = UIColor.black.cgColor
|
||||
result.layer.shadowRadius = 8
|
||||
result.layer.shadowOpacity = 0.64
|
||||
return result
|
||||
}()
|
||||
|
||||
lazy var cancelButton: UIButton = {
|
||||
let result = UIButton()
|
||||
result.set(.height, to: Values.mediumButtonHeight)
|
||||
result.layer.cornerRadius = Values.modalButtonCornerRadius
|
||||
result.backgroundColor = Colors.buttonBackground
|
||||
result.titleLabel!.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
result.setTitleColor(Colors.text, for: UIControl.State.normal)
|
||||
result.setTitle(NSLocalizedString("Cancel", comment: ""), for: UIControl.State.normal)
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: Lifecycle
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.backgroundColor = UIColor(hex: 0x000000).withAlphaComponent(Values.modalBackgroundOpacity)
|
||||
cancelButton.addTarget(self, action: #selector(close), for: UIControl.Event.touchUpInside)
|
||||
let swipeGestureRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(close))
|
||||
swipeGestureRecognizer.direction = .down
|
||||
view.addGestureRecognizer(swipeGestureRecognizer)
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
private func setUpViewHierarchy() {
|
||||
view.addSubview(contentView)
|
||||
contentView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: Values.veryLargeSpacing).isActive = true
|
||||
view.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: Values.veryLargeSpacing).isActive = true
|
||||
verticalCenteringConstraint = contentView.center(.vertical, in: view)
|
||||
populateContentView()
|
||||
}
|
||||
|
||||
/// To be overridden by subclasses.
|
||||
func populateContentView() {
|
||||
preconditionFailure("populateContentView() is abstract and must be overridden.")
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
verticalCenteringConstraint.constant = contentView.height() / 2 + view.height() / 2
|
||||
view.layoutIfNeeded()
|
||||
verticalCenteringConstraint.constant = 0
|
||||
UIView.animate(withDuration: 0.25) {
|
||||
self.view.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Interaction
|
||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
let touch = touches.first!
|
||||
let location = touch.location(in: view)
|
||||
if contentView.frame.contains(location) {
|
||||
super.touchesBegan(touches, with: event)
|
||||
} else {
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func close() {
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,303 @@
|
|||
|
||||
final class NewPrivateChatVC : UIViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate, OWSQRScannerDelegate {
|
||||
private let pageVC = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
|
||||
private var pages: [UIViewController] = []
|
||||
private var targetVCIndex: Int?
|
||||
|
||||
// MARK: Settings
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle { return .lightContent }
|
||||
|
||||
// MARK: Components
|
||||
private lazy var tabBar: TabBar = {
|
||||
let tabs = [
|
||||
TabBar.Tab(title: NSLocalizedString("Enter Public Key", comment: "")) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.pageVC.setViewControllers([ self.pages[0] ], direction: .forward, animated: false, completion: nil)
|
||||
},
|
||||
TabBar.Tab(title: NSLocalizedString("Scan QR Code", comment: "")) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.pageVC.setViewControllers([ self.pages[1] ], direction: .forward, animated: false, completion: nil)
|
||||
}
|
||||
]
|
||||
return TabBar(tabs: tabs)
|
||||
}()
|
||||
|
||||
private lazy var enterPublicKeyVC: EnterPublicKeyVC = {
|
||||
let result = EnterPublicKeyVC()
|
||||
result.newPrivateChatVC = self
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var scanQRCodePlaceholderVC: ScanQRCodePlaceholderVC = {
|
||||
let result = ScanQRCodePlaceholderVC()
|
||||
result.newPrivateChatVC = self
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var scanQRCodeWrapperVC: ScanQRCodeWrapperVC = {
|
||||
let message = NSLocalizedString("Users can share their QR code by going into their account settings and tapping \"Share QR Code\".", comment: "")
|
||||
let result = ScanQRCodeWrapperVC(message: message)
|
||||
result.delegate = self
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: Lifecycle
|
||||
override func viewDidLoad() {
|
||||
// Set gradient background
|
||||
view.backgroundColor = .clear
|
||||
let gradient = Gradients.defaultLokiBackground
|
||||
view.setGradient(gradient)
|
||||
// Set navigation bar background color
|
||||
let navigationBar = navigationController!.navigationBar
|
||||
navigationBar.setBackgroundImage(UIImage(), for: UIBarMetrics.default)
|
||||
navigationBar.shadowImage = UIImage()
|
||||
navigationBar.isTranslucent = false
|
||||
navigationBar.barTintColor = Colors.navigationBarBackground
|
||||
// Set up navigation bar buttons
|
||||
let closeButton = UIBarButtonItem(image: #imageLiteral(resourceName: "X"), style: .plain, target: self, action: #selector(close))
|
||||
closeButton.tintColor = Colors.text
|
||||
navigationItem.leftBarButtonItem = closeButton
|
||||
// Customize title
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.text = NSLocalizedString("New Conversation", comment: "")
|
||||
titleLabel.textColor = Colors.text
|
||||
titleLabel.font = .boldSystemFont(ofSize: Values.veryLargeFontSize)
|
||||
navigationItem.titleView = titleLabel
|
||||
// Set up page VC
|
||||
let hasCameraAccess = (AVCaptureDevice.authorizationStatus(for: .video) == .authorized)
|
||||
pages = [ enterPublicKeyVC, (hasCameraAccess ? scanQRCodeWrapperVC : scanQRCodePlaceholderVC) ]
|
||||
pageVC.dataSource = self
|
||||
pageVC.delegate = self
|
||||
pageVC.setViewControllers([ enterPublicKeyVC ], direction: .forward, animated: false, completion: nil)
|
||||
// Set up tab bar
|
||||
view.addSubview(tabBar)
|
||||
tabBar.pin(.leading, to: .leading, of: view)
|
||||
tabBar.pin(.top, to: .top, of: view, withInset: navigationBar.height())
|
||||
view.pin(.trailing, to: .trailing, of: tabBar)
|
||||
// Set up page VC constraints
|
||||
let pageVCView = pageVC.view!
|
||||
view.addSubview(pageVCView)
|
||||
pageVCView.pin(.leading, to: .leading, of: view)
|
||||
pageVCView.pin(.top, to: .bottom, of: tabBar)
|
||||
view.pin(.trailing, to: .trailing, of: pageVCView)
|
||||
view.pin(.bottom, to: .bottom, of: pageVCView)
|
||||
let screen = UIScreen.main.bounds
|
||||
pageVCView.set(.width, to: screen.width)
|
||||
let height = navigationController!.view.bounds.height - navigationBar.height() - Values.tabBarHeight
|
||||
pageVCView.set(.height, to: height)
|
||||
enterPublicKeyVC.constrainHeight(to: height)
|
||||
scanQRCodePlaceholderVC.constrainHeight(to: height)
|
||||
}
|
||||
|
||||
// MARK: General
|
||||
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
|
||||
guard let index = pages.firstIndex(of: viewController), index != 0 else { return nil }
|
||||
return pages[index - 1]
|
||||
}
|
||||
|
||||
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
|
||||
guard let index = pages.firstIndex(of: viewController), index != (pages.count - 1) else { return nil }
|
||||
return pages[index + 1]
|
||||
}
|
||||
|
||||
fileprivate func handleCameraAccessGranted() {
|
||||
pages[1] = scanQRCodeWrapperVC
|
||||
pageVC.setViewControllers([ scanQRCodeWrapperVC ], direction: .forward, animated: false, completion: nil)
|
||||
}
|
||||
|
||||
// MARK: Updating
|
||||
func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) {
|
||||
guard let targetVC = pendingViewControllers.first, let index = pages.firstIndex(of: targetVC) else { return }
|
||||
targetVCIndex = index
|
||||
}
|
||||
|
||||
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating isFinished: Bool, previousViewControllers: [UIViewController], transitionCompleted isCompleted: Bool) {
|
||||
guard isCompleted, let index = targetVCIndex else { return }
|
||||
tabBar.selectTab(at: index)
|
||||
}
|
||||
|
||||
// MARK: Interaction
|
||||
@objc private func close() {
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
func controller(_ controller: OWSQRCodeScanningViewController, didDetectQRCodeWith string: String) {
|
||||
let hexEncodedPublicKey = string
|
||||
startNewPrivateChatIfPossible(with: hexEncodedPublicKey)
|
||||
}
|
||||
|
||||
fileprivate func startNewPrivateChatIfPossible(with hexEncodedPublicKey: String) {
|
||||
if !ECKeyPair.isValidHexEncodedPublicKey(candidate: hexEncodedPublicKey) {
|
||||
let alert = UIAlertController(title: NSLocalizedString("Invalid Public Key", comment: ""), message: NSLocalizedString("Please check the public key you entered and try again.", comment: ""), preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
|
||||
presentAlert(alert)
|
||||
} else {
|
||||
let thread = TSContactThread.getOrCreateThread(contactId: hexEncodedPublicKey)
|
||||
presentingViewController?.dismiss(animated: true, completion: nil)
|
||||
SignalApp.shared().presentConversation(for: thread, action: .compose, animated: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class EnterPublicKeyVC : UIViewController {
|
||||
weak var newPrivateChatVC: NewPrivateChatVC!
|
||||
|
||||
private lazy var userHexEncodedPublicKey: String = {
|
||||
if let masterHexEncodedPublicKey = UserDefaults.standard.string(forKey: "masterDeviceHexEncodedPublicKey") {
|
||||
return masterHexEncodedPublicKey
|
||||
} else {
|
||||
return OWSIdentityManager.shared().identityKeyPair()!.hexEncodedPublicKey
|
||||
}
|
||||
}()
|
||||
|
||||
// MARK: Components
|
||||
private lazy var publicKeyTextField = TextField(placeholder: NSLocalizedString("Enter public key of recipient", comment: ""))
|
||||
|
||||
private lazy var copyButton: Button = {
|
||||
let result = Button(style: .unimportant, size: .medium)
|
||||
result.setTitle(NSLocalizedString("Copy", comment: ""), for: UIControl.State.normal)
|
||||
result.addTarget(self, action: #selector(copyPublicKey), for: UIControl.Event.touchUpInside)
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: Lifecycle
|
||||
override func viewDidLoad() {
|
||||
// Remove background color
|
||||
view.backgroundColor = .clear
|
||||
// Set up explanation label
|
||||
let explanationLabel = UILabel()
|
||||
explanationLabel.textColor = Colors.text.withAlphaComponent(Values.unimportantElementOpacity)
|
||||
explanationLabel.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
explanationLabel.text = NSLocalizedString("Users can share their public key by going into their account settings and tapping \"Share Public Key\", or by sharing their QR code.", comment: "")
|
||||
explanationLabel.numberOfLines = 0
|
||||
explanationLabel.textAlignment = .center
|
||||
explanationLabel.lineBreakMode = .byWordWrapping
|
||||
// Set up separator
|
||||
let separator = Separator(title: NSLocalizedString("Your Public Key", comment: ""))
|
||||
// Set up user public key label
|
||||
let userPublicKeyLabel = UILabel()
|
||||
userPublicKeyLabel.textColor = Colors.text
|
||||
userPublicKeyLabel.font = Fonts.spaceMono(ofSize: Values.mediumFontSize)
|
||||
userPublicKeyLabel.numberOfLines = 0
|
||||
userPublicKeyLabel.textAlignment = .center
|
||||
userPublicKeyLabel.lineBreakMode = .byCharWrapping
|
||||
userPublicKeyLabel.text = userHexEncodedPublicKey
|
||||
// Set up share button
|
||||
let shareButton = Button(style: .unimportant, size: .medium)
|
||||
shareButton.setTitle(NSLocalizedString("Share", comment: ""), for: UIControl.State.normal)
|
||||
shareButton.addTarget(self, action: #selector(sharePublicKey), for: UIControl.Event.touchUpInside)
|
||||
// Set up button container
|
||||
let buttonContainer = UIStackView(arrangedSubviews: [ copyButton, shareButton ])
|
||||
buttonContainer.axis = .horizontal
|
||||
buttonContainer.spacing = Values.mediumSpacing
|
||||
buttonContainer.distribution = .fillEqually
|
||||
// Next button
|
||||
let nextButton = Button(style: .prominentOutline, size: .large)
|
||||
nextButton.setTitle(NSLocalizedString("Next", comment: ""), for: UIControl.State.normal)
|
||||
nextButton.addTarget(self, action: #selector(startNewPrivateChatIfPossible), for: UIControl.Event.touchUpInside)
|
||||
let nextButtonContainer = UIView()
|
||||
nextButtonContainer.addSubview(nextButton)
|
||||
nextButton.pin(.leading, to: .leading, of: nextButtonContainer, withInset: 80)
|
||||
nextButton.pin(.top, to: .top, of: nextButtonContainer)
|
||||
nextButtonContainer.pin(.trailing, to: .trailing, of: nextButton, withInset: 80)
|
||||
nextButtonContainer.pin(.bottom, to: .bottom, of: nextButton)
|
||||
// Set up stack view
|
||||
let stackView = UIStackView(arrangedSubviews: [ publicKeyTextField, UIView.spacer(withHeight: Values.smallSpacing), explanationLabel, UIView.spacer(withHeight: Values.largeSpacing), separator, UIView.spacer(withHeight: Values.veryLargeSpacing), userPublicKeyLabel, UIView.spacer(withHeight: Values.veryLargeSpacing), buttonContainer, UIView.vStretchingSpacer(), nextButtonContainer ])
|
||||
stackView.axis = .vertical
|
||||
stackView.alignment = .fill
|
||||
stackView.layoutMargins = UIEdgeInsets(top: Values.mediumSpacing, left: Values.largeSpacing, bottom: Values.mediumSpacing, right: Values.largeSpacing)
|
||||
stackView.isLayoutMarginsRelativeArrangement = true
|
||||
view.addSubview(stackView)
|
||||
stackView.pin(to: view)
|
||||
// Set up width constraint
|
||||
view.set(.width, to: UIScreen.main.bounds.width)
|
||||
// Dismiss keyboard on tap
|
||||
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
|
||||
view.addGestureRecognizer(tapGestureRecognizer)
|
||||
}
|
||||
|
||||
// MARK: General
|
||||
func constrainHeight(to height: CGFloat) {
|
||||
view.set(.height, to: height)
|
||||
}
|
||||
|
||||
@objc private func dismissKeyboard() {
|
||||
publicKeyTextField.resignFirstResponder()
|
||||
}
|
||||
|
||||
@objc private func enableCopyButton() {
|
||||
copyButton.isUserInteractionEnabled = true
|
||||
UIView.transition(with: copyButton, duration: 0.25, options: .transitionCrossDissolve, animations: {
|
||||
self.copyButton.setTitle(NSLocalizedString("Copy", comment: ""), for: UIControl.State.normal)
|
||||
}, completion: nil)
|
||||
}
|
||||
|
||||
// MARK: Interaction
|
||||
@objc private func copyPublicKey() {
|
||||
UIPasteboard.general.string = userHexEncodedPublicKey
|
||||
copyButton.isUserInteractionEnabled = false
|
||||
UIView.transition(with: copyButton, duration: 0.25, options: .transitionCrossDissolve, animations: {
|
||||
self.copyButton.setTitle(NSLocalizedString("Copied", comment: ""), for: UIControl.State.normal)
|
||||
}, completion: nil)
|
||||
Timer.scheduledTimer(timeInterval: 4, target: self, selector: #selector(enableCopyButton), userInfo: nil, repeats: false)
|
||||
}
|
||||
|
||||
@objc private func sharePublicKey() {
|
||||
let shareVC = UIActivityViewController(activityItems: [ userHexEncodedPublicKey ], applicationActivities: nil)
|
||||
newPrivateChatVC.navigationController!.present(shareVC, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@objc private func startNewPrivateChatIfPossible() {
|
||||
let hexEncodedPublicKey = publicKeyTextField.text?.trimmingCharacters(in: .whitespaces) ?? ""
|
||||
newPrivateChatVC.startNewPrivateChatIfPossible(with: hexEncodedPublicKey)
|
||||
}
|
||||
}
|
||||
|
||||
private final class ScanQRCodePlaceholderVC : UIViewController {
|
||||
weak var newPrivateChatVC: NewPrivateChatVC!
|
||||
|
||||
override func viewDidLoad() {
|
||||
// Remove background color
|
||||
view.backgroundColor = .clear
|
||||
// Set up explanation label
|
||||
let explanationLabel = UILabel()
|
||||
explanationLabel.textColor = Colors.text
|
||||
explanationLabel.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
explanationLabel.text = NSLocalizedString("Loki Messenger needs camera access to scan QR codes", comment: "")
|
||||
explanationLabel.numberOfLines = 0
|
||||
explanationLabel.textAlignment = .center
|
||||
explanationLabel.lineBreakMode = .byWordWrapping
|
||||
// Set up call to action button
|
||||
let callToActionButton = UIButton()
|
||||
callToActionButton.titleLabel!.font = .boldSystemFont(ofSize: Values.mediumFontSize)
|
||||
callToActionButton.setTitleColor(Colors.accent, for: UIControl.State.normal)
|
||||
callToActionButton.setTitle(NSLocalizedString("Enable Camera Access", comment: ""), for: UIControl.State.normal)
|
||||
callToActionButton.addTarget(self, action: #selector(requestCameraAccess), for: UIControl.Event.touchUpInside)
|
||||
// Set up stack view
|
||||
let stackView = UIStackView(arrangedSubviews: [ explanationLabel, callToActionButton ])
|
||||
stackView.axis = .vertical
|
||||
stackView.spacing = Values.mediumSpacing
|
||||
stackView.alignment = .center
|
||||
// Set up constraints
|
||||
view.set(.width, to: UIScreen.main.bounds.width)
|
||||
view.addSubview(stackView)
|
||||
stackView.pin(.leading, to: .leading, of: view, withInset: Values.massiveSpacing)
|
||||
view.pin(.trailing, to: .trailing, of: stackView, withInset: Values.massiveSpacing)
|
||||
let verticalCenteringConstraint = stackView.center(.vertical, in: view)
|
||||
verticalCenteringConstraint.constant = -16 // Makes things appear centered visually
|
||||
}
|
||||
|
||||
func constrainHeight(to height: CGFloat) {
|
||||
view.set(.height, to: height)
|
||||
}
|
||||
|
||||
@objc private func requestCameraAccess() {
|
||||
ows_ask(forCameraPermissions: { [weak self] hasCameraAccess in
|
||||
if hasCameraAccess {
|
||||
self?.newPrivateChatVC.handleCameraAccessGranted()
|
||||
} else {
|
||||
// Do nothing
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
|
||||
@objc(LKNukeDataModal)
|
||||
final class NukeDataModal : Modal {
|
||||
|
||||
// MARK: Lifecycle
|
||||
override func populateContentView() {
|
||||
// Set up title label
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.textColor = Colors.text
|
||||
titleLabel.font = .boldSystemFont(ofSize: Values.mediumFontSize)
|
||||
titleLabel.text = NSLocalizedString("Clear All Data", comment: "")
|
||||
titleLabel.numberOfLines = 0
|
||||
titleLabel.lineBreakMode = .byWordWrapping
|
||||
titleLabel.textAlignment = .center
|
||||
// Set up explanation label
|
||||
let explanationLabel = UILabel()
|
||||
explanationLabel.textColor = Colors.text.withAlphaComponent(Values.unimportantElementOpacity)
|
||||
explanationLabel.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
explanationLabel.text = NSLocalizedString("This will delete your entire account, including all data, any messages currently linked to your public key, as well as your personal key pair.", comment: "")
|
||||
explanationLabel.numberOfLines = 0
|
||||
explanationLabel.textAlignment = .center
|
||||
explanationLabel.lineBreakMode = .byWordWrapping
|
||||
// Set up nuke data button
|
||||
let nukeDataButton = UIButton()
|
||||
nukeDataButton.set(.height, to: Values.mediumButtonHeight)
|
||||
nukeDataButton.layer.cornerRadius = Values.modalButtonCornerRadius
|
||||
nukeDataButton.backgroundColor = Colors.destructive
|
||||
nukeDataButton.titleLabel!.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
nukeDataButton.setTitleColor(Colors.text, for: UIControl.State.normal)
|
||||
nukeDataButton.setTitle(NSLocalizedString("Delete", comment: ""), for: UIControl.State.normal)
|
||||
nukeDataButton.addTarget(self, action: #selector(nuke), for: UIControl.Event.touchUpInside)
|
||||
// Set up button stack view
|
||||
let buttonStackView = UIStackView(arrangedSubviews: [ cancelButton, nukeDataButton ])
|
||||
buttonStackView.axis = .horizontal
|
||||
buttonStackView.spacing = Values.mediumSpacing
|
||||
buttonStackView.distribution = .fillEqually
|
||||
// Set up stack view
|
||||
let stackView = UIStackView(arrangedSubviews: [ titleLabel, explanationLabel, buttonStackView ])
|
||||
stackView.axis = .vertical
|
||||
stackView.spacing = Values.largeSpacing
|
||||
contentView.addSubview(stackView)
|
||||
stackView.pin(.leading, to: .leading, of: contentView, withInset: Values.largeSpacing)
|
||||
stackView.pin(.top, to: .top, of: contentView, withInset: Values.largeSpacing)
|
||||
contentView.pin(.trailing, to: .trailing, of: stackView, withInset: Values.largeSpacing)
|
||||
contentView.pin(.bottom, to: .bottom, of: stackView, withInset: Values.largeSpacing)
|
||||
}
|
||||
|
||||
// MARK: Interaction
|
||||
@objc private func nuke() {
|
||||
UserDefaults.removeAll() // Not done in the nuke data implementation as unlinking requires this to happen later
|
||||
NotificationCenter.default.post(name: .dataNukeRequested, object: nil)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,279 @@
|
|||
|
||||
final class QRCodeVC : UIViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate, OWSQRScannerDelegate {
|
||||
private let pageVC = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
|
||||
private var pages: [UIViewController] = []
|
||||
private var targetVCIndex: Int?
|
||||
private var tabBarTopConstraint: NSLayoutConstraint!
|
||||
|
||||
// MARK: Settings
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle { return .lightContent }
|
||||
|
||||
// MARK: Components
|
||||
private lazy var tabBar: TabBar = {
|
||||
let tabs = [
|
||||
TabBar.Tab(title: NSLocalizedString("View My QR Code", comment: "")) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.pageVC.setViewControllers([ self.pages[0] ], direction: .forward, animated: false, completion: nil)
|
||||
},
|
||||
TabBar.Tab(title: NSLocalizedString("Scan QR Code", comment: "")) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.pageVC.setViewControllers([ self.pages[1] ], direction: .forward, animated: false, completion: nil)
|
||||
}
|
||||
]
|
||||
return TabBar(tabs: tabs)
|
||||
}()
|
||||
|
||||
private lazy var viewMyQRCodeVC: ViewMyQRCodeVC = {
|
||||
let result = ViewMyQRCodeVC()
|
||||
result.qrCodeVC = self
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var scanQRCodePlaceholderVC: ScanQRCodePlaceholderVC = {
|
||||
let result = ScanQRCodePlaceholderVC()
|
||||
result.qrCodeVC = self
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var scanQRCodeWrapperVC: ScanQRCodeWrapperVC = {
|
||||
let message = NSLocalizedString("Scan someone's QR code to start a conversation with them", comment: "")
|
||||
let result = ScanQRCodeWrapperVC(message: message)
|
||||
result.delegate = self
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: Lifecycle
|
||||
override func viewDidLoad() {
|
||||
// Set gradient background
|
||||
view.backgroundColor = .clear
|
||||
let gradient = Gradients.defaultLokiBackground
|
||||
view.setGradient(gradient)
|
||||
// Set navigation bar background color
|
||||
let navigationBar = navigationController!.navigationBar
|
||||
navigationBar.setBackgroundImage(UIImage(), for: UIBarMetrics.default)
|
||||
navigationBar.shadowImage = UIImage()
|
||||
navigationBar.isTranslucent = false
|
||||
navigationBar.barTintColor = Colors.navigationBarBackground
|
||||
// Customize title
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.text = NSLocalizedString("QR Code", comment: "")
|
||||
titleLabel.textColor = Colors.text
|
||||
titleLabel.font = .boldSystemFont(ofSize: Values.veryLargeFontSize)
|
||||
navigationItem.titleView = titleLabel
|
||||
// Set up page VC
|
||||
let hasCameraAccess = (AVCaptureDevice.authorizationStatus(for: .video) == .authorized)
|
||||
pages = [ viewMyQRCodeVC, (hasCameraAccess ? scanQRCodeWrapperVC : scanQRCodePlaceholderVC) ]
|
||||
pageVC.dataSource = self
|
||||
pageVC.delegate = self
|
||||
pageVC.setViewControllers([ viewMyQRCodeVC ], direction: .forward, animated: false, completion: nil)
|
||||
// Set up tab bar
|
||||
view.addSubview(tabBar)
|
||||
tabBar.pin(.leading, to: .leading, of: view)
|
||||
tabBarTopConstraint = tabBar.pin(.top, to: .top, of: view)
|
||||
view.pin(.trailing, to: .trailing, of: tabBar)
|
||||
// Set up page VC constraints
|
||||
let pageVCView = pageVC.view!
|
||||
view.addSubview(pageVCView)
|
||||
pageVCView.pin(.leading, to: .leading, of: view)
|
||||
pageVCView.pin(.top, to: .bottom, of: tabBar)
|
||||
view.pin(.trailing, to: .trailing, of: pageVCView)
|
||||
view.pin(.bottom, to: .bottom, of: pageVCView)
|
||||
let screen = UIScreen.main.bounds
|
||||
pageVCView.set(.width, to: screen.width)
|
||||
let height = navigationController!.view.bounds.height - navigationBar.height() - Values.tabBarHeight
|
||||
pageVCView.set(.height, to: height)
|
||||
viewMyQRCodeVC.constrainHeight(to: height)
|
||||
scanQRCodePlaceholderVC.constrainHeight(to: height)
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
tabBarTopConstraint.constant = navigationController!.navigationBar.height()
|
||||
}
|
||||
|
||||
// MARK: General
|
||||
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
|
||||
guard let index = pages.firstIndex(of: viewController), index != 0 else { return nil }
|
||||
return pages[index - 1]
|
||||
}
|
||||
|
||||
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
|
||||
guard let index = pages.firstIndex(of: viewController), index != (pages.count - 1) else { return nil }
|
||||
return pages[index + 1]
|
||||
}
|
||||
|
||||
fileprivate func handleCameraAccessGranted() {
|
||||
pages[1] = scanQRCodeWrapperVC
|
||||
pageVC.setViewControllers([ scanQRCodeWrapperVC ], direction: .forward, animated: false, completion: nil)
|
||||
}
|
||||
|
||||
// MARK: Updating
|
||||
func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) {
|
||||
guard let targetVC = pendingViewControllers.first, let index = pages.firstIndex(of: targetVC) else { return }
|
||||
targetVCIndex = index
|
||||
}
|
||||
|
||||
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating isFinished: Bool, previousViewControllers: [UIViewController], transitionCompleted isCompleted: Bool) {
|
||||
guard isCompleted, let index = targetVCIndex else { return }
|
||||
tabBar.selectTab(at: index)
|
||||
}
|
||||
|
||||
// MARK: Interaction
|
||||
@objc private func close() {
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
func controller(_ controller: OWSQRCodeScanningViewController, didDetectQRCodeWith string: String) {
|
||||
let hexEncodedPublicKey = string
|
||||
startNewPrivateChatIfPossible(with: hexEncodedPublicKey)
|
||||
}
|
||||
|
||||
fileprivate func startNewPrivateChatIfPossible(with hexEncodedPublicKey: String) {
|
||||
if !ECKeyPair.isValidHexEncodedPublicKey(candidate: hexEncodedPublicKey) {
|
||||
let alert = UIAlertController(title: NSLocalizedString("Invalid Public Key", comment: ""), message: NSLocalizedString("Please check the public key you entered and try again.", comment: ""), preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
|
||||
presentAlert(alert)
|
||||
} else {
|
||||
let thread = TSContactThread.getOrCreateThread(contactId: hexEncodedPublicKey)
|
||||
presentingViewController?.dismiss(animated: true, completion: nil)
|
||||
SignalApp.shared().presentConversation(for: thread, action: .compose, animated: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class ViewMyQRCodeVC : UIViewController {
|
||||
weak var qrCodeVC: QRCodeVC!
|
||||
private var bottomConstraint: NSLayoutConstraint!
|
||||
|
||||
private lazy var userHexEncodedPublicKey: String = {
|
||||
if let masterHexEncodedPublicKey = UserDefaults.standard.string(forKey: "masterDeviceHexEncodedPublicKey") {
|
||||
return masterHexEncodedPublicKey
|
||||
} else {
|
||||
return OWSIdentityManager.shared().identityKeyPair()!.hexEncodedPublicKey
|
||||
}
|
||||
}()
|
||||
|
||||
// MARK: Lifecycle
|
||||
override func viewDidLoad() {
|
||||
// Remove background color
|
||||
view.backgroundColor = .clear
|
||||
// Set up title label
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.textColor = Colors.text
|
||||
titleLabel.font = .boldSystemFont(ofSize: Values.massiveFontSize)
|
||||
titleLabel.text = NSLocalizedString("Scan Me", comment: "")
|
||||
titleLabel.numberOfLines = 0
|
||||
titleLabel.textAlignment = .center
|
||||
titleLabel.lineBreakMode = .byWordWrapping
|
||||
// Set up QR code image view
|
||||
let qrCodeImageView = UIImageView()
|
||||
let qrCode = QRCode.generate(for: userHexEncodedPublicKey)
|
||||
qrCodeImageView.image = qrCode
|
||||
qrCodeImageView.contentMode = .scaleAspectFit
|
||||
qrCodeImageView.set(.height, to: 240)
|
||||
qrCodeImageView.set(.width, to: 240)
|
||||
// Set up QR code image view container
|
||||
let qrCodeImageViewContainer = UIView()
|
||||
qrCodeImageViewContainer.addSubview(qrCodeImageView)
|
||||
qrCodeImageView.center(.horizontal, in: qrCodeImageViewContainer)
|
||||
qrCodeImageView.pin(.top, to: .top, of: qrCodeImageViewContainer)
|
||||
qrCodeImageView.pin(.bottom, to: .bottom, of: qrCodeImageViewContainer)
|
||||
// Set up explanation label
|
||||
let explanationLabel = UILabel()
|
||||
explanationLabel.textColor = Colors.text
|
||||
explanationLabel.font = Fonts.spaceMono(ofSize: Values.mediumFontSize)
|
||||
let text = NSLocalizedString("This is your unique public QR code. Other users may scan this in order to begin a conversation with you.", comment: "")
|
||||
let attributedText = NSMutableAttributedString(string: text)
|
||||
attributedText.addAttribute(.font, value: Fonts.boldSpaceMono(ofSize: Values.mediumFontSize), range: (text as NSString).range(of: "your unique public QR code"))
|
||||
explanationLabel.attributedText = attributedText
|
||||
explanationLabel.numberOfLines = 0
|
||||
explanationLabel.textAlignment = .center
|
||||
explanationLabel.lineBreakMode = .byWordWrapping
|
||||
// Set up share button
|
||||
let shareButton = Button(style: .regular, size: .large)
|
||||
shareButton.setTitle(NSLocalizedString("Share", comment: ""), for: UIControl.State.normal)
|
||||
shareButton.addTarget(self, action: #selector(shareQRCode), for: UIControl.Event.touchUpInside)
|
||||
// Set up share button container
|
||||
let shareButtonContainer = UIView()
|
||||
shareButtonContainer.addSubview(shareButton)
|
||||
shareButton.pin(.leading, to: .leading, of: shareButtonContainer, withInset: 80)
|
||||
shareButton.pin(.top, to: .top, of: shareButtonContainer)
|
||||
shareButtonContainer.pin(.trailing, to: .trailing, of: shareButton, withInset: 80)
|
||||
shareButtonContainer.pin(.bottom, to: .bottom, of: shareButton)
|
||||
// Set up stack view
|
||||
let stackView = UIStackView(arrangedSubviews: [ titleLabel, qrCodeImageViewContainer, explanationLabel, shareButtonContainer, UIView.vStretchingSpacer() ])
|
||||
stackView.axis = .vertical
|
||||
stackView.spacing = Values.largeSpacing
|
||||
stackView.alignment = .fill
|
||||
stackView.layoutMargins = UIEdgeInsets(top: Values.mediumSpacing, left: Values.largeSpacing, bottom: Values.mediumSpacing, right: Values.largeSpacing)
|
||||
stackView.isLayoutMarginsRelativeArrangement = true
|
||||
view.addSubview(stackView)
|
||||
stackView.pin(.leading, to: .leading, of: view)
|
||||
stackView.pin(.top, to: .top, of: view)
|
||||
view.pin(.trailing, to: .trailing, of: stackView)
|
||||
bottomConstraint = view.pin(.bottom, to: .bottom, of: stackView)
|
||||
// Set up width constraint
|
||||
view.set(.width, to: UIScreen.main.bounds.width)
|
||||
}
|
||||
|
||||
// MARK: General
|
||||
func constrainHeight(to height: CGFloat) {
|
||||
view.set(.height, to: height)
|
||||
}
|
||||
|
||||
// MARK: Interaction
|
||||
@objc private func shareQRCode() {
|
||||
let qrCode = QRCode.generate(for: userHexEncodedPublicKey, hasBackground: true)
|
||||
let shareVC = UIActivityViewController(activityItems: [ qrCode ], applicationActivities: nil)
|
||||
qrCodeVC.navigationController!.present(shareVC, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
private final class ScanQRCodePlaceholderVC : UIViewController {
|
||||
weak var qrCodeVC: QRCodeVC!
|
||||
|
||||
override func viewDidLoad() {
|
||||
// Remove background color
|
||||
view.backgroundColor = .clear
|
||||
// Set up explanation label
|
||||
let explanationLabel = UILabel()
|
||||
explanationLabel.textColor = Colors.text
|
||||
explanationLabel.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
explanationLabel.text = NSLocalizedString("Loki Messenger needs camera access to scan QR codes", comment: "")
|
||||
explanationLabel.numberOfLines = 0
|
||||
explanationLabel.textAlignment = .center
|
||||
explanationLabel.lineBreakMode = .byWordWrapping
|
||||
// Set up call to action button
|
||||
let callToActionButton = UIButton()
|
||||
callToActionButton.titleLabel!.font = .boldSystemFont(ofSize: Values.mediumFontSize)
|
||||
callToActionButton.setTitleColor(Colors.accent, for: UIControl.State.normal)
|
||||
callToActionButton.setTitle(NSLocalizedString("Enable Camera Access", comment: ""), for: UIControl.State.normal)
|
||||
callToActionButton.addTarget(self, action: #selector(requestCameraAccess), for: UIControl.Event.touchUpInside)
|
||||
// Set up stack view
|
||||
let stackView = UIStackView(arrangedSubviews: [ explanationLabel, callToActionButton ])
|
||||
stackView.axis = .vertical
|
||||
stackView.spacing = Values.mediumSpacing
|
||||
stackView.alignment = .center
|
||||
// Set up constraints
|
||||
view.set(.width, to: UIScreen.main.bounds.width)
|
||||
view.addSubview(stackView)
|
||||
stackView.pin(.leading, to: .leading, of: view, withInset: Values.massiveSpacing)
|
||||
view.pin(.trailing, to: .trailing, of: stackView, withInset: Values.massiveSpacing)
|
||||
let verticalCenteringConstraint = stackView.center(.vertical, in: view)
|
||||
verticalCenteringConstraint.constant = -16 // Makes things appear centered visually
|
||||
}
|
||||
|
||||
func constrainHeight(to height: CGFloat) {
|
||||
view.set(.height, to: height)
|
||||
}
|
||||
|
||||
@objc private func requestCameraAccess() {
|
||||
ows_ask(forCameraPermissions: { [weak self] hasCameraAccess in
|
||||
if hasCameraAccess {
|
||||
self?.qrCodeVC.handleCameraAccessGranted()
|
||||
} else {
|
||||
// Do nothing
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,182 @@
|
|||
|
||||
final class RegisterVC : UIViewController {
|
||||
private var seed: Data! { didSet { updateKeyPair() } }
|
||||
private var keyPair: ECKeyPair! { didSet { updatePublicKeyLabel() } }
|
||||
|
||||
// MARK: Components
|
||||
private lazy var publicKeyLabel: UILabel = {
|
||||
let result = UILabel()
|
||||
result.textColor = Colors.text
|
||||
result.font = Fonts.spaceMono(ofSize: Values.largeFontSize)
|
||||
result.numberOfLines = 0
|
||||
result.textAlignment = .center
|
||||
result.lineBreakMode = .byCharWrapping
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var copyPublicKeyButton: Button = {
|
||||
let result = Button(style: .prominentOutline, size: .large)
|
||||
result.setTitle(NSLocalizedString("Copy", comment: ""), for: UIControl.State.normal)
|
||||
result.addTarget(self, action: #selector(copyPublicKey), for: UIControl.Event.touchUpInside)
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var legalLabel: UILabel = {
|
||||
let result = UILabel()
|
||||
result.textColor = Colors.text
|
||||
result.font = .systemFont(ofSize: Values.verySmallFontSize)
|
||||
let text = "By using this service, you agree to our Terms and Conditions and Privacy Statement"
|
||||
let attributedText = NSMutableAttributedString(string: text)
|
||||
attributedText.addAttribute(.font, value: UIFont.boldSystemFont(ofSize: Values.verySmallFontSize), range: (text as NSString).range(of: "Terms and Conditions"))
|
||||
attributedText.addAttribute(.font, value: UIFont.boldSystemFont(ofSize: Values.verySmallFontSize), range: (text as NSString).range(of: "Privacy Statement"))
|
||||
result.attributedText = attributedText
|
||||
result.numberOfLines = 0
|
||||
result.textAlignment = .center
|
||||
result.lineBreakMode = .byWordWrapping
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: Settings
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle { return .lightContent }
|
||||
|
||||
// MARK: Lifecycle
|
||||
override func viewDidLoad() {
|
||||
// Set gradient background
|
||||
view.backgroundColor = .clear
|
||||
let gradient = Gradients.defaultLokiBackground
|
||||
view.setGradient(gradient)
|
||||
// Set up navigation bar
|
||||
let navigationBar = navigationController!.navigationBar
|
||||
navigationBar.setBackgroundImage(UIImage(), for: UIBarMetrics.default)
|
||||
navigationBar.shadowImage = UIImage()
|
||||
navigationBar.isTranslucent = false
|
||||
navigationBar.barTintColor = Colors.navigationBarBackground
|
||||
// Set up logo image view
|
||||
let logoImageView = UIImageView()
|
||||
logoImageView.image = #imageLiteral(resourceName: "Session")
|
||||
logoImageView.contentMode = .scaleAspectFit
|
||||
logoImageView.set(.width, to: 32)
|
||||
logoImageView.set(.height, to: 32)
|
||||
navigationItem.titleView = logoImageView
|
||||
// Set up title label
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.textColor = Colors.text
|
||||
titleLabel.font = .boldSystemFont(ofSize: Values.veryLargeFontSize)
|
||||
titleLabel.text = NSLocalizedString("Say hello to your Session ID", comment: "")
|
||||
titleLabel.numberOfLines = 0
|
||||
titleLabel.lineBreakMode = .byWordWrapping
|
||||
// Set up explanation label
|
||||
let explanationLabel = UILabel()
|
||||
explanationLabel.textColor = Colors.text
|
||||
explanationLabel.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
explanationLabel.text = NSLocalizedString("Your Session ID is the unique address that people can use to contact you on Session. With no connection to your real identity, your Session ID is totally anonymous and private by design.", comment: "")
|
||||
explanationLabel.numberOfLines = 0
|
||||
explanationLabel.lineBreakMode = .byWordWrapping
|
||||
// Set up public key label container
|
||||
let publicKeyLabelContainer = UIView()
|
||||
publicKeyLabelContainer.addSubview(publicKeyLabel)
|
||||
publicKeyLabel.pin(to: publicKeyLabelContainer, withInset: Values.mediumSpacing)
|
||||
publicKeyLabelContainer.layer.cornerRadius = Values.textFieldCornerRadius
|
||||
publicKeyLabelContainer.layer.borderWidth = Values.borderThickness
|
||||
publicKeyLabelContainer.layer.borderColor = Colors.text.cgColor
|
||||
// Set up spacers
|
||||
let topSpacer = UIView.vStretchingSpacer()
|
||||
let bottomSpacer = UIView.vStretchingSpacer()
|
||||
// Set up register button
|
||||
let registerButton = Button(style: .prominentFilled, size: .large)
|
||||
registerButton.setTitle(NSLocalizedString("Continue", comment: ""), for: UIControl.State.normal)
|
||||
registerButton.titleLabel!.font = .boldSystemFont(ofSize: Values.mediumFontSize)
|
||||
registerButton.addTarget(self, action: #selector(register), for: UIControl.Event.touchUpInside)
|
||||
// Set up button stack view
|
||||
let buttonStackView = UIStackView(arrangedSubviews: [ registerButton, copyPublicKeyButton ])
|
||||
buttonStackView.axis = .vertical
|
||||
buttonStackView.spacing = Values.mediumSpacing
|
||||
buttonStackView.alignment = .fill
|
||||
// Set up button stack view container
|
||||
let buttonStackViewContainer = UIView()
|
||||
buttonStackViewContainer.addSubview(buttonStackView)
|
||||
buttonStackView.pin(.leading, to: .leading, of: buttonStackViewContainer, withInset: Values.massiveSpacing)
|
||||
buttonStackView.pin(.top, to: .top, of: buttonStackViewContainer)
|
||||
buttonStackViewContainer.pin(.trailing, to: .trailing, of: buttonStackView, withInset: Values.massiveSpacing)
|
||||
buttonStackViewContainer.pin(.bottom, to: .bottom, of: buttonStackView)
|
||||
// Set up legal label
|
||||
legalLabel.isUserInteractionEnabled = true
|
||||
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleLegalLabelTapped))
|
||||
legalLabel.addGestureRecognizer(tapGestureRecognizer)
|
||||
// Set up legal label container
|
||||
let legalLabelContainer = UIView()
|
||||
legalLabelContainer.set(.height, to: Values.onboardingButtonBottomOffset)
|
||||
legalLabelContainer.addSubview(legalLabel)
|
||||
legalLabel.pin(.leading, to: .leading, of: legalLabelContainer, withInset: Values.massiveSpacing)
|
||||
legalLabel.pin(.top, to: .top, of: legalLabelContainer)
|
||||
legalLabelContainer.pin(.trailing, to: .trailing, of: legalLabel, withInset: Values.massiveSpacing)
|
||||
legalLabelContainer.pin(.bottom, to: .bottom, of: legalLabel, withInset: 10)
|
||||
// Set up top stack view
|
||||
let topStackView = UIStackView(arrangedSubviews: [ titleLabel, explanationLabel, publicKeyLabelContainer ])
|
||||
topStackView.axis = .vertical
|
||||
topStackView.spacing = Values.veryLargeSpacing
|
||||
topStackView.alignment = .fill
|
||||
// Set up top stack view container
|
||||
let topStackViewContainer = UIView()
|
||||
topStackViewContainer.addSubview(topStackView)
|
||||
topStackView.pin(.leading, to: .leading, of: topStackViewContainer, withInset: Values.veryLargeSpacing)
|
||||
topStackView.pin(.top, to: .top, of: topStackViewContainer)
|
||||
topStackViewContainer.pin(.trailing, to: .trailing, of: topStackView, withInset: Values.veryLargeSpacing)
|
||||
topStackViewContainer.pin(.bottom, to: .bottom, of: topStackView)
|
||||
// Set up main stack view
|
||||
let mainStackView = UIStackView(arrangedSubviews: [ topSpacer, topStackViewContainer, bottomSpacer, buttonStackViewContainer, legalLabelContainer ])
|
||||
mainStackView.axis = .vertical
|
||||
mainStackView.alignment = .fill
|
||||
view.addSubview(mainStackView)
|
||||
mainStackView.pin(to: view)
|
||||
topSpacer.heightAnchor.constraint(equalTo: bottomSpacer.heightAnchor, multiplier: 1).isActive = true
|
||||
// Peform initial seed update
|
||||
updateSeed()
|
||||
}
|
||||
|
||||
// MARK: General
|
||||
@objc private func enableCopyButton() {
|
||||
copyPublicKeyButton.isUserInteractionEnabled = true
|
||||
UIView.transition(with: copyPublicKeyButton, duration: 0.25, options: .transitionCrossDissolve, animations: {
|
||||
self.copyPublicKeyButton.setTitle(NSLocalizedString("Copy", comment: ""), for: UIControl.State.normal)
|
||||
}, completion: nil)
|
||||
}
|
||||
|
||||
// MARK: Updating
|
||||
private func updateSeed() {
|
||||
seed = Randomness.generateRandomBytes(16)
|
||||
}
|
||||
|
||||
private func updateKeyPair() {
|
||||
keyPair = Curve25519.generateKeyPair(fromSeed: seed + seed)
|
||||
}
|
||||
|
||||
private func updatePublicKeyLabel() {
|
||||
publicKeyLabel.text = keyPair.hexEncodedPublicKey
|
||||
}
|
||||
|
||||
// MARK: Interaction
|
||||
@objc private func register() {
|
||||
let identityManager = OWSIdentityManager.shared()
|
||||
let databaseConnection = identityManager.value(forKey: "dbConnection") as! YapDatabaseConnection
|
||||
databaseConnection.setObject(seed.toHexString(), forKey: "LKLokiSeed", inCollection: OWSPrimaryStorageIdentityKeyStoreCollection)
|
||||
databaseConnection.setObject(keyPair!, forKey: OWSPrimaryStorageIdentityKeyStoreIdentityKey, inCollection: OWSPrimaryStorageIdentityKeyStoreCollection)
|
||||
TSAccountManager.sharedInstance().phoneNumberAwaitingVerification = keyPair!.hexEncodedPublicKey
|
||||
let displayNameVC = DisplayNameVC()
|
||||
navigationController!.pushViewController(displayNameVC, animated: true)
|
||||
}
|
||||
|
||||
@objc private func copyPublicKey() {
|
||||
UIPasteboard.general.string = keyPair.hexEncodedPublicKey
|
||||
copyPublicKeyButton.isUserInteractionEnabled = false
|
||||
UIView.transition(with: copyPublicKeyButton, duration: 0.25, options: .transitionCrossDissolve, animations: {
|
||||
self.copyPublicKeyButton.setTitle(NSLocalizedString("Copied", comment: ""), for: UIControl.State.normal)
|
||||
}, completion: nil)
|
||||
Timer.scheduledTimer(timeInterval: 4, target: self, selector: #selector(enableCopyButton), userInfo: nil, repeats: false)
|
||||
}
|
||||
|
||||
@objc private func handleLegalLabelTapped(_ tapGestureRecognizer: UITapGestureRecognizer) {
|
||||
let url = URL(string: "https://github.com/loki-project/loki-messenger-ios/blob/master/privacy-policy.md")!
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,194 @@
|
|||
|
||||
final class RestoreVC : UIViewController {
|
||||
private var spacer1HeightConstraint: NSLayoutConstraint!
|
||||
private var spacer2HeightConstraint: NSLayoutConstraint!
|
||||
private var spacer3HeightConstraint: NSLayoutConstraint!
|
||||
private var restoreButtonBottomOffsetConstraint: NSLayoutConstraint!
|
||||
private var bottomConstraint: NSLayoutConstraint!
|
||||
|
||||
// MARK: Components
|
||||
private lazy var mnemonicTextField: TextField = {
|
||||
let result = TextField(placeholder: NSLocalizedString("Enter your seed", comment: ""))
|
||||
result.layer.borderColor = Colors.text.cgColor
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var legalLabel: UILabel = {
|
||||
let result = UILabel()
|
||||
result.textColor = Colors.text
|
||||
result.font = .systemFont(ofSize: Values.verySmallFontSize)
|
||||
let text = "By using this service, you agree to our Terms and Conditions and Privacy Statement"
|
||||
let attributedText = NSMutableAttributedString(string: text)
|
||||
attributedText.addAttribute(.font, value: UIFont.boldSystemFont(ofSize: Values.verySmallFontSize), range: (text as NSString).range(of: "Terms and Conditions"))
|
||||
attributedText.addAttribute(.font, value: UIFont.boldSystemFont(ofSize: Values.verySmallFontSize), range: (text as NSString).range(of: "Privacy Statement"))
|
||||
result.attributedText = attributedText
|
||||
result.numberOfLines = 0
|
||||
result.textAlignment = .center
|
||||
result.lineBreakMode = .byWordWrapping
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: Settings
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle { return .lightContent }
|
||||
|
||||
// MARK: Lifecycle
|
||||
override func viewDidLoad() {
|
||||
// Set gradient background
|
||||
view.backgroundColor = .clear
|
||||
let gradient = Gradients.defaultLokiBackground
|
||||
view.setGradient(gradient)
|
||||
// Set up navigation bar
|
||||
let navigationBar = navigationController!.navigationBar
|
||||
navigationBar.setBackgroundImage(UIImage(), for: UIBarMetrics.default)
|
||||
navigationBar.shadowImage = UIImage()
|
||||
navigationBar.isTranslucent = false
|
||||
navigationBar.barTintColor = Colors.navigationBarBackground
|
||||
// Set up logo image view
|
||||
let logoImageView = UIImageView()
|
||||
logoImageView.image = #imageLiteral(resourceName: "Session")
|
||||
logoImageView.contentMode = .scaleAspectFit
|
||||
logoImageView.set(.width, to: 32)
|
||||
logoImageView.set(.height, to: 32)
|
||||
navigationItem.titleView = logoImageView
|
||||
// Set up title label
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.textColor = Colors.text
|
||||
titleLabel.font = .boldSystemFont(ofSize: Values.veryLargeFontSize)
|
||||
titleLabel.text = NSLocalizedString("Restore your account using your seed", comment: "")
|
||||
titleLabel.numberOfLines = 0
|
||||
titleLabel.lineBreakMode = .byWordWrapping
|
||||
// Set up explanation label
|
||||
let explanationLabel = UILabel()
|
||||
explanationLabel.textColor = Colors.text
|
||||
explanationLabel.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
explanationLabel.text = "Enter the seed that was given to you when you signed up to restore your account."
|
||||
explanationLabel.numberOfLines = 0
|
||||
explanationLabel.lineBreakMode = .byWordWrapping
|
||||
// Set up legal label
|
||||
legalLabel.isUserInteractionEnabled = true
|
||||
let legalLabelTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleLegalLabelTapped))
|
||||
legalLabel.addGestureRecognizer(legalLabelTapGestureRecognizer)
|
||||
// Set up spacers
|
||||
let topSpacer = UIView.vStretchingSpacer()
|
||||
let spacer1 = UIView()
|
||||
spacer1HeightConstraint = spacer1.set(.height, to: Values.veryLargeSpacing)
|
||||
let spacer2 = UIView()
|
||||
spacer2HeightConstraint = spacer2.set(.height, to: Values.veryLargeSpacing)
|
||||
let spacer3 = UIView()
|
||||
spacer3HeightConstraint = spacer3.set(.height, to: Values.veryLargeSpacing)
|
||||
let bottomSpacer = UIView.vStretchingSpacer()
|
||||
let restoreButtonBottomOffsetSpacer = UIView()
|
||||
restoreButtonBottomOffsetConstraint = restoreButtonBottomOffsetSpacer.set(.height, to: Values.onboardingButtonBottomOffset)
|
||||
// Set up restore button
|
||||
let restoreButton = Button(style: .prominentFilled, size: .large)
|
||||
restoreButton.setTitle(NSLocalizedString("Continue", comment: ""), for: UIControl.State.normal)
|
||||
restoreButton.titleLabel!.font = .boldSystemFont(ofSize: Values.mediumFontSize)
|
||||
restoreButton.addTarget(self, action: #selector(restore), for: UIControl.Event.touchUpInside)
|
||||
// Set up restore button container
|
||||
let restoreButtonContainer = UIView()
|
||||
restoreButtonContainer.addSubview(restoreButton)
|
||||
restoreButton.pin(.leading, to: .leading, of: restoreButtonContainer, withInset: Values.massiveSpacing)
|
||||
restoreButton.pin(.top, to: .top, of: restoreButtonContainer)
|
||||
restoreButtonContainer.pin(.trailing, to: .trailing, of: restoreButton, withInset: Values.massiveSpacing)
|
||||
restoreButtonContainer.pin(.bottom, to: .bottom, of: restoreButton)
|
||||
// Set up top stack view
|
||||
let topStackView = UIStackView(arrangedSubviews: [ titleLabel, spacer1, explanationLabel, spacer2, mnemonicTextField, spacer3, legalLabel ])
|
||||
topStackView.axis = .vertical
|
||||
topStackView.alignment = .fill
|
||||
// Set up top stack view container
|
||||
let topStackViewContainer = UIView()
|
||||
topStackViewContainer.addSubview(topStackView)
|
||||
topStackView.pin(.leading, to: .leading, of: topStackViewContainer, withInset: Values.veryLargeSpacing)
|
||||
topStackView.pin(.top, to: .top, of: topStackViewContainer)
|
||||
topStackViewContainer.pin(.trailing, to: .trailing, of: topStackView, withInset: Values.veryLargeSpacing)
|
||||
topStackViewContainer.pin(.bottom, to: .bottom, of: topStackView)
|
||||
// Set up main stack view
|
||||
let mainStackView = UIStackView(arrangedSubviews: [ topSpacer, topStackViewContainer, bottomSpacer, restoreButtonContainer, restoreButtonBottomOffsetSpacer ])
|
||||
mainStackView.axis = .vertical
|
||||
mainStackView.alignment = .fill
|
||||
view.addSubview(mainStackView)
|
||||
mainStackView.pin(.leading, to: .leading, of: view)
|
||||
mainStackView.pin(.top, to: .top, of: view)
|
||||
mainStackView.pin(.trailing, to: .trailing, of: view)
|
||||
bottomConstraint = mainStackView.pin(.bottom, to: .bottom, of: view)
|
||||
topSpacer.heightAnchor.constraint(equalTo: bottomSpacer.heightAnchor, multiplier: 1).isActive = true
|
||||
// Dismiss keyboard on tap
|
||||
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
|
||||
view.addGestureRecognizer(tapGestureRecognizer)
|
||||
// Listen to keyboard notifications
|
||||
let notificationCenter = NotificationCenter.default
|
||||
notificationCenter.addObserver(self, selector: #selector(handleKeyboardWillChangeFrameNotification(_:)), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(handleKeyboardWillHideNotification(_:)), name: UIResponder.keyboardWillHideNotification, object: nil)
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
mnemonicTextField.becomeFirstResponder()
|
||||
}
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
// MARK: General
|
||||
@objc private func dismissKeyboard() {
|
||||
mnemonicTextField.resignFirstResponder()
|
||||
}
|
||||
|
||||
// MARK: Updating
|
||||
@objc private func handleKeyboardWillChangeFrameNotification(_ notification: Notification) {
|
||||
guard let newHeight = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue.size.height else { return }
|
||||
bottomConstraint.constant = -newHeight // Negative due to how the constraint is set up
|
||||
restoreButtonBottomOffsetConstraint.constant = Values.largeSpacing
|
||||
spacer1HeightConstraint.constant = Values.mediumSpacing
|
||||
spacer2HeightConstraint.constant = Values.mediumSpacing
|
||||
spacer3HeightConstraint.constant = Values.mediumSpacing
|
||||
UIView.animate(withDuration: 0.25) {
|
||||
self.view.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func handleKeyboardWillHideNotification(_ notification: Notification) {
|
||||
bottomConstraint.constant = 0
|
||||
restoreButtonBottomOffsetConstraint.constant = Values.onboardingButtonBottomOffset
|
||||
spacer1HeightConstraint.constant = Values.veryLargeSpacing
|
||||
spacer2HeightConstraint.constant = Values.veryLargeSpacing
|
||||
spacer3HeightConstraint.constant = Values.veryLargeSpacing
|
||||
UIView.animate(withDuration: 0.25) {
|
||||
self.view.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Interaction
|
||||
@objc private func restore() {
|
||||
func showError(title: String, message: String = "") {
|
||||
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
|
||||
presentAlert(alert)
|
||||
}
|
||||
let mnemonic = mnemonicTextField.text!
|
||||
do {
|
||||
let hexEncodedSeed = try Mnemonic.decode(mnemonic: mnemonic)
|
||||
let seed = Data(hex: hexEncodedSeed)
|
||||
let keyPair = Curve25519.generateKeyPair(fromSeed: seed + seed)
|
||||
let identityManager = OWSIdentityManager.shared()
|
||||
let databaseConnection = identityManager.value(forKey: "dbConnection") as! YapDatabaseConnection
|
||||
databaseConnection.setObject(seed.toHexString(), forKey: "LKLokiSeed", inCollection: OWSPrimaryStorageIdentityKeyStoreCollection)
|
||||
databaseConnection.setObject(keyPair, forKey: OWSPrimaryStorageIdentityKeyStoreIdentityKey, inCollection: OWSPrimaryStorageIdentityKeyStoreCollection)
|
||||
TSAccountManager.sharedInstance().phoneNumberAwaitingVerification = keyPair.hexEncodedPublicKey
|
||||
mnemonicTextField.resignFirstResponder()
|
||||
Timer.scheduledTimer(withTimeInterval: 0.25, repeats: false) { _ in
|
||||
let displayNameVC = DisplayNameVC()
|
||||
self.navigationController!.pushViewController(displayNameVC, animated: true)
|
||||
}
|
||||
} catch let error {
|
||||
let error = error as? Mnemonic.DecodingError ?? Mnemonic.DecodingError.generic
|
||||
showError(title: error.errorDescription!)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func handleLegalLabelTapped(_ tapGestureRecognizer: UITapGestureRecognizer) {
|
||||
let url = URL(string: "https://github.com/loki-project/loki-messenger-ios/blob/master/privacy-policy.md")!
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
}
|
|
@ -15,21 +15,23 @@ final class ScanQRCodeWrapperVC : UIViewController {
|
|||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
preconditionFailure("Use init(title:) instead.")
|
||||
preconditionFailure("Use init(message:) instead.")
|
||||
}
|
||||
|
||||
override init(nibName: String?, bundle: Bundle?) {
|
||||
preconditionFailure("Use init(title:) instead.")
|
||||
preconditionFailure("Use init(message:) instead.")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
// Navigation bar
|
||||
// Set up navigation bar if needed
|
||||
if isPresentedModally {
|
||||
navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .stop, target: self, action: #selector(objc_dismiss))
|
||||
navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .stop, target: self, action: #selector(close))
|
||||
}
|
||||
// Background color
|
||||
view.backgroundColor = Theme.backgroundColor
|
||||
// Scan QR code VC
|
||||
// Set gradient background
|
||||
view.backgroundColor = .clear
|
||||
let gradient = Gradients.defaultLokiBackground
|
||||
view.setGradient(gradient)
|
||||
// Set up scan QR code VC
|
||||
scanQRCodeVC.scanDelegate = delegate
|
||||
let scanQRCodeVCView = scanQRCodeVC.view!
|
||||
view.addSubview(scanQRCodeVCView)
|
||||
|
@ -37,18 +39,18 @@ final class ScanQRCodeWrapperVC : UIViewController {
|
|||
scanQRCodeVCView.pin(.trailing, to: .trailing, of: view)
|
||||
scanQRCodeVCView.autoPin(toTopLayoutGuideOf: self, withInset: 0)
|
||||
scanQRCodeVCView.autoPinToSquareAspectRatio()
|
||||
// Bottom view
|
||||
// Set up bottom view
|
||||
let bottomView = UIView()
|
||||
view.addSubview(bottomView)
|
||||
bottomView.pin(.top, to: .bottom, of: scanQRCodeVCView)
|
||||
bottomView.pin(.leading, to: .leading, of: view)
|
||||
bottomView.pin(.trailing, to: .trailing, of: view)
|
||||
bottomView.pin(.bottom, to: .bottom, of: view)
|
||||
// Explanation label
|
||||
// Set up explanation label
|
||||
let explanationLabel = UILabel()
|
||||
explanationLabel.text = message
|
||||
explanationLabel.textColor = Theme.primaryColor
|
||||
explanationLabel.font = .ows_dynamicTypeSubheadlineClamped
|
||||
explanationLabel.textColor = Colors.text
|
||||
explanationLabel.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
explanationLabel.numberOfLines = 0
|
||||
explanationLabel.lineBreakMode = .byWordWrapping
|
||||
explanationLabel.textAlignment = .center
|
||||
|
@ -68,7 +70,7 @@ final class ScanQRCodeWrapperVC : UIViewController {
|
|||
}
|
||||
|
||||
// MARK: Interaction
|
||||
@objc private func objc_dismiss() {
|
||||
@objc private func close() {
|
||||
presentingViewController?.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
|
||||
@objc(LKSeedModal)
|
||||
final class SeedModal : Modal {
|
||||
|
||||
private let mnemonic: String = {
|
||||
let identityManager = OWSIdentityManager.shared()
|
||||
let databaseConnection = identityManager.value(forKey: "dbConnection") as! YapDatabaseConnection
|
||||
var hexEncodedSeed: String! = databaseConnection.object(forKey: "LKLokiSeed", inCollection: OWSPrimaryStorageIdentityKeyStoreCollection) as! String?
|
||||
if hexEncodedSeed == nil {
|
||||
hexEncodedSeed = identityManager.identityKeyPair()!.hexEncodedPrivateKey // Legacy account
|
||||
}
|
||||
return Mnemonic.encode(hexEncodedString: hexEncodedSeed)
|
||||
}()
|
||||
|
||||
// MARK: Lifecycle
|
||||
override func populateContentView() {
|
||||
// Set up title label
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.textColor = Colors.text
|
||||
titleLabel.font = .boldSystemFont(ofSize: Values.mediumFontSize)
|
||||
titleLabel.text = NSLocalizedString("Your Seed", comment: "")
|
||||
titleLabel.numberOfLines = 0
|
||||
titleLabel.lineBreakMode = .byWordWrapping
|
||||
titleLabel.textAlignment = .center
|
||||
// Set up mnemonic label
|
||||
let mnemonicLabel = UILabel()
|
||||
mnemonicLabel.textColor = Colors.text
|
||||
mnemonicLabel.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
mnemonicLabel.text = mnemonic
|
||||
mnemonicLabel.numberOfLines = 0
|
||||
mnemonicLabel.lineBreakMode = .byWordWrapping
|
||||
mnemonicLabel.textAlignment = .center
|
||||
// Set up explanation label
|
||||
let explanationLabel = UILabel()
|
||||
explanationLabel.textColor = Colors.text.withAlphaComponent(Values.unimportantElementOpacity)
|
||||
explanationLabel.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
explanationLabel.text = NSLocalizedString("This is your personal password. It can be used to restore your account or migrate your account to a new device.", comment: "")
|
||||
explanationLabel.numberOfLines = 0
|
||||
explanationLabel.lineBreakMode = .byWordWrapping
|
||||
explanationLabel.textAlignment = .center
|
||||
// Set up copy button
|
||||
let copyButton = UIButton()
|
||||
copyButton.set(.height, to: Values.mediumButtonHeight)
|
||||
copyButton.layer.cornerRadius = Values.modalButtonCornerRadius
|
||||
copyButton.backgroundColor = Colors.buttonBackground
|
||||
copyButton.titleLabel!.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
copyButton.setTitleColor(Colors.text, for: UIControl.State.normal)
|
||||
copyButton.setTitle(NSLocalizedString("Copy", comment: ""), for: UIControl.State.normal)
|
||||
copyButton.addTarget(self, action: #selector(copySeed), for: UIControl.Event.touchUpInside)
|
||||
// Set up button stack view
|
||||
let buttonStackView = UIStackView(arrangedSubviews: [ cancelButton, copyButton ])
|
||||
buttonStackView.axis = .horizontal
|
||||
buttonStackView.spacing = Values.mediumSpacing
|
||||
buttonStackView.distribution = .fillEqually
|
||||
// Set up stack view
|
||||
let stackView = UIStackView(arrangedSubviews: [ titleLabel, mnemonicLabel, explanationLabel, buttonStackView ])
|
||||
stackView.axis = .vertical
|
||||
stackView.spacing = Values.largeSpacing
|
||||
contentView.addSubview(stackView)
|
||||
stackView.pin(.leading, to: .leading, of: contentView, withInset: Values.largeSpacing)
|
||||
stackView.pin(.top, to: .top, of: contentView, withInset: Values.largeSpacing)
|
||||
contentView.pin(.trailing, to: .trailing, of: stackView, withInset: Values.largeSpacing)
|
||||
contentView.pin(.bottom, to: .bottom, of: stackView, withInset: Values.largeSpacing)
|
||||
// Mark seed as viewed
|
||||
UserDefaults.standard.set(true, forKey: "hasViewedSeed")
|
||||
NotificationCenter.default.post(name: .seedViewed, object: nil)
|
||||
}
|
||||
|
||||
// MARK: Interaction
|
||||
@objc private func copySeed() {
|
||||
UIPasteboard.general.string = mnemonic
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,198 @@
|
|||
|
||||
final class SeedVCV2 : UIViewController {
|
||||
|
||||
private let mnemonic: String = {
|
||||
let identityManager = OWSIdentityManager.shared()
|
||||
let databaseConnection = identityManager.value(forKey: "dbConnection") as! YapDatabaseConnection
|
||||
var hexEncodedSeed: String! = databaseConnection.object(forKey: "LKLokiSeed", inCollection: OWSPrimaryStorageIdentityKeyStoreCollection) as! String?
|
||||
if hexEncodedSeed == nil {
|
||||
hexEncodedSeed = identityManager.identityKeyPair()!.hexEncodedPrivateKey // Legacy account
|
||||
}
|
||||
return Mnemonic.encode(hexEncodedString: hexEncodedSeed)
|
||||
}()
|
||||
|
||||
private lazy var redactedMnemonic: NSAttributedString = {
|
||||
var mnemonic = self.mnemonic
|
||||
let regex = try! NSRegularExpression(pattern: "\\w*", options: [])
|
||||
let matches = regex.matches(in: mnemonic, options: .withoutAnchoringBounds, range: NSRange(location: 0, length: mnemonic.count))
|
||||
let result = NSMutableAttributedString(string: mnemonic)
|
||||
matches.forEach { match in
|
||||
result.addAttribute(.strikethroughStyle, value: NSUnderlineStyle.thick.rawValue, range: match.range)
|
||||
result.addAttribute(.strikethroughColor, value: Colors.accent, range: match.range)
|
||||
}
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: Components
|
||||
private lazy var seedReminderView: SeedReminderView = {
|
||||
let result = SeedReminderView(hasContinueButton: false)
|
||||
let title = "You're almost finished! 90%"
|
||||
let attributedTitle = NSMutableAttributedString(string: title)
|
||||
attributedTitle.addAttribute(.foregroundColor, value: Colors.accent, range: (title as NSString).range(of: "90%"))
|
||||
result.title = attributedTitle
|
||||
result.subtitle = NSLocalizedString("Press the redacted words to view your seed and secure your account", comment: "")
|
||||
result.setProgress(0.9, animated: false)
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var mnemonicLabel: UILabel = {
|
||||
let result = UILabel()
|
||||
result.textColor = Colors.text
|
||||
result.font = Fonts.spaceMono(ofSize: Values.mediumFontSize)
|
||||
result.numberOfLines = 0
|
||||
result.textAlignment = .center
|
||||
result.lineBreakMode = .byWordWrapping
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var copyButton: Button = {
|
||||
let result = Button(style: .prominentOutline, size: .large)
|
||||
result.setTitle(NSLocalizedString("Copy", comment: ""), for: UIControl.State.normal)
|
||||
result.addTarget(self, action: #selector(copyMnemonic), for: UIControl.Event.touchUpInside)
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: Settings
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle { return .lightContent }
|
||||
|
||||
// MARK: Lifecycle
|
||||
override func viewDidLoad() {
|
||||
// Set gradient background
|
||||
view.backgroundColor = .clear
|
||||
let gradient = Gradients.defaultLokiBackground
|
||||
view.setGradient(gradient)
|
||||
// Set up navigation bar
|
||||
let navigationBar = navigationController!.navigationBar
|
||||
navigationBar.setBackgroundImage(UIImage(), for: UIBarMetrics.default)
|
||||
navigationBar.shadowImage = UIImage()
|
||||
navigationBar.isTranslucent = false
|
||||
navigationBar.barTintColor = Colors.navigationBarBackground
|
||||
// Customize title
|
||||
let navigationBarTitleLabel = UILabel()
|
||||
navigationBarTitleLabel.text = NSLocalizedString("Your Seed", comment: "")
|
||||
navigationBarTitleLabel.textColor = Colors.text
|
||||
navigationBarTitleLabel.font = .boldSystemFont(ofSize: Values.veryLargeFontSize)
|
||||
navigationItem.titleView = navigationBarTitleLabel
|
||||
// Set up navigation bar buttons
|
||||
let closeButton = UIBarButtonItem(image: #imageLiteral(resourceName: "X"), style: .plain, target: self, action: #selector(close))
|
||||
closeButton.tintColor = Colors.text
|
||||
navigationItem.leftBarButtonItem = closeButton
|
||||
// Set up title label
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.textColor = Colors.text
|
||||
titleLabel.font = .boldSystemFont(ofSize: Values.veryLargeFontSize)
|
||||
titleLabel.text = NSLocalizedString("Meet your seed", comment: "")
|
||||
titleLabel.numberOfLines = 0
|
||||
titleLabel.lineBreakMode = .byWordWrapping
|
||||
// Set up explanation label
|
||||
let explanationLabel = UILabel()
|
||||
explanationLabel.textColor = Colors.text
|
||||
explanationLabel.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
explanationLabel.text = NSLocalizedString("Think of this as the crypto-equivalent of a social security number. This allows whomever has it complete access to your account.", comment: "")
|
||||
explanationLabel.numberOfLines = 0
|
||||
explanationLabel.lineBreakMode = .byWordWrapping
|
||||
// Set up mnemonic label
|
||||
mnemonicLabel.attributedText = redactedMnemonic
|
||||
let mnemonicLabelGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(revealMnemonic))
|
||||
mnemonicLabel.addGestureRecognizer(mnemonicLabelGestureRecognizer)
|
||||
mnemonicLabel.isUserInteractionEnabled = true
|
||||
mnemonicLabel.isEnabled = true
|
||||
// Set up mnemonic label container
|
||||
let mnemonicLabelContainer = UIView()
|
||||
mnemonicLabelContainer.addSubview(mnemonicLabel)
|
||||
mnemonicLabel.pin(to: mnemonicLabelContainer, withInset: Values.mediumSpacing)
|
||||
mnemonicLabelContainer.layer.cornerRadius = Values.textFieldCornerRadius
|
||||
mnemonicLabelContainer.layer.borderWidth = Values.borderThickness
|
||||
mnemonicLabelContainer.layer.borderColor = Colors.text.cgColor
|
||||
// Set up call to action label
|
||||
let callToActionLabel = UILabel()
|
||||
callToActionLabel.textColor = Colors.text.withAlphaComponent(Values.unimportantElementOpacity)
|
||||
callToActionLabel.font = .systemFont(ofSize: Values.mediumFontSize)
|
||||
callToActionLabel.text = NSLocalizedString("Hold to reveal", comment: "")
|
||||
callToActionLabel.textAlignment = .center
|
||||
let callToActionLabelGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(revealMnemonic))
|
||||
callToActionLabel.addGestureRecognizer(callToActionLabelGestureRecognizer)
|
||||
callToActionLabel.isUserInteractionEnabled = true
|
||||
callToActionLabel.isEnabled = true
|
||||
// Set up spacers
|
||||
let topSpacer = UIView.vStretchingSpacer()
|
||||
let bottomSpacer = UIView.vStretchingSpacer()
|
||||
// Set up copy button container
|
||||
let copyButtonContainer = UIView()
|
||||
copyButtonContainer.addSubview(copyButton)
|
||||
copyButton.pin(.leading, to: .leading, of: copyButtonContainer, withInset: Values.massiveSpacing)
|
||||
copyButton.pin(.top, to: .top, of: copyButtonContainer)
|
||||
copyButtonContainer.pin(.trailing, to: .trailing, of: copyButton, withInset: Values.massiveSpacing)
|
||||
copyButtonContainer.pin(.bottom, to: .bottom, of: copyButton)
|
||||
// Set up top stack view
|
||||
let topStackView = UIStackView(arrangedSubviews: [ titleLabel, explanationLabel, mnemonicLabelContainer, callToActionLabel ])
|
||||
topStackView.axis = .vertical
|
||||
topStackView.spacing = Values.largeSpacing
|
||||
topStackView.alignment = .fill
|
||||
// Set up top stack view container
|
||||
let topStackViewContainer = UIView()
|
||||
topStackViewContainer.addSubview(topStackView)
|
||||
topStackView.pin(.leading, to: .leading, of: topStackViewContainer, withInset: Values.veryLargeSpacing)
|
||||
topStackView.pin(.top, to: .top, of: topStackViewContainer)
|
||||
topStackViewContainer.pin(.trailing, to: .trailing, of: topStackView, withInset: Values.veryLargeSpacing)
|
||||
topStackViewContainer.pin(.bottom, to: .bottom, of: topStackView)
|
||||
// Set up seed reminder view
|
||||
view.addSubview(seedReminderView)
|
||||
seedReminderView.pin(.leading, to: .leading, of: view)
|
||||
seedReminderView.pin(.top, to: .top, of: view)
|
||||
seedReminderView.pin(.trailing, to: .trailing, of: view)
|
||||
// Set up main stack view
|
||||
let mainStackView = UIStackView(arrangedSubviews: [ topSpacer, topStackViewContainer, bottomSpacer, copyButtonContainer ])
|
||||
mainStackView.axis = .vertical
|
||||
mainStackView.alignment = .fill
|
||||
mainStackView.layoutMargins = UIEdgeInsets(top: 0, leading: 0, bottom: Values.onboardingButtonBottomOffset, trailing: 0)
|
||||
mainStackView.isLayoutMarginsRelativeArrangement = true
|
||||
view.addSubview(mainStackView)
|
||||
mainStackView.pin(.leading, to: .leading, of: view)
|
||||
mainStackView.pin(.top, to: .bottom, of: seedReminderView)
|
||||
mainStackView.pin(.trailing, to: .trailing, of: view)
|
||||
mainStackView.pin(.bottom, to: .bottom, of: view)
|
||||
topSpacer.heightAnchor.constraint(equalTo: bottomSpacer.heightAnchor, multiplier: 1).isActive = true
|
||||
}
|
||||
|
||||
// MARK: General
|
||||
@objc private func enableCopyButton() {
|
||||
copyButton.isUserInteractionEnabled = true
|
||||
UIView.transition(with: copyButton, duration: 0.25, options: .transitionCrossDissolve, animations: {
|
||||
self.copyButton.setTitle(NSLocalizedString("Copy", comment: ""), for: UIControl.State.normal)
|
||||
}, completion: nil)
|
||||
}
|
||||
|
||||
// MARK: Interaction
|
||||
@objc private func close() {
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@objc private func revealMnemonic() {
|
||||
UIView.transition(with: mnemonicLabel, duration: 0.25, options: .transitionCrossDissolve, animations: {
|
||||
self.mnemonicLabel.attributedText = NSAttributedString(string: self.mnemonic)
|
||||
}, completion: nil)
|
||||
UIView.transition(with: seedReminderView.titleLabel, duration: 0.25, options: .transitionCrossDissolve, animations: {
|
||||
let title = "Account Secured! 100%"
|
||||
let attributedTitle = NSMutableAttributedString(string: title)
|
||||
attributedTitle.addAttribute(.foregroundColor, value: Colors.accent, range: (title as NSString).range(of: "100%"))
|
||||
self.seedReminderView.title = attributedTitle
|
||||
}, completion: nil)
|
||||
UIView.transition(with: seedReminderView.subtitleLabel, duration: 1, options: .transitionCrossDissolve, animations: {
|
||||
self.seedReminderView.subtitle = NSLocalizedString("Make sure to store your seed in a safe place", comment: "")
|
||||
}, completion: nil)
|
||||
seedReminderView.setProgress(1, animated: true)
|
||||
UserDefaults.standard.set(true, forKey: "hasViewedSeed")
|
||||
NotificationCenter.default.post(name: .seedViewed, object: nil)
|
||||
}
|
||||
|
||||
@objc private func copyMnemonic() {
|
||||
revealMnemonic()
|
||||
UIPasteboard.general.string = mnemonic
|
||||
copyButton.isUserInteractionEnabled = false
|
||||
UIView.transition(with: copyButton, duration: 0.25, options: .transitionCrossDissolve, animations: {
|
||||
self.copyButton.setTitle(NSLocalizedString("Copied", comment: ""), for: UIControl.State.normal)
|
||||
}, completion: nil)
|
||||
Timer.scheduledTimer(timeInterval: 4, target: self, selector: #selector(enableCopyButton), userInfo: nil, repeats: false)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,376 @@
|
|||
|
||||
final class SettingsVC : UIViewController, AvatarViewHelperDelegate {
|
||||
private var profilePictureToBeUploaded: UIImage?
|
||||
private var displayNameToBeUploaded: String?
|
||||
private var isEditingDisplayName = false { didSet { handleIsEditingDisplayNameChanged() } }
|
||||
|
||||
private lazy var userHexEncodedPublicKey: String = {
|
||||
if let masterHexEncodedPublicKey = UserDefaults.standard.string(forKey: "masterDeviceHexEncodedPublicKey") {
|
||||
return masterHexEncodedPublicKey
|
||||
} else {
|
||||
return OWSIdentityManager.shared().identityKeyPair()!.hexEncodedPublicKey
|
||||
}
|
||||
}()
|
||||
|
||||
// MARK: Settings
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle { return .lightContent }
|
||||
|
||||
// MARK: Components
|
||||
private lazy var profilePictureView: ProfilePictureView = {
|
||||
let result = ProfilePictureView()
|
||||
let size = Values.largeProfilePictureSize
|
||||
result.size = size
|
||||
result.set(.width, to: size)
|
||||
result.set(.height, to: size)
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var profilePictureUtilities: AvatarViewHelper = {
|
||||
let result = AvatarViewHelper()
|
||||
result.delegate = self
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var displayNameLabel: UILabel = {
|
||||
let result = UILabel()
|
||||
result.textColor = Colors.text
|
||||
result.font = .boldSystemFont(ofSize: Values.veryLargeFontSize)
|
||||
result.lineBreakMode = .byTruncatingTail
|
||||
result.textAlignment = .center
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var displayNameTextField: TextField = {
|
||||
let result = TextField(placeholder: NSLocalizedString("Enter a display name", comment: ""), usesDefaultHeight: false)
|
||||
result.textAlignment = .center
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var copyButton: Button = {
|
||||
let result = Button(style: .prominentOutline, size: .medium)
|
||||
result.setTitle(NSLocalizedString("Copy", comment: ""), for: UIControl.State.normal)
|
||||
result.addTarget(self, action: #selector(copyPublicKey), for: UIControl.Event.touchUpInside)
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: Lifecycle
|
||||
override func viewDidLoad() {
|
||||
// Set gradient background
|
||||
view.backgroundColor = .clear
|
||||
let gradient = Gradients.defaultLokiBackground
|
||||
view.setGradient(gradient)
|
||||
// Set navigation bar background color
|
||||
let navigationBar = navigationController!.navigationBar
|
||||
navigationBar.setBackgroundImage(UIImage(), for: UIBarMetrics.default)
|
||||
navigationBar.shadowImage = UIImage()
|
||||
navigationBar.isTranslucent = false
|
||||
navigationBar.barTintColor = Colors.navigationBarBackground
|
||||
// Set up navigation bar buttons
|
||||
let backButton = UIBarButtonItem(title: NSLocalizedString("Back", comment: ""), style: .plain, target: nil, action: nil)
|
||||
backButton.tintColor = Colors.text
|
||||
navigationItem.backBarButtonItem = backButton
|
||||
updateNavigationBarButtons()
|
||||
// Customize title
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.text = NSLocalizedString("Settings", comment: "")
|
||||
titleLabel.textColor = Colors.text
|
||||
titleLabel.font = .boldSystemFont(ofSize: Values.veryLargeFontSize)
|
||||
navigationItem.titleView = titleLabel
|
||||
// Set up profile picture view
|
||||
let profilePictureTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(showEditProfilePictureUI))
|
||||
profilePictureView.addGestureRecognizer(profilePictureTapGestureRecognizer)
|
||||
profilePictureView.hexEncodedPublicKey = userHexEncodedPublicKey
|
||||
profilePictureView.update()
|
||||
// Set up display name label
|
||||
displayNameLabel.text = OWSProfileManager.shared().profileName(forRecipientId: userHexEncodedPublicKey)
|
||||
// Set up display name container
|
||||
let displayNameContainer = UIView()
|
||||
displayNameContainer.addSubview(displayNameLabel)
|
||||
displayNameLabel.pin(to: displayNameContainer)
|
||||
displayNameContainer.addSubview(displayNameTextField)
|
||||
displayNameTextField.pin(to: displayNameContainer)
|
||||
displayNameContainer.set(.height, to: 40)
|
||||
displayNameTextField.alpha = 0
|
||||
let displayNameLabelTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(showEditDisplayNameUI))
|
||||
displayNameContainer.addGestureRecognizer(displayNameLabelTapGestureRecognizer)
|
||||
// Set up header view
|
||||
let headerStackView = UIStackView(arrangedSubviews: [ profilePictureView, displayNameContainer ])
|
||||
headerStackView.axis = .vertical
|
||||
headerStackView.spacing = Values.smallSpacing
|
||||
headerStackView.alignment = .center
|
||||
// Set up separator
|
||||
let separator = Separator(title: NSLocalizedString("Your Public Key", comment: ""))
|
||||
// Set up public key label
|
||||
let publicKeyLabel = UILabel()
|
||||
publicKeyLabel.textColor = Colors.text
|
||||
publicKeyLabel.font = Fonts.spaceMono(ofSize: Values.largeFontSize)
|
||||
publicKeyLabel.numberOfLines = 0
|
||||
publicKeyLabel.textAlignment = .center
|
||||
publicKeyLabel.lineBreakMode = .byCharWrapping
|
||||
publicKeyLabel.text = userHexEncodedPublicKey
|
||||
// Set up share button
|
||||
let shareButton = Button(style: .regular, size: .medium)
|
||||
shareButton.setTitle(NSLocalizedString("Share", comment: ""), for: UIControl.State.normal)
|
||||
shareButton.addTarget(self, action: #selector(sharePublicKey), for: UIControl.Event.touchUpInside)
|
||||
// Set up button container
|
||||
let buttonContainer = UIStackView(arrangedSubviews: [ copyButton, shareButton ])
|
||||
buttonContainer.axis = .horizontal
|
||||
buttonContainer.spacing = Values.mediumSpacing
|
||||
buttonContainer.distribution = .fillEqually
|
||||
// Set up top stack view
|
||||
let topStackView = UIStackView(arrangedSubviews: [ headerStackView, separator, publicKeyLabel, buttonContainer ])
|
||||
topStackView.axis = .vertical
|
||||
topStackView.spacing = Values.largeSpacing
|
||||
topStackView.alignment = .fill
|
||||
topStackView.layoutMargins = UIEdgeInsets(top: 0, left: Values.largeSpacing, bottom: 0, right: Values.largeSpacing)
|
||||
topStackView.isLayoutMarginsRelativeArrangement = true
|
||||
// Set up setting buttons stack view
|
||||
let settingButtonsStackView = UIStackView(arrangedSubviews: getSettingButtons() )
|
||||
settingButtonsStackView.axis = .vertical
|
||||
settingButtonsStackView.alignment = .fill
|
||||
// Set up stack view
|
||||
let stackView = UIStackView(arrangedSubviews: [ topStackView, settingButtonsStackView ])
|
||||
stackView.axis = .vertical
|
||||
stackView.spacing = Values.largeSpacing
|
||||
stackView.alignment = .fill
|
||||
stackView.layoutMargins = UIEdgeInsets(top: Values.mediumSpacing, left: 0, bottom: Values.mediumSpacing, right: 0)
|
||||
stackView.isLayoutMarginsRelativeArrangement = true
|
||||
stackView.set(.width, to: UIScreen.main.bounds.width)
|
||||
// Set up scroll view
|
||||
let scrollView = UIScrollView()
|
||||
scrollView.showsVerticalScrollIndicator = false
|
||||
scrollView.addSubview(stackView)
|
||||
stackView.pin(to: scrollView)
|
||||
view.addSubview(scrollView)
|
||||
scrollView.pin(to: view)
|
||||
}
|
||||
|
||||
private func getSettingButtons() -> [UIView] {
|
||||
func getSeparator() -> UIView {
|
||||
let result = UIView()
|
||||
result.backgroundColor = Colors.separator
|
||||
result.set(.height, to: Values.separatorThickness)
|
||||
return result
|
||||
}
|
||||
func getSettingButton(withTitle title: String, color: UIColor, action selector: Selector) -> UIButton {
|
||||
let button = UIButton()
|
||||
button.setTitle(title, for: UIControl.State.normal)
|
||||
button.setTitleColor(color, for: UIControl.State.normal)
|
||||
button.titleLabel!.font = .boldSystemFont(ofSize: Values.mediumFontSize)
|
||||
button.titleLabel!.textAlignment = .center
|
||||
func getImage(withColor color: UIColor) -> UIImage {
|
||||
let rect = CGRect(origin: CGPoint.zero, size: CGSize(width: 1, height: 1))
|
||||
UIGraphicsBeginImageContext(rect.size)
|
||||
let context = UIGraphicsGetCurrentContext()!
|
||||
context.setFillColor(color.cgColor)
|
||||
context.fill(rect)
|
||||
let image = UIGraphicsGetImageFromCurrentImageContext()
|
||||
UIGraphicsEndImageContext()
|
||||
return image!
|
||||
}
|
||||
button.setBackgroundImage(getImage(withColor: Colors.buttonBackground), for: UIControl.State.normal)
|
||||
button.setBackgroundImage(getImage(withColor: Colors.settingButtonSelected), for: UIControl.State.highlighted)
|
||||
button.addTarget(self, action: selector, for: UIControl.Event.touchUpInside)
|
||||
button.set(.height, to: Values.settingButtonHeight)
|
||||
return button
|
||||
}
|
||||
var result = [
|
||||
getSeparator(),
|
||||
getSettingButton(withTitle: NSLocalizedString("Privacy", comment: ""), color: Colors.text, action: #selector(showPrivacySettings)),
|
||||
getSeparator(),
|
||||
getSettingButton(withTitle: NSLocalizedString("Notifications", comment: ""), color: Colors.text, action: #selector(showNotificationSettings))
|
||||
]
|
||||
let isMasterDevice = (UserDefaults.standard.string(forKey: "masterDeviceHexEncodedPublicKey") == nil)
|
||||
if isMasterDevice {
|
||||
result.append(getSeparator())
|
||||
result.append(getSettingButton(withTitle: NSLocalizedString("Linked Devices", comment: ""), color: Colors.text, action: #selector(showLinkedDevices)))
|
||||
result.append(getSeparator())
|
||||
result.append(getSettingButton(withTitle: NSLocalizedString("Show Seed", comment: ""), color: Colors.text, action: #selector(showSeed)))
|
||||
}
|
||||
result.append(getSeparator())
|
||||
result.append(getSettingButton(withTitle: NSLocalizedString("Clear All Data", comment: ""), color: Colors.destructive, action: #selector(clearAllData)))
|
||||
result.append(getSeparator())
|
||||
return result
|
||||
}
|
||||
|
||||
// MARK: General
|
||||
@objc private func enableCopyButton() {
|
||||
copyButton.isUserInteractionEnabled = true
|
||||
UIView.transition(with: copyButton, duration: 0.25, options: .transitionCrossDissolve, animations: {
|
||||
self.copyButton.setTitle(NSLocalizedString("Copy", comment: ""), for: UIControl.State.normal)
|
||||
}, completion: nil)
|
||||
}
|
||||
|
||||
func avatarActionSheetTitle() -> String? {
|
||||
return NSLocalizedString("Update Profile Picture", comment: "")
|
||||
}
|
||||
|
||||
func fromViewController() -> UIViewController {
|
||||
return self
|
||||
}
|
||||
|
||||
func hasClearAvatarAction() -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func clearAvatarActionLabel() -> String {
|
||||
return NSLocalizedString("Clear", comment: "")
|
||||
}
|
||||
|
||||
// MARK: Updating
|
||||
private func handleIsEditingDisplayNameChanged() {
|
||||
updateNavigationBarButtons()
|
||||
UIView.animate(withDuration: 0.25) {
|
||||
self.displayNameLabel.alpha = self.isEditingDisplayName ? 0 : 1
|
||||
self.displayNameTextField.alpha = self.isEditingDisplayName ? 1 : 0
|
||||
}
|
||||
if isEditingDisplayName {
|
||||
displayNameTextField.becomeFirstResponder()
|
||||
} else {
|
||||
displayNameTextField.resignFirstResponder()
|
||||
}
|
||||
}
|
||||
|
||||
private func updateNavigationBarButtons() {
|
||||
if isEditingDisplayName {
|
||||
let cancelButton = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(handleCancelDisplayNameEditingButtonTapped))
|
||||
cancelButton.tintColor = Colors.text
|
||||
navigationItem.leftBarButtonItem = cancelButton
|
||||
let doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(handleSaveDisplayNameButtonTapped))
|
||||
doneButton.tintColor = Colors.text
|
||||
navigationItem.rightBarButtonItem = doneButton
|
||||
} else {
|
||||
let closeButton = UIBarButtonItem(image: #imageLiteral(resourceName: "X"), style: .plain, target: self, action: #selector(close))
|
||||
closeButton.tintColor = Colors.text
|
||||
navigationItem.leftBarButtonItem = closeButton
|
||||
let qrCodeButton = UIBarButtonItem(image: #imageLiteral(resourceName: "QRCode"), style: .plain, target: self, action: #selector(showQRCode))
|
||||
qrCodeButton.tintColor = Colors.text
|
||||
navigationItem.rightBarButtonItem = qrCodeButton
|
||||
}
|
||||
}
|
||||
|
||||
func avatarDidChange(_ image: UIImage) {
|
||||
let maxSize = Int(kOWSProfileManager_MaxAvatarDiameter)
|
||||
profilePictureToBeUploaded = image.resizedImage(toFillPixelSize: CGSize(width: maxSize, height: maxSize))
|
||||
updateProfile(isUpdatingDisplayName: false, isUpdatingProfilePicture: true)
|
||||
}
|
||||
|
||||
func clearAvatar() {
|
||||
profilePictureToBeUploaded = nil
|
||||
updateProfile(isUpdatingDisplayName: false, isUpdatingProfilePicture: true)
|
||||
}
|
||||
|
||||
private func updateProfile(isUpdatingDisplayName: Bool, isUpdatingProfilePicture: Bool) {
|
||||
let displayName = displayNameToBeUploaded ?? OWSProfileManager.shared().profileName(forRecipientId: userHexEncodedPublicKey)
|
||||
let profilePicture = profilePictureToBeUploaded ?? OWSProfileManager.shared().profileAvatar(forRecipientId: userHexEncodedPublicKey)
|
||||
ModalActivityIndicatorViewController.present(fromViewController: navigationController!, canCancel: false) { [weak self] modalActivityIndicator in
|
||||
OWSProfileManager.shared().updateLocalProfileName(displayName, avatarImage: profilePicture, success: {
|
||||
DispatchQueue.main.async {
|
||||
modalActivityIndicator.dismiss {
|
||||
guard let self = self else { return }
|
||||
self.profilePictureView.update()
|
||||
self.displayNameLabel.text = displayName
|
||||
self.profilePictureToBeUploaded = nil
|
||||
self.displayNameToBeUploaded = nil
|
||||
}
|
||||
}
|
||||
}, failure: {
|
||||
DispatchQueue.main.async {
|
||||
modalActivityIndicator.dismiss {
|
||||
let alert = UIAlertController(title: NSLocalizedString("Couldn't Update Profile", comment: ""), message: NSLocalizedString("Please check your internet connection and try again", comment: ""), preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
|
||||
self?.present(alert, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Interaction
|
||||
@objc private func close() {
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@objc private func showQRCode() {
|
||||
let qrCodeVC = QRCodeVC()
|
||||
navigationController!.pushViewController(qrCodeVC, animated: true)
|
||||
}
|
||||
|
||||
@objc private func handleCancelDisplayNameEditingButtonTapped() {
|
||||
isEditingDisplayName = false
|
||||
}
|
||||
|
||||
@objc private func handleSaveDisplayNameButtonTapped() {
|
||||
func showError(title: String, message: String = "") {
|
||||
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
|
||||
presentAlert(alert)
|
||||
}
|
||||
let displayName = displayNameTextField.text!.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
|
||||
guard !displayName.isEmpty else {
|
||||
return showError(title: NSLocalizedString("Please pick a display name", comment: ""))
|
||||
}
|
||||
let allowedCharacters = CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_ ")
|
||||
let hasInvalidCharacters = !displayName.allSatisfy { $0.unicodeScalars.allSatisfy { allowedCharacters.contains($0) } }
|
||||
guard !hasInvalidCharacters else {
|
||||
return showError(title: NSLocalizedString("Please pick a display name that consists of only a-z, A-Z, 0-9 and _ characters", comment: ""))
|
||||
}
|
||||
guard !OWSProfileManager.shared().isProfileNameTooLong(displayName) else {
|
||||
return showError(title: NSLocalizedString("Please pick a shorter display name", comment: ""))
|
||||
}
|
||||
isEditingDisplayName = false
|
||||
displayNameToBeUploaded = displayName
|
||||
updateProfile(isUpdatingDisplayName: true, isUpdatingProfilePicture: false)
|
||||
}
|
||||
|
||||
@objc private func showEditProfilePictureUI() {
|
||||
profilePictureUtilities.showChangeAvatarUI()
|
||||
}
|
||||
|
||||
@objc private func showEditDisplayNameUI() {
|
||||
isEditingDisplayName = true
|
||||
}
|
||||
|
||||
@objc private func copyPublicKey() {
|
||||
UIPasteboard.general.string = userHexEncodedPublicKey
|
||||
copyButton.isUserInteractionEnabled = false
|
||||
UIView.transition(with: copyButton, duration: 0.25, options: .transitionCrossDissolve, animations: {
|
||||
self.copyButton.setTitle(NSLocalizedString("Copied", comment: ""), for: UIControl.State.normal)
|
||||
}, completion: nil)
|
||||
Timer.scheduledTimer(timeInterval: 4, target: self, selector: #selector(enableCopyButton), userInfo: nil, repeats: false)
|
||||
}
|
||||
|
||||
@objc private func sharePublicKey() {
|
||||
let shareVC = UIActivityViewController(activityItems: [ userHexEncodedPublicKey ], applicationActivities: nil)
|
||||
navigationController!.present(shareVC, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@objc private func showPrivacySettings() {
|
||||
let privacySettingsVC = PrivacySettingsTableViewController()
|
||||
navigationController!.pushViewController(privacySettingsVC, animated: true)
|
||||
}
|
||||
|
||||
@objc private func showNotificationSettings() {
|
||||
let notificationSettingsVC = NotificationSettingsViewController()
|
||||
navigationController!.pushViewController(notificationSettingsVC, animated: true)
|
||||
}
|
||||
|
||||
@objc private func showLinkedDevices() {
|
||||
let deviceLinksVC = DeviceLinksVC()
|
||||
navigationController!.pushViewController(deviceLinksVC, animated: true)
|
||||
}
|
||||
|
||||
@objc private func showSeed() {
|
||||
let seedModal = SeedModal()
|
||||
seedModal.modalPresentationStyle = .overFullScreen
|
||||
seedModal.modalTransitionStyle = .crossDissolve
|
||||
present(seedModal, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@objc private func clearAllData() {
|
||||
let nukeDataModal = NukeDataModal()
|
||||
nukeDataModal.modalPresentationStyle = .overFullScreen
|
||||
nukeDataModal.modalTransitionStyle = .crossDissolve
|
||||
present(nukeDataModal, animated: true, completion: nil)
|
||||
}
|
||||
}
|
|
@ -32,7 +32,7 @@ struct MessageActionBuilder {
|
|||
}
|
||||
|
||||
static func copyPublicKey(conversationViewItem: ConversationViewItem, delegate: MessageActionsDelegate) -> MenuAction {
|
||||
return MenuAction(image: #imageLiteral(resourceName: "table_ic_add_to_existing_contact"),
|
||||
return MenuAction(image: #imageLiteral(resourceName: "Key"),
|
||||
title: NSLocalizedString("Copy Public Key", comment: ""),
|
||||
subtitle: nil,
|
||||
block: { [weak delegate] _ in delegate?.copyPublicKey(for: conversationViewItem) }
|
||||
|
|
|
@ -541,9 +541,9 @@
|
|||
|
||||
- (void)showQRCode
|
||||
{
|
||||
LKQRCodeModal *qrCodeModal = [LKQRCodeModal new];
|
||||
qrCodeModal.modalPresentationStyle = UIModalPresentationOverFullScreen;
|
||||
[self presentViewController:qrCodeModal animated:YES completion:nil];
|
||||
// LKQRCodeModal *qrCodeModal = [LKQRCodeModal new];
|
||||
// qrCodeModal.modalPresentationStyle = UIModalPresentationOverFullScreen;
|
||||
// [self presentViewController:qrCodeModal animated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (void)showSeed
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue