Cleared out some legacy code, fixed a few bugs, got typing indicators and mentions working

Got mentions working again
Got typing indicators working again
Got the notification sound and preview preferences working
Fixed a few issues with attachment image loading
Fixed an issue where enum settings weren't getting stored correctly
This commit is contained in:
Morgan Pretty 2022-05-10 17:42:15 +10:00
parent 333849c32e
commit 06eef99766
48 changed files with 966 additions and 1733 deletions

View File

@ -335,8 +335,6 @@
C32C5CAD256DD1DF003C73A2 /* TSAccountManager.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB94255A581300E217F9 /* TSAccountManager.h */; settings = {ATTRIBUTES = (Public, ); }; };
C32C5CF0256DD3E4003C73A2 /* Storage+Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3F0A5FD255C988A007BE2A3 /* Storage+Shared.swift */; };
C32C5D19256DD493003C73A2 /* OWSLinkPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBA8255A581500E217F9 /* OWSLinkPreview.swift */; };
C32C5D23256DD4C0003C73A2 /* Mention.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA7E255A57FB00E217F9 /* Mention.swift */; };
C32C5D24256DD4C0003C73A2 /* MentionsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA81255A57FC00E217F9 /* MentionsManager.swift */; };
C32C5D83256DD5B6003C73A2 /* SSKKeychainStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBBC255A581600E217F9 /* SSKKeychainStorage.swift */; };
C32C5DBF256DD743003C73A2 /* ClosedGroupPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB34255A580B00E217F9 /* ClosedGroupPoller.swift */; };
C32C5DC0256DD743003C73A2 /* Poller.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB3A255A580B00E217F9 /* Poller.swift */; };
@ -409,7 +407,6 @@
C33FDC96255A582000E217F9 /* NSObject+Casting.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDADC255A580400E217F9 /* NSObject+Casting.h */; settings = {ATTRIBUTES = (Public, ); }; };
C33FDC98255A582000E217F9 /* SwiftSingletons.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDADE255A580400E217F9 /* SwiftSingletons.swift */; };
C33FDC9A255A582000E217F9 /* ByteParser.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAE0255A580400E217F9 /* ByteParser.m */; };
C33FDCA2255A582000E217F9 /* OWSMessageUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDAE8255A580500E217F9 /* OWSMessageUtils.h */; settings = {ATTRIBUTES = (Public, ); }; };
C33FDCC7255A582000E217F9 /* NSArray+OWS.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB0D255A580800E217F9 /* NSArray+OWS.m */; };
C33FDCD1255A582000E217F9 /* FunctionalUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB17255A580800E217F9 /* FunctionalUtil.m */; };
C33FDCD3255A582000E217F9 /* GroupUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB19255A580900E217F9 /* GroupUtilities.swift */; };
@ -427,7 +424,6 @@
C33FDD74255A582000E217F9 /* OWSPrimaryStorage+keyFromIntLong.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBBA255A581600E217F9 /* OWSPrimaryStorage+keyFromIntLong.h */; settings = {ATTRIBUTES = (Public, ); }; };
C33FDD7C255A582000E217F9 /* SSKAsserts.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBC2255A581700E217F9 /* SSKAsserts.h */; settings = {ATTRIBUTES = (Public, ); }; };
C33FDD8D255A582000E217F9 /* OWSSignalAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBD3255A581800E217F9 /* OWSSignalAddress.swift */; };
C33FDD91255A582000E217F9 /* OWSMessageUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBD7255A581900E217F9 /* OWSMessageUtils.m */; };
C33FDD92255A582000E217F9 /* SignalIOS.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBD8255A581900E217F9 /* SignalIOS.pb.swift */; };
C33FDDB0255A582000E217F9 /* NSURLSessionDataTask+StatusCode.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBF6255A581C00E217F9 /* NSURLSessionDataTask+StatusCode.h */; settings = {ATTRIBUTES = (Public, ); }; };
C33FDDB2255A582000E217F9 /* NSArray+OWS.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBF8255A581C00E217F9 /* NSArray+OWS.h */; settings = {ATTRIBUTES = (Public, ); }; };
@ -557,7 +553,6 @@
C38EF3EF255B6DF7007E1867 /* ThreadViewHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = C38EF3D1255B6DEE007E1867 /* ThreadViewHelper.m */; };
C38EF3F0255B6DF7007E1867 /* ThreadViewHelper.h in Headers */ = {isa = PBXBuildFile; fileRef = C38EF3D2255B6DEE007E1867 /* ThreadViewHelper.h */; settings = {ATTRIBUTES = (Public, ); }; };
C38EF3F2255B6DF7007E1867 /* DisappearingTimerConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF3D4255B6DEE007E1867 /* DisappearingTimerConfigurationView.swift */; };
C38EF3F4255B6DF7007E1867 /* ContactCellView.m in Sources */ = {isa = PBXBuildFile; fileRef = C38EF3D6255B6DEF007E1867 /* ContactCellView.m */; };
C38EF3F5255B6DF7007E1867 /* OWSTextField.h in Headers */ = {isa = PBXBuildFile; fileRef = C38EF3D7255B6DF0007E1867 /* OWSTextField.h */; settings = {ATTRIBUTES = (Public, ); }; };
C38EF3F6255B6DF7007E1867 /* OWSTextView.h in Headers */ = {isa = PBXBuildFile; fileRef = C38EF3D8255B6DF0007E1867 /* OWSTextView.h */; settings = {ATTRIBUTES = (Public, ); }; };
C38EF3F7255B6DF7007E1867 /* OWSNavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF3D9255B6DF1007E1867 /* OWSNavigationBar.swift */; };
@ -570,11 +565,8 @@
C38EF400255B6DF7007E1867 /* GalleryRailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF3E2255B6DF3007E1867 /* GalleryRailView.swift */; };
C38EF401255B6DF7007E1867 /* VideoPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF3E3255B6DF4007E1867 /* VideoPlayerView.swift */; };
C38EF402255B6DF7007E1867 /* CommonStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF3E4255B6DF4007E1867 /* CommonStrings.swift */; };
C38EF403255B6DF7007E1867 /* ContactCellView.h in Headers */ = {isa = PBXBuildFile; fileRef = C38EF3E5255B6DF4007E1867 /* ContactCellView.h */; settings = {ATTRIBUTES = (Public, ); }; };
C38EF404255B6DF7007E1867 /* ContactTableViewCell.h in Headers */ = {isa = PBXBuildFile; fileRef = C38EF3E6255B6DF4007E1867 /* ContactTableViewCell.h */; settings = {ATTRIBUTES = (Public, ); }; };
C38EF405255B6DF7007E1867 /* OWSButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF3E7255B6DF5007E1867 /* OWSButton.swift */; };
C38EF407255B6DF7007E1867 /* Toast.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF3E9255B6DF6007E1867 /* Toast.swift */; };
C38EF409255B6DF7007E1867 /* ContactTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = C38EF3EB255B6DF6007E1867 /* ContactTableViewCell.m */; };
C38EF40A255B6DF7007E1867 /* OWSFlatButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF3EC255B6DF6007E1867 /* OWSFlatButton.swift */; };
C38EF40B255B6DF7007E1867 /* TappableStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF3ED255B6DF6007E1867 /* TappableStackView.swift */; };
C38EF40C255B6DF7007E1867 /* GradientView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF3EE255B6DF6007E1867 /* GradientView.swift */; };
@ -729,6 +721,7 @@
FD09C5E428237209000CE219 /* MessageRequestsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09C5E328237209000CE219 /* MessageRequestsViewModel.swift */; };
FD09C5E628260FF9000CE219 /* MediaGalleryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09C5E528260FF9000CE219 /* MediaGalleryViewModel.swift */; };
FD09C5E828264937000CE219 /* MediaDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09C5E728264937000CE219 /* MediaDetailViewController.swift */; };
FD09C5EA282A1BB2000CE219 /* ThreadTypingIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09C5E9282A1BB2000CE219 /* ThreadTypingIndicator.swift */; };
FD17D79927F40AB800122BE0 /* _003_YDBToGRDBMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D79827F40AB800122BE0 /* _003_YDBToGRDBMigration.swift */; };
FD17D79C27F40B2E00122BE0 /* SMKLegacyModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D79B27F40B2E00122BE0 /* SMKLegacyModels.swift */; };
FD17D7A027F40CC800122BE0 /* _001_InitialSetupMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D79F27F40CC800122BE0 /* _001_InitialSetupMigration.swift */; };
@ -796,7 +789,6 @@
FDF0B7472804F0CE004C14C5 /* DisappearingMessagesJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B7462804F0CE004C14C5 /* DisappearingMessagesJob.swift */; };
FDF0B74928060D13004C14C5 /* QuotedReplyModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B74828060D13004C14C5 /* QuotedReplyModel.swift */; };
FDF0B74B28061F7A004C14C5 /* InteractionAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B74A28061F7A004C14C5 /* InteractionAttachment.swift */; };
FDF0B74D280664E9004C14C5 /* PersistableRecord+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B74C280664E9004C14C5 /* PersistableRecord+Utilities.swift */; };
FDF0B74F28079E5E004C14C5 /* SendReadReceiptsJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B74E28079E5E004C14C5 /* SendReadReceiptsJob.swift */; };
FDF0B7512807BA56004C14C5 /* NotificationsProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B7502807BA56004C14C5 /* NotificationsProtocol.swift */; };
FDF0B7552807C4BB004C14C5 /* SSKEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B7542807C4BB004C14C5 /* SSKEnvironment.swift */; };
@ -1339,8 +1331,6 @@
C33FDA73255A57FA00E217F9 /* ECKeyPair+Hexadecimal.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ECKeyPair+Hexadecimal.swift"; sourceTree = "<group>"; };
C33FDA79255A57FB00E217F9 /* TSGroupThread.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSGroupThread.h; sourceTree = "<group>"; };
C33FDA7A255A57FB00E217F9 /* NSRegularExpression+SSK.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSRegularExpression+SSK.swift"; sourceTree = "<group>"; };
C33FDA7E255A57FB00E217F9 /* Mention.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Mention.swift; sourceTree = "<group>"; };
C33FDA81255A57FC00E217F9 /* MentionsManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MentionsManager.swift; sourceTree = "<group>"; };
C33FDA86255A57FC00E217F9 /* OWSDisappearingMessagesFinder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSDisappearingMessagesFinder.m; sourceTree = "<group>"; };
C33FDA87255A57FC00E217F9 /* TypingIndicators.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TypingIndicators.swift; sourceTree = "<group>"; };
C33FDA88255A57FD00E217F9 /* YapDatabaseTransaction+OWS.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "YapDatabaseTransaction+OWS.h"; sourceTree = "<group>"; };
@ -1373,7 +1363,6 @@
C33FDAE0255A580400E217F9 /* ByteParser.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ByteParser.m; sourceTree = "<group>"; };
C33FDAE4255A580400E217F9 /* TSAttachmentStream.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSAttachmentStream.h; sourceTree = "<group>"; };
C33FDAE6255A580400E217F9 /* TSInteraction.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSInteraction.h; sourceTree = "<group>"; };
C33FDAE8255A580500E217F9 /* OWSMessageUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSMessageUtils.h; sourceTree = "<group>"; };
C33FDAEA255A580500E217F9 /* OWSBackupFragment.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBackupFragment.h; sourceTree = "<group>"; };
C33FDAEC255A580500E217F9 /* SignalRecipient.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SignalRecipient.h; sourceTree = "<group>"; };
C33FDAEF255A580500E217F9 /* NSData+Image.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSData+Image.m"; sourceTree = "<group>"; };
@ -1458,7 +1447,6 @@
C33FDBC2255A581700E217F9 /* SSKAsserts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SSKAsserts.h; sourceTree = "<group>"; };
C33FDBCA255A581700E217F9 /* LKGroupUtilities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LKGroupUtilities.h; sourceTree = "<group>"; };
C33FDBD3255A581800E217F9 /* OWSSignalAddress.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSSignalAddress.swift; sourceTree = "<group>"; };
C33FDBD7255A581900E217F9 /* OWSMessageUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSMessageUtils.m; sourceTree = "<group>"; };
C33FDBD8255A581900E217F9 /* SignalIOS.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignalIOS.pb.swift; sourceTree = "<group>"; };
C33FDBDE255A581900E217F9 /* PushNotificationAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PushNotificationAPI.swift; sourceTree = "<group>"; };
C33FDBE1255A581A00E217F9 /* LKGroupUtilities.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LKGroupUtilities.m; sourceTree = "<group>"; };
@ -1619,7 +1607,6 @@
C38EF3D1255B6DEE007E1867 /* ThreadViewHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ThreadViewHelper.m; path = SignalUtilitiesKit/Database/ThreadViewHelper.m; sourceTree = SOURCE_ROOT; };
C38EF3D2255B6DEE007E1867 /* ThreadViewHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ThreadViewHelper.h; path = SignalUtilitiesKit/Database/ThreadViewHelper.h; sourceTree = SOURCE_ROOT; };
C38EF3D4255B6DEE007E1867 /* DisappearingTimerConfigurationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = DisappearingTimerConfigurationView.swift; path = SignalUtilitiesKit/Messaging/DisappearingTimerConfigurationView.swift; sourceTree = SOURCE_ROOT; };
C38EF3D6255B6DEF007E1867 /* ContactCellView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ContactCellView.m; path = "SignalUtilitiesKit/To Do/ContactCellView.m"; sourceTree = SOURCE_ROOT; };
C38EF3D7255B6DF0007E1867 /* OWSTextField.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OWSTextField.h; path = "SignalUtilitiesKit/Shared Views/OWSTextField.h"; sourceTree = SOURCE_ROOT; };
C38EF3D8255B6DF0007E1867 /* OWSTextView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OWSTextView.h; path = "SignalUtilitiesKit/Shared Views/OWSTextView.h"; sourceTree = SOURCE_ROOT; };
C38EF3D9255B6DF1007E1867 /* OWSNavigationBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OWSNavigationBar.swift; path = "SignalUtilitiesKit/Shared Views/OWSNavigationBar.swift"; sourceTree = SOURCE_ROOT; };
@ -1632,11 +1619,8 @@
C38EF3E2255B6DF3007E1867 /* GalleryRailView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GalleryRailView.swift; path = "SignalUtilitiesKit/Shared Views/GalleryRailView.swift"; sourceTree = SOURCE_ROOT; };
C38EF3E3255B6DF4007E1867 /* VideoPlayerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = VideoPlayerView.swift; path = "SignalUtilitiesKit/Media Viewing & Editing/VideoPlayerView.swift"; sourceTree = SOURCE_ROOT; };
C38EF3E4255B6DF4007E1867 /* CommonStrings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CommonStrings.swift; path = SignalUtilitiesKit/Utilities/CommonStrings.swift; sourceTree = SOURCE_ROOT; };
C38EF3E5255B6DF4007E1867 /* ContactCellView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ContactCellView.h; path = "SignalUtilitiesKit/To Do/ContactCellView.h"; sourceTree = SOURCE_ROOT; };
C38EF3E6255B6DF4007E1867 /* ContactTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ContactTableViewCell.h; path = "SignalUtilitiesKit/To Do/ContactTableViewCell.h"; sourceTree = SOURCE_ROOT; };
C38EF3E7255B6DF5007E1867 /* OWSButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OWSButton.swift; path = "SignalUtilitiesKit/Shared Views/OWSButton.swift"; sourceTree = SOURCE_ROOT; };
C38EF3E9255B6DF6007E1867 /* Toast.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Toast.swift; path = "SignalUtilitiesKit/Shared Views/Toast.swift"; sourceTree = SOURCE_ROOT; };
C38EF3EB255B6DF6007E1867 /* ContactTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ContactTableViewCell.m; path = "SignalUtilitiesKit/To Do/ContactTableViewCell.m"; sourceTree = SOURCE_ROOT; };
C38EF3EC255B6DF6007E1867 /* OWSFlatButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OWSFlatButton.swift; path = "SignalUtilitiesKit/Shared Views/OWSFlatButton.swift"; sourceTree = SOURCE_ROOT; };
C38EF3ED255B6DF6007E1867 /* TappableStackView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TappableStackView.swift; path = "SignalUtilitiesKit/Shared Views/TappableStackView.swift"; sourceTree = SOURCE_ROOT; };
C38EF3EE255B6DF6007E1867 /* GradientView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GradientView.swift; path = "SignalUtilitiesKit/Shared Views/GradientView.swift"; sourceTree = SOURCE_ROOT; };
@ -1786,6 +1770,7 @@
FD09C5E328237209000CE219 /* MessageRequestsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestsViewModel.swift; sourceTree = "<group>"; };
FD09C5E528260FF9000CE219 /* MediaGalleryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaGalleryViewModel.swift; sourceTree = "<group>"; };
FD09C5E728264937000CE219 /* MediaDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaDetailViewController.swift; sourceTree = "<group>"; };
FD09C5E9282A1BB2000CE219 /* ThreadTypingIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadTypingIndicator.swift; sourceTree = "<group>"; };
FD17D79527F3E04600122BE0 /* _001_InitialSetupMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _001_InitialSetupMigration.swift; sourceTree = "<group>"; };
FD17D79827F40AB800122BE0 /* _003_YDBToGRDBMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _003_YDBToGRDBMigration.swift; sourceTree = "<group>"; };
FD17D79B27F40B2E00122BE0 /* SMKLegacyModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SMKLegacyModels.swift; sourceTree = "<group>"; };
@ -1851,7 +1836,6 @@
FDF0B7462804F0CE004C14C5 /* DisappearingMessagesJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisappearingMessagesJob.swift; sourceTree = "<group>"; };
FDF0B74828060D13004C14C5 /* QuotedReplyModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuotedReplyModel.swift; sourceTree = "<group>"; };
FDF0B74A28061F7A004C14C5 /* InteractionAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractionAttachment.swift; sourceTree = "<group>"; };
FDF0B74C280664E9004C14C5 /* PersistableRecord+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PersistableRecord+Utilities.swift"; sourceTree = "<group>"; };
FDF0B74E28079E5E004C14C5 /* SendReadReceiptsJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendReadReceiptsJob.swift; sourceTree = "<group>"; };
FDF0B7502807BA56004C14C5 /* NotificationsProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsProtocol.swift; sourceTree = "<group>"; };
FDF0B7542807C4BB004C14C5 /* SSKEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSKEnvironment.swift; sourceTree = "<group>"; };
@ -2521,7 +2505,6 @@
C3D9E3B52567685D0040E4F3 /* Attachments */,
B8F5F61925EDE4B0003BF8D4 /* Data Extraction */,
C32C5D22256DD496003C73A2 /* Link Previews */,
C32C5D2D256DD4C4003C73A2 /* Mentions */,
C379DC6825672B5E0002D4EB /* Notifications */,
C32C59F8256DB5A6003C73A2 /* Pollers */,
C32C5B1B256DC160003C73A2 /* Quotes */,
@ -2712,15 +2695,6 @@
path = "Link Previews";
sourceTree = "<group>";
};
C32C5D2D256DD4C4003C73A2 /* Mentions */ = {
isa = PBXGroup;
children = (
C33FDA7E255A57FB00E217F9 /* Mention.swift */,
C33FDA81255A57FC00E217F9 /* MentionsManager.swift */,
);
path = Mentions;
sourceTree = "<group>";
};
C331FF1C2558F9D300070591 /* SessionUIKit */ = {
isa = PBXGroup;
children = (
@ -3097,10 +3071,6 @@
isa = PBXGroup;
children = (
C33FDB19255A580900E217F9 /* GroupUtilities.swift */,
C38EF3E5255B6DF4007E1867 /* ContactCellView.h */,
C38EF3D6255B6DEF007E1867 /* ContactCellView.m */,
C38EF3E6255B6DF4007E1867 /* ContactTableViewCell.h */,
C38EF3EB255B6DF6007E1867 /* ContactTableViewCell.m */,
C38EF2D2255B6DAF007E1867 /* OWSProfileManager.h */,
C38EF2CF255B6DAE007E1867 /* OWSProfileManager.m */,
);
@ -3117,8 +3087,6 @@
C38EF2E4255B6DB9007E1867 /* FullTextSearcher.swift */,
C38EF2E9255B6DBA007E1867 /* OWSUnreadIndicator.h */,
C38EF2E3255B6DB9007E1867 /* OWSUnreadIndicator.m */,
C33FDAE8255A580500E217F9 /* OWSMessageUtils.h */,
C33FDBD7255A581900E217F9 /* OWSMessageUtils.m */,
);
path = Messaging;
sourceTree = "<group>";
@ -3598,6 +3566,7 @@
FD09799827FFC1A300936362 /* Attachment.swift */,
FD09799A27FFC82D00936362 /* Quote.swift */,
FDF0B73B27FFD3D6004C14C5 /* LinkPreview.swift */,
FD09C5E9282A1BB2000CE219 /* ThreadTypingIndicator.swift */,
FDE77F6A280FEB28002CFC5D /* ControlMessageProcessRecord.swift */,
);
path = Models;
@ -3687,7 +3656,6 @@
FD17D7C227F5204C00122BE0 /* Database+Utilities.swift */,
FD17D7C627F5207C00122BE0 /* DatabaseMigrator+Utilities.swift */,
FD17D7BC27F51F6900122BE0 /* GRDB+Notifications.swift */,
FDF0B74C280664E9004C14C5 /* PersistableRecord+Utilities.swift */,
FDF22210281B5E0B000A4995 /* TableRecord+Utilities.swift */,
FDF2220E281B55E6000A4995 /* QueryInterfaceRequest+Utilities.swift */,
);
@ -3838,11 +3806,9 @@
C33FDDD3255A582000E217F9 /* OWSQueues.h in Headers */,
C33FDC96255A582000E217F9 /* NSObject+Casting.h in Headers */,
C33FDDB3255A582000E217F9 /* OWSError.h in Headers */,
C38EF403255B6DF7007E1867 /* ContactCellView.h in Headers */,
C33FDD68255A582000E217F9 /* SignalAccount.h in Headers */,
C38EF35E255B6DCC007E1867 /* OWSViewController.h in Headers */,
C37F5396255B95BD002AEA92 /* OWSAnyTouchGestureRecognizer.h in Headers */,
C38EF404255B6DF7007E1867 /* ContactTableViewCell.h in Headers */,
C33FDDB2255A582000E217F9 /* NSArray+OWS.h in Headers */,
C38EF2D7255B6DAF007E1867 /* OWSProfileManager.h in Headers */,
C38EF367255B6DCC007E1867 /* OWSTableViewController.h in Headers */,
@ -3850,7 +3816,6 @@
C33FD9AF255A548A00E217F9 /* SignalUtilitiesKit.h in Headers */,
C33FDC50255A582000E217F9 /* OWSDispatch.h in Headers */,
C33FDD06255A582000E217F9 /* AppVersion.h in Headers */,
C33FDCA2255A582000E217F9 /* OWSMessageUtils.h in Headers */,
C38EF28F255B6D86007E1867 /* VersionMigrations.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -4657,7 +4622,6 @@
C33FDC29255A581F00E217F9 /* ReachabilityManager.swift in Sources */,
C38EF407255B6DF7007E1867 /* Toast.swift in Sources */,
C38EF38C255B6DD2007E1867 /* ApprovalRailCellView.swift in Sources */,
C38EF409255B6DF7007E1867 /* ContactTableViewCell.m in Sources */,
FD3C907327E8387300CD579F /* OpenGroupServerIdLookupMigration.swift in Sources */,
C38EF32A255B6DBF007E1867 /* UIUtil.m in Sources */,
C38EF2A6255B6D93007E1867 /* PlaceholderIcon.swift in Sources */,
@ -4705,7 +4669,6 @@
C33FDC9A255A582000E217F9 /* ByteParser.m in Sources */,
C33FDDC0255A582000E217F9 /* SignalAccount.m in Sources */,
C33FDC58255A582000E217F9 /* ReverseDispatchQueue.swift in Sources */,
C38EF3F4255B6DF7007E1867 /* ContactCellView.m in Sources */,
C33FDC78255A582000E217F9 /* TSConstants.m in Sources */,
C38EF324255B6DBF007E1867 /* Bench.swift in Sources */,
FDCDB8DE2810F73B00352A0C /* Differentiable+Utilities.swift in Sources */,
@ -4721,7 +4684,6 @@
FD88BADB27A750F200BBC442 /* MessageRequestsMigration.swift in Sources */,
C38EF326255B6DBF007E1867 /* ConversationStyle.swift in Sources */,
C38EF3B8255B6DE7007E1867 /* ImageEditorTextViewController.swift in Sources */,
C33FDD91255A582000E217F9 /* OWSMessageUtils.m in Sources */,
B8C2B332256376F000551B4D /* ThreadUtil.m in Sources */,
C38EF40B255B6DF7007E1867 /* TappableStackView.swift in Sources */,
C38EF31D255B6DBF007E1867 /* UIImage+OWS.swift in Sources */,
@ -4796,7 +4758,6 @@
FD09C5E2282212B3000CE219 /* JobDependencies.swift in Sources */,
FD09796727F6B0B600936362 /* Sodium+Conversion.swift in Sources */,
FD9004122818ABDC00ABAAF6 /* Job.swift in Sources */,
FDF0B74D280664E9004C14C5 /* PersistableRecord+Utilities.swift in Sources */,
FD09797927FAB7E800936362 /* ImageFormat.swift in Sources */,
C32C5DC9256DD935003C73A2 /* ProxiedContentDownloader.swift in Sources */,
C3D9E4C02567767F0040E4F3 /* DataSource.m in Sources */,
@ -4937,7 +4898,6 @@
C3A71D0B2558989C0043A11F /* MessageWrapper.swift in Sources */,
FDE77F6B280FEB28002CFC5D /* ControlMessageProcessRecord.swift in Sources */,
B8F5F60325EDE16F003BF8D4 /* DataExtractionNotification.swift in Sources */,
C32C5D24256DD4C0003C73A2 /* MentionsManager.swift in Sources */,
C3A71D1E25589AC30043A11F /* WebSocketProto.swift in Sources */,
C3C2A7852553AAF300C340D1 /* SessionProtos.pb.swift in Sources */,
B8566C63256F55930045A0B9 /* OWSLinkPreview+Conversion.swift in Sources */,
@ -5014,7 +4974,6 @@
C3A3A13C256E1B27004D228D /* OWSMediaGalleryFinder.m in Sources */,
FD09797027FA6FF300936362 /* Profile.swift in Sources */,
C32C5AAE256DBE8F003C73A2 /* TSInteraction.m in Sources */,
C32C5D23256DD4C0003C73A2 /* Mention.swift in Sources */,
C32C5FD6256E0346003C73A2 /* Notification+Thread.swift in Sources */,
FD09798B27FD1CFE00936362 /* Capability.swift in Sources */,
C3BBE0C72554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift in Sources */,
@ -5026,6 +4985,7 @@
C32C5A75256DBBCF003C73A2 /* TSAttachmentPointer+Conversion.swift in Sources */,
C32C5EBA256DE130003C73A2 /* OWSQuotedReplyModel.m in Sources */,
C32C59C4256DB41F003C73A2 /* TSContactThread.m in Sources */,
FD09C5EA282A1BB2000CE219 /* ThreadTypingIndicator.swift in Sources */,
FDF0B75E280AAF35004C14C5 /* Preferences.swift in Sources */,
C32C5AB0256DBE8F003C73A2 /* TSOutgoingMessage.m in Sources */,
B82A0C3826B9098200C1BCE3 /* MessageInvalidator.swift in Sources */,

View File

@ -1,98 +1,127 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
extension ContextMenuVC {
struct Action {
let icon: UIImage
let icon: UIImage?
let title: String
let work: () -> Void
static func reply(_ viewItem: ConversationViewItem, _ delegate: ContextMenuActionDelegate?) -> Action {
let title = NSLocalizedString("context_menu_reply", comment: "")
return Action(icon: UIImage(named: "ic_reply")!, title: title) { delegate?.reply(viewItem) }
static func reply(_ item: ConversationViewModel.Item, _ delegate: ContextMenuActionDelegate?) -> Action {
return Action(
icon: UIImage(named: "ic_reply"),
title: "context_menu_reply".localized()
) { delegate?.reply(item) }
}
static func copy(_ viewItem: ConversationViewItem, _ delegate: ContextMenuActionDelegate?) -> Action {
let title = NSLocalizedString("copy", comment: "")
return Action(icon: UIImage(named: "ic_copy")!, title: title) { delegate?.copy(viewItem) }
static func copy(_ item: ConversationViewModel.Item, _ delegate: ContextMenuActionDelegate?) -> Action {
return Action(
icon: UIImage(named: "ic_copy"),
title: "copy".localized()
) { delegate?.copy(item) }
}
static func copySessionID(_ viewItem: ConversationViewItem, _ delegate: ContextMenuActionDelegate?) -> Action {
let title = NSLocalizedString("vc_conversation_settings_copy_session_id_button_title", comment: "")
return Action(icon: UIImage(named: "ic_copy")!, title: title) { delegate?.copySessionID(viewItem) }
static func copySessionID(_ item: ConversationViewModel.Item, _ delegate: ContextMenuActionDelegate?) -> Action {
return Action(
icon: UIImage(named: "ic_copy"),
title: "vc_conversation_settings_copy_session_id_button_title".localized()
) { delegate?.copySessionID(item) }
}
static func delete(_ viewItem: ConversationViewItem, _ delegate: ContextMenuActionDelegate?) -> Action {
let title = NSLocalizedString("TXT_DELETE_TITLE", comment: "")
return Action(icon: UIImage(named: "ic_trash")!, title: title) { delegate?.delete(viewItem) }
static func delete(_ item: ConversationViewModel.Item, _ delegate: ContextMenuActionDelegate?) -> Action {
return Action(
icon: UIImage(named: "ic_trash"),
title: "TXT_DELETE_TITLE".localized()
) { delegate?.delete(item) }
}
static func save(_ viewItem: ConversationViewItem, _ delegate: ContextMenuActionDelegate?) -> Action {
let title = NSLocalizedString("context_menu_save", comment: "")
return Action(icon: UIImage(named: "ic_download")!, title: title) { delegate?.save(viewItem) }
static func save(_ item: ConversationViewModel.Item, _ delegate: ContextMenuActionDelegate?) -> Action {
return Action(
icon: UIImage(named: "ic_download"),
title: "context_menu_save".localized()
) { delegate?.save(item) }
}
static func ban(_ viewItem: ConversationViewItem, _ delegate: ContextMenuActionDelegate?) -> Action {
let title = NSLocalizedString("context_menu_ban_user", comment: "")
return Action(icon: UIImage(named: "ic_block")!, title: title) { delegate?.ban(viewItem) }
static func ban(_ item: ConversationViewModel.Item, _ delegate: ContextMenuActionDelegate?) -> Action {
return Action(
icon: UIImage(named: "ic_block"),
title: "context_menu_ban_user".localized()
) { delegate?.ban(item) }
}
static func banAndDeleteAllMessages(_ viewItem: ConversationViewItem, _ delegate: ContextMenuActionDelegate?) -> Action {
let title = NSLocalizedString("context_menu_ban_and_delete_all", comment: "")
return Action(icon: UIImage(named: "ic_block")!, title: title) { delegate?.banAndDeleteAllMessages(viewItem) }
static func banAndDeleteAllMessages(_ item: ConversationViewModel.Item, _ delegate: ContextMenuActionDelegate?) -> Action {
return Action(
icon: UIImage(named: "ic_block"),
title: "context_menu_ban_and_delete_all".localized()
) { delegate?.banAndDeleteAllMessages(item) }
}
}
static func actions(for viewItem: ConversationViewItem, delegate: ContextMenuActionDelegate?) -> [Action] {
func isReplyingAllowed() -> Bool {
guard let message = viewItem.interaction as? TSOutgoingMessage else { return true }
switch message.messageState {
case .failed, .sending: return false
default: return true
}
}
switch viewItem.messageCellType {
case .textOnlyMessage:
var result: [Action] = []
if isReplyingAllowed() { result.append(Action.reply(viewItem, delegate)) }
result.append(Action.copy(viewItem, delegate))
let isGroup = viewItem.isGroupThread
if let message = viewItem.interaction as? TSIncomingMessage, isGroup, !message.isOpenGroupMessage {
result.append(Action.copySessionID(viewItem, delegate))
}
if !isGroup || viewItem.userCanDeleteGroupMessage { result.append(Action.delete(viewItem, delegate)) }
if isGroup && viewItem.interaction is TSIncomingMessage && viewItem.userHasModerationPermission {
result.append(Action.ban(viewItem, delegate))
result.append(Action.banAndDeleteAllMessages(viewItem, delegate))
}
return result
case .mediaMessage, .audio, .genericAttachment:
var result: [Action] = []
if isReplyingAllowed() { result.append(Action.reply(viewItem, delegate)) }
if viewItem.canCopyMedia() { result.append(Action.copy(viewItem, delegate)) }
if viewItem.canSaveMedia() { result.append(Action.save(viewItem, delegate)) }
let isGroup = viewItem.isGroupThread
if let message = viewItem.interaction as? TSIncomingMessage, isGroup, !message.isOpenGroupMessage {
result.append(Action.copySessionID(viewItem, delegate))
}
if !isGroup || viewItem.userCanDeleteGroupMessage { result.append(Action.delete(viewItem, delegate)) }
if isGroup && viewItem.interaction is TSIncomingMessage && viewItem.userHasModerationPermission {
result.append(Action.ban(viewItem, delegate))
result.append(Action.banAndDeleteAllMessages(viewItem, delegate))
}
return result
default: return []
static func actions(for item: ConversationViewModel.Item, currentUserIsOpenGroupModerator: Bool, delegate: ContextMenuActionDelegate?) -> [Action]? {
// No context items for info messages
guard item.interactionVariant == .standardOutgoing || item.interactionVariant == .standardIncoming else {
return nil
}
let canReply: Bool = (
item.interactionVariant != .standardOutgoing || (
item.state != .failed &&
item.state != .sending
)
)
let canCopy: Bool = (
item.cellType == .textOnlyMessage || (
(
item.cellType == .genericAttachment ||
item.cellType == .mediaMessage
) &&
(item.attachments ?? []).count == 1 &&
(item.attachments ?? []).first?.isVisualMedia == true &&
(item.attachments ?? []).first?.isValid == true && (
(item.attachments ?? []).first?.state == .downloaded ||
(item.attachments ?? []).first?.state == .uploaded
)
)
)
let canSave: Bool = (
item.cellType != .textOnlyMessage &&
canCopy
)
let canCopySessionId: Bool = (
item.interactionVariant == .standardIncoming &&
item.threadVariant != .openGroup
)
let canDelete: Bool = (
item.threadVariant != .openGroup ||
currentUserIsOpenGroupModerator
)
let canBan: Bool = (
item.threadVariant == .openGroup &&
currentUserIsOpenGroupModerator
)
return [
(canReply ? Action.reply(item, delegate) : nil),
(canCopy ? Action.copy(item, delegate) : nil),
(canSave ? Action.save(item, delegate) : nil),
(canCopySessionId ? Action.copySessionID(item, delegate) : nil),
(canDelete ? Action.delete(item, delegate) : nil),
(canBan ? Action.ban(item, delegate) : nil),
(canBan ? Action.banAndDeleteAllMessages(item, delegate) : nil)
]
.compactMap { $0 }
}
}
// MARK: Delegate
protocol ContextMenuActionDelegate : AnyObject {
func reply(_ viewItem: ConversationViewItem)
func copy(_ viewItem: ConversationViewItem)
func copySessionID(_ viewItem: ConversationViewItem)
func delete(_ viewItem: ConversationViewItem)
func save(_ viewItem: ConversationViewItem)
func ban(_ viewItem: ConversationViewItem)
func banAndDeleteAllMessages(_ viewItem: ConversationViewItem)
// MARK: - Delegate
protocol ContextMenuActionDelegate {
func reply(_ item: ConversationViewModel.Item)
func copy(_ item: ConversationViewModel.Item)
func copySessionID(_ item: ConversationViewModel.Item)
func delete(_ item: ConversationViewModel.Item)
func save(_ item: ConversationViewModel.Item)
func ban(_ item: ConversationViewModel.Item)
func banAndDeleteAllMessages(_ item: ConversationViewModel.Item)
}

View File

@ -1,19 +1,25 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import SessionUIKit
import SessionUtilitiesKit
extension ContextMenuVC {
final class ActionView : UIView {
private let action: Action
private let dismiss: () -> Void
// MARK: Settings
final class ActionView: UIView {
private static let iconSize: CGFloat = 16
private static let iconImageViewSize: CGFloat = 24
// MARK: Lifecycle
private let action: Action
private let dismiss: () -> Void
// MARK: - Lifecycle
init(for action: Action, dismiss: @escaping () -> Void) {
self.action = action
self.dismiss = dismiss
super.init(frame: CGRect.zero)
setUpViewHierarchy()
}
@ -28,32 +34,46 @@ extension ContextMenuVC {
private func setUpViewHierarchy() {
// Icon
let iconSize = ActionView.iconSize
let iconImageView = UIImageView(image: action.icon.resizedImage(to: CGSize(width: iconSize, height: iconSize))!.withTint(Colors.text))
let iconImageViewSize = ActionView.iconImageViewSize
iconImageView.set(.width, to: iconImageViewSize)
iconImageView.set(.height, to: iconImageViewSize)
let iconImageView: UIImageView = UIImageView(
image: action.icon?
.resizedImage(to: CGSize(width: iconSize, height: iconSize))?
.withRenderingMode(.alwaysTemplate)
)
iconImageView.set(.width, to: ActionView.iconImageViewSize)
iconImageView.set(.height, to: ActionView.iconImageViewSize)
iconImageView.contentMode = .center
iconImageView.tintColor = Colors.text
// Title
let titleLabel = UILabel()
titleLabel.text = action.title
titleLabel.textColor = Colors.text
titleLabel.font = .systemFont(ofSize: Values.mediumFontSize)
// Stack view
let stackView = UIStackView(arrangedSubviews: [ iconImageView, titleLabel ])
let stackView: UIStackView = UIStackView(arrangedSubviews: [ iconImageView, titleLabel ])
stackView.axis = .horizontal
stackView.spacing = Values.smallSpacing
stackView.alignment = .center
stackView.isLayoutMarginsRelativeArrangement = true
let smallSpacing = Values.smallSpacing
stackView.layoutMargins = UIEdgeInsets(top: smallSpacing, leading: smallSpacing, bottom: smallSpacing, trailing: Values.mediumSpacing)
stackView.layoutMargins = UIEdgeInsets(
top: smallSpacing,
leading: smallSpacing,
bottom: smallSpacing,
trailing: Values.mediumSpacing
)
addSubview(stackView)
stackView.pin(to: self)
// Tap gesture recognizer
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap))
addGestureRecognizer(tapGestureRecognizer)
}
// MARK: Interaction
// MARK: - Interaction
@objc private func handleTap() {
action.work()
dismiss()

View File

@ -1,43 +1,59 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
final class ContextMenuVC : UIViewController {
import UIKit
import SessionUIKit
final class ContextMenuVC: UIViewController {
private static let actionViewHeight: CGFloat = 40
private static let menuCornerRadius: CGFloat = 8
private let snapshot: UIView
private let viewItem: ConversationViewItem
private let frame: CGRect
private let item: ConversationViewModel.Item
private let actions: [Action]
private let dismiss: () -> Void
private weak var delegate: ContextMenuActionDelegate?
// MARK: UI Components
private lazy var blurView = UIVisualEffectView(effect: nil)
// MARK: - UI
private lazy var blurView: UIVisualEffectView = UIVisualEffectView(effect: nil)
private lazy var menuView: UIView = {
let result = UIView()
let result: UIView = UIView()
result.layer.shadowColor = UIColor.black.cgColor
result.layer.shadowOffset = CGSize.zero
result.layer.shadowOpacity = 0.4
result.layer.shadowRadius = 4
return result
}()
private lazy var timestampLabel: UILabel = {
let result = UILabel()
let date = viewItem.interaction.dateForUI()
result.text = DateUtil.formatDate(forDisplay: date)
let result: UILabel = UILabel()
result.font = .systemFont(ofSize: Values.verySmallFontSize)
result.textColor = isLightMode ? .black : .white
result.textColor = (isLightMode ? .black : .white)
if let dateForUI: Date = item.dateForUI {
result.text = DateUtil.formatDate(forDisplay: dateForUI)
}
return result
}()
// MARK: Settings
private static let actionViewHeight: CGFloat = 40
private static let menuCornerRadius: CGFloat = 8
// MARK: Lifecycle
init(snapshot: UIView, viewItem: ConversationViewItem, frame: CGRect, delegate: ContextMenuActionDelegate, dismiss: @escaping () -> Void) {
// MARK: - Initialization
init(
snapshot: UIView,
frame: CGRect,
item: ConversationViewModel.Item,
actions: [Action],
dismiss: @escaping () -> Void
) {
self.snapshot = snapshot
self.viewItem = viewItem
self.frame = frame
self.delegate = delegate
self.item = item
self.actions = actions
self.dismiss = dismiss
super.init(nibName: nil, bundle: nil)
}
@ -48,33 +64,42 @@ final class ContextMenuVC : UIViewController {
required init?(coder: NSCoder) {
preconditionFailure("Use init(coder:) instead.")
}
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
// Background color
view.backgroundColor = .clear
// Blur
view.addSubview(blurView)
blurView.pin(to: view)
// Snapshot
snapshot.layer.shadowColor = UIColor.black.cgColor
snapshot.layer.shadowOffset = CGSize.zero
snapshot.layer.shadowOpacity = 0.4
snapshot.layer.shadowRadius = 4
view.addSubview(snapshot)
snapshot.pin(.left, to: .left, of: view, withInset: frame.origin.x)
snapshot.pin(.top, to: .top, of: view, withInset: frame.origin.y)
snapshot.set(.width, to: frame.width)
snapshot.set(.height, to: frame.height)
// Timestamp
view.addSubview(timestampLabel)
timestampLabel.center(.vertical, in: snapshot)
let isOutgoing = (viewItem.interaction.interactionType() == .outgoingMessage)
if isOutgoing {
if item.interactionVariant == .standardOutgoing {
timestampLabel.pin(.right, to: .left, of: snapshot, withInset: -Values.smallSpacing)
} else {
}
else {
timestampLabel.pin(.left, to: .right, of: snapshot, withInset: Values.smallSpacing)
}
// Menu
let menuBackgroundView = UIView()
menuBackgroundView.backgroundColor = Colors.receivedMessageBackground
@ -82,25 +107,33 @@ final class ContextMenuVC : UIViewController {
menuBackgroundView.layer.masksToBounds = true
menuView.addSubview(menuBackgroundView)
menuBackgroundView.pin(to: menuView)
let actionViews = ContextMenuVC.actions(for: viewItem, delegate: delegate).map { ActionView(for: $0, dismiss: snDismiss) }
let menuStackView = UIStackView(arrangedSubviews: actionViews)
let menuStackView = UIStackView(
arrangedSubviews: actions
.map { action -> ActionView in ActionView(for: action, dismiss: snDismiss) }
)
menuStackView.axis = .vertical
menuView.addSubview(menuStackView)
menuStackView.pin(to: menuView)
view.addSubview(menuView)
let menuHeight = CGFloat(actionViews.count) * ContextMenuVC.actionViewHeight
let menuHeight = (CGFloat(actions.count) * ContextMenuVC.actionViewHeight)
let spacing = Values.smallSpacing
let margin = max(UIApplication.shared.keyWindow!.safeAreaInsets.bottom, Values.mediumSpacing)
if frame.maxY + spacing + menuHeight > UIScreen.main.bounds.height - margin {
menuView.pin(.bottom, to: .top, of: snapshot, withInset: -spacing)
} else {
}
else {
menuView.pin(.top, to: .bottom, of: snapshot, withInset: spacing)
}
switch viewItem.interaction.interactionType() {
case .outgoingMessage: menuView.pin(.right, to: .right, of: snapshot)
case .incomingMessage: menuView.pin(.left, to: .left, of: snapshot)
default: break // Should never occur
switch item.interactionVariant {
case .standardOutgoing: menuView.pin(.right, to: .right, of: snapshot)
case .standardIncoming: menuView.pin(.left, to: .left, of: snapshot)
default: break // Should never occur
}
// Tap gesture
let mainTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap))
view.addGestureRecognizer(mainTapGestureRecognizer)
@ -108,30 +141,41 @@ final class ContextMenuVC : UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
UIView.animate(withDuration: 0.25) {
self.blurView.effect = UIBlurEffect(style: .regular)
self.menuView.alpha = 1
}
}
// MARK: Updating
// MARK: - Layout
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
menuView.layer.shadowPath = UIBezierPath(roundedRect: menuView.bounds, cornerRadius: ContextMenuVC.menuCornerRadius).cgPath
menuView.layer.shadowPath = UIBezierPath(
roundedRect: menuView.bounds,
cornerRadius: ContextMenuVC.menuCornerRadius
).cgPath
}
// MARK: Interaction
// MARK: - Interaction
@objc private func handleTap() {
snDismiss()
}
func snDismiss() {
UIView.animate(withDuration: 0.25, animations: {
self.blurView.effect = nil
self.menuView.alpha = 0
self.timestampLabel.alpha = 0
}, completion: { _ in
self.dismiss()
})
UIView.animate(
withDuration: 0.25,
animations: { [weak self] in
self?.blurView.effect = nil
self?.menuView.alpha = 0
self?.timestampLabel.alpha = 0
},
completion: { [weak self] _ in
self?.dismiss()
}
)
}
}

View File

@ -12,6 +12,7 @@ import SignalUtilitiesKit
extension ConversationVC:
InputViewDelegate,
MessageCellDelegate,
ContextMenuActionDelegate,
ScrollToBottomButtonDelegate,
SendMediaNavDelegate,
UIDocumentPickerDelegate,
@ -50,31 +51,23 @@ extension ConversationVC:
// MARK: - Blocking
@objc func unblock() {
guard let thread = thread as? TSContactThread else { return }
let publicKey = thread.contactSessionID()
guard self.viewModel.viewData.thread.variant == .contact else { return }
let publicKey: String = self.viewModel.viewData.thread.id
UIView.animate(
withDuration: 0.25,
animations: {
self.blockedBanner.alpha = 0
},
completion: { _ in
GRDBStorage.shared.writeAsync(
updates: { db in
try Contact
.fetchOne(db, id: publicKey)?
.with(isBlocked: false)
.update(db)
},
completion: { db, result in
switch result {
case .success:
MessageSender.syncConfiguration(db, forceSyncNow: true).retainUntilComplete()
default: break
}
}
)
GRDBStorage.shared.write { db in
try Contact
.filter(id: publicKey)
.updateAll(db, Contact.Columns.isBlocked.set(to: true))
try MessageSender.syncConfiguration(db, forceSyncNow: true).retainUntilComplete()
}
}
)
}
@ -484,15 +477,6 @@ extension ConversationVC:
}
}
// MARK: Input View
func inputTextViewDidChangeContent(_ inputTextView: InputTextView) {
let newText = inputTextView.text ?? ""
if !newText.isEmpty {
SSKEnvironment.shared.typingIndicators.didStartTypingOutgoingInput(inThread: thread)
}
updateMentions(for: newText)
}
func showLinkPreviewSuggestionModal() {
let linkPreviewModel = LinkPreviewModal() { [weak self] in
self?.snInputView.autoGenerateLinkPreview()
@ -501,45 +485,82 @@ extension ConversationVC:
linkPreviewModel.modalTransitionStyle = .crossDissolve
present(linkPreviewModel, animated: true, completion: nil)
}
// MARK: Mentions
func updateMentions(for newText: String) {
if newText.count < oldText.count {
currentMentionStartIndex = nil
snInputView.hideMentionsUI()
mentions = mentions.filter { $0.isContained(in: newText) }
func inputTextViewDidChangeContent(_ inputTextView: InputTextView) {
let newText: String = (inputTextView.text ?? "")
if !newText.isEmpty {
}
updateMentions(for: newText)
}
// MARK: --Attachments
func didPasteImageFromPasteboard(_ image: UIImage) {
guard let imageData = image.jpegData(compressionQuality: 1.0) else { return }
let dataSource = DataSourceValue.dataSource(with: imageData, utiType: kUTTypeJPEG as String)
let attachment = SignalAttachment.attachment(dataSource: dataSource, dataUTI: kUTTypeJPEG as String, imageQuality: .medium)
let approvalVC = AttachmentApprovalViewController.wrappedInNavController(attachments: [ attachment ], approvalDelegate: self)
approvalVC.modalPresentationStyle = .fullScreen
self.present(approvalVC, animated: true, completion: nil)
}
// MARK: --Mentions
func handleMentionSelected(_ mentionInfo: ConversationViewModel.MentionInfo, from view: MentionSelectionView) {
guard let currentMentionStartIndex = currentMentionStartIndex else { return }
mentions.append(mentionInfo)
let newText: String = snInputView.text.replacingCharacters(
in: currentMentionStartIndex...,
with: "@\(mentionInfo.profile.displayName(for: self.viewModel.viewData.thread.variant)) "
)
snInputView.text = newText
self.currentMentionStartIndex = nil
snInputView.hideMentionsUI()
mentions = mentions.filter { mentionInfo -> Bool in
newText.contains(mentionInfo.profile.displayName(for: self.viewModel.viewData.thread.variant))
}
}
func updateMentions(for newText: String) {
if !newText.isEmpty {
let lastCharacterIndex = newText.index(before: newText.endIndex)
let lastCharacter = newText[lastCharacterIndex]
// Check if there is whitespace before the '@' or the '@' is the first character
let isCharacterBeforeLastWhiteSpaceOrStartOfLine: Bool
if newText.count == 1 {
isCharacterBeforeLastWhiteSpaceOrStartOfLine = true // Start of line
} else {
}
else {
let characterBeforeLast = newText[newText.index(before: lastCharacterIndex)]
isCharacterBeforeLastWhiteSpaceOrStartOfLine = characterBeforeLast.isWhitespace
}
if lastCharacter == "@" && isCharacterBeforeLastWhiteSpaceOrStartOfLine {
let candidates = MentionsManager.getMentionCandidates(for: "", in: thread.uniqueId!)
currentMentionStartIndex = lastCharacterIndex
snInputView.showMentionsUI(for: candidates, in: thread)
} else if lastCharacter.isWhitespace || lastCharacter == "@" { // the lastCharacter == "@" is to check for @@
snInputView.showMentionsUI(for: self.viewModel.mentions())
}
else if lastCharacter.isWhitespace || lastCharacter == "@" { // the lastCharacter == "@" is to check for @@
currentMentionStartIndex = nil
snInputView.hideMentionsUI()
} else {
}
else {
if let currentMentionStartIndex = currentMentionStartIndex {
let query = String(newText[newText.index(after: currentMentionStartIndex)...]) // + 1 to get rid of the @
let candidates = MentionsManager.getMentionCandidates(for: query, in: thread.uniqueId!)
snInputView.showMentionsUI(for: candidates, in: thread)
snInputView.showMentionsUI(for: self.viewModel.mentions(for: query))
}
}
}
oldText = newText
}
func resetMentions() {
oldText = ""
currentMentionStartIndex = nil
mentions = []
}
@ -554,33 +575,11 @@ extension ConversationVC:
func replaceMentions(in text: String) -> String {
var result = text
for mention in mentions {
guard let range = result.range(of: "@\(mention.displayName)") else { continue }
result = result.replacingCharacters(in: range, with: "@\(mention.publicKey)")
guard let range = result.range(of: "@\(mention.profile.displayName(for: mention.threadVariant))") else { continue }
result = result.replacingCharacters(in: range, with: "@\(mention.profile.id)")
}
return result
let approvalVC = AttachmentApprovalViewController.wrappedInNavController(attachments: [ attachment ], approvalDelegate: self)
approvalVC.modalPresentationStyle = .fullScreen
self.present(approvalVC, animated: true, completion: nil)
}
// MARK: --Mentions
func handleMentionSelected(_ mention: Mention, from view: MentionSelectionView) {
guard let currentMentionStartIndex = currentMentionStartIndex else { return }
mentions.append(mention)
let oldText = snInputView.text
let newText = oldText.replacingCharacters(in: currentMentionStartIndex..., with: "@\(mention.displayName) ")
snInputView.text = newText
self.currentMentionStartIndex = nil
snInputView.hideMentionsUI()
self.oldText = newText
}
func showInputAccessoryView() {
UIView.animate(withDuration: 0.25, animations: {
self.inputAccessoryView?.isHidden = false
self.inputAccessoryView?.alpha = 1
})
}
// MARK: View Item Interaction
@ -925,14 +924,15 @@ extension ConversationVC:
present(joinOpenGroupModal, animated: true, completion: nil)
}
func handleReplyButtonTapped(for viewItem: ConversationViewItem) {
reply(viewItem)
func handleReplyButtonTapped(for item: ConversationViewModel.Item) {
reply(item)
}
func showUserDetails(for sessionID: String) {
let userDetailsSheet = UserDetailsSheet(for: sessionID)
func showUserDetails(for profile: Profile) {
let userDetailsSheet = UserDetailsSheet(for: profile)
userDetailsSheet.modalPresentationStyle = .overFullScreen
userDetailsSheet.modalTransitionStyle = .crossDissolve
present(userDetailsSheet, animated: true, completion: nil)
}

View File

@ -39,9 +39,8 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
var contextMenuVC: ContextMenuVC?
// Mentions
var oldText = ""
var currentMentionStartIndex: String.Index?
var mentions: [Mention] = []
var mentions: [ConversationViewModel.MentionInfo] = []
// Scrolling & paging
var isUserScrolling = false
@ -321,6 +320,13 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
// Nav bar
setUpNavBarStyle()
navigationItem.titleView = titleView
titleView.update(
with: viewModel.viewData.threadName,
mutedUntilTimestamp: viewModel.viewData.thread.mutedUntilTimestamp,
onlyNotifyForMentions: viewModel.viewData.thread.onlyNotifyForMentions,
userCount: viewModel.viewData.userCount
)
updateNavBarButtons(viewData: viewModel.viewData)
// Constraints

View File

@ -1,8 +1,11 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import SessionUIKit
import SessionMessagingKit
final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate, QuoteViewDelegate, LinkPreviewViewDelegate, MentionSelectionViewDelegate {
enum MessageTypes {
final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, MentionSelectionViewDelegate {
enum MessageTypes: Equatable {
case all
case textOnly
case none
@ -29,23 +32,22 @@ final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate,
setEnabledMessageTypes(enabledMessageTypes, message: nil)
}
}
override var intrinsicContentSize: CGSize { CGSize.zero }
var lastSearchedText: String? { nil }
// MARK: UI Components
// MARK: - UI
private var bottomStackView: UIStackView?
private lazy var attachmentsButton = ExpandingAttachmentsButton(delegate: delegate)
private lazy var voiceMessageButton: InputViewButton = {
let result = InputViewButton(icon: #imageLiteral(resourceName: "Microphone"), delegate: self)
result.accessibilityLabel = NSLocalizedString("VOICE_MESSAGE_TOO_SHORT_ALERT_TITLE", comment: "")
result.accessibilityHint = NSLocalizedString("VOICE_MESSAGE_TOO_SHORT_ALERT_MESSAGE", comment: "")
return result
}()
private lazy var sendButton: InputViewButton = {
let result = InputViewButton(icon: #imageLiteral(resourceName: "ArrowUp"), isSendButton: true, delegate: self)
result.isHidden = true
@ -55,25 +57,28 @@ final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate,
private lazy var voiceMessageButtonContainer = container(for: voiceMessageButton)
private lazy var mentionsView: MentionSelectionView = {
let result = MentionSelectionView()
let result: MentionSelectionView = MentionSelectionView()
result.delegate = self
return result
}()
private lazy var mentionsViewContainer: UIView = {
let result = UIView()
let result: UIView = UIView()
let backgroundView = UIView()
backgroundView.backgroundColor = isLightMode ? .white : .black
backgroundView.backgroundColor = (isLightMode ? .white : .black)
backgroundView.alpha = Values.lowOpacity
result.addSubview(backgroundView)
backgroundView.pin(to: result)
let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .regular))
let blurView: UIVisualEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .regular))
result.addSubview(blurView)
blurView.pin(to: result)
result.alpha = 0
return result
}()
private lazy var inputTextView: InputTextView = {
// HACK: When restoring a draft the input text view won't have a frame yet, and therefore it won't
// be able to calculate what size it should be to accommodate the draft text. As a workaround, we
@ -83,7 +88,7 @@ final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate,
let maxWidth = UIScreen.main.bounds.width - 2 * InputViewButton.expandedSize - 2 * Values.smallSpacing - 2 * (Values.mediumSpacing - adjustment)
return InputTextView(delegate: self, maxWidth: maxWidth)
}()
private lazy var disabledInputLabel: UILabel = {
let label: UILabel = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false

View File

@ -519,8 +519,8 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, NewConve
}
@objc func joinOpenGroup() {
let joinOpenGroupVC = JoinOpenGroupVC()
let navigationController = OWSNavigationController(rootViewController: joinOpenGroupVC)
let joinOpenGroupVC: JoinOpenGroupVC = JoinOpenGroupVC()
let navigationController: OWSNavigationController = OWSNavigationController(rootViewController: joinOpenGroupVC)
present(navigationController, animated: true, completion: nil)
}

View File

@ -73,6 +73,7 @@ public class HomeViewModel {
}
}
fileprivate static let contactIsTypingKey = CodingKeys.contactIsTyping.stringValue
fileprivate static let closedGroupNameKey = CodingKeys.closedGroupName.stringValue
fileprivate static let openGroupNameKey = CodingKeys.openGroupName.stringValue
fileprivate static let openGroupProfilePictureDataKey = CodingKeys.openGroupProfilePictureData.stringValue
@ -92,6 +93,7 @@ public class HomeViewModel {
public let variant: SessionThread.Variant
private let creationDateTimestamp: TimeInterval
public let contactIsTyping: Bool
public let closedGroupName: String?
public let openGroupName: String?
public let openGroupProfilePictureData: Data?
@ -176,6 +178,7 @@ public class HomeViewModel {
self.id = "FALLBACK"
self.variant = .contact
self.creationDateTimestamp = 0
self.contactIsTyping = false
self.closedGroupName = nil
self.openGroupName = nil
self.openGroupProfilePictureData = nil
@ -198,6 +201,7 @@ public class HomeViewModel {
public static func query(userPublicKey: String) -> QueryInterfaceRequest<ThreadInfo> {
let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
let contact: TypedTableAlias<Contact> = TypedTableAlias()
let typingIndicator: TypedTableAlias<ThreadTypingIndicator> = TypedTableAlias()
let closedGroup: TypedTableAlias<ClosedGroup> = TypedTableAlias()
let closedGroupMember: TypedTableAlias<GroupMember> = TypedTableAlias()
let openGroup: TypedTableAlias<OpenGroup> = TypedTableAlias()
@ -262,12 +266,14 @@ public class HomeViewModel {
)
.group(Interaction.Columns.threadId) // One interaction per thread
)
return SessionThread
.select(
thread[.id],
thread[.variant],
thread[.creationDateTimestamp],
(typingIndicator[.threadId] != nil).forKey(ThreadInfo.contactIsTypingKey),
closedGroup[.name].forKey(ThreadInfo.closedGroupNameKey),
openGroup[.name].forKey(ThreadInfo.openGroupNameKey),
openGroup[.imageData].forKey(ThreadInfo.openGroupProfilePictureDataKey),
@ -291,6 +297,7 @@ public class HomeViewModel {
.forKey(ThreadInfo.contactProfileKey)
)
)
.joining(optional: SessionThread.typingIndicator.aliased(typingIndicator))
.joining(
optional: SessionThread.closedGroup
.aliased(closedGroup)

View File

@ -297,23 +297,34 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
CurrentAppContext().setMainAppBadgeNumber(
GRDBStorage.shared
.read({ db in
.read { db in
let userPublicKey: String = getUserHexEncodedPublicKey(db)
let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
// Don't increase the count for muted threads or message requests
return try Interaction
.filter(Interaction.Columns.wasRead == false)
.filter(
// Only count mentions if 'onlyNotifyForMentions' is set
thread[.onlyNotifyForMentions] == false ||
Interaction.Columns.hasMention == true
)
.joining(
required: Interaction.thread
.aliased(thread)
.joining(optional: SessionThread.contact)
.filter(SessionThread.Columns.notificationMode != SessionThread.NotificationMode.none)
.filter(
// Ignore muted threads
SessionThread.Columns.mutedUntilTimestamp == nil ||
SessionThread.Columns.mutedUntilTimestamp < Date().timeIntervalSince1970
)
.filter(
// Ignore message request threads
SessionThread.Columns.variant != SessionThread.Variant.contact ||
!SessionThread.isMessageRequest(userPublicKey: userPublicKey)
)
)
.fetchCount(db)
})
}
.defaulting(to: 0)
)
}

View File

@ -124,10 +124,6 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
return Environment.shared.preferences
}
var previewType: NotificationType {
return preferences.notificationPreviewType()
}
// MARK: -
@objc
@ -278,10 +274,12 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
public func notifyForFailedSend(_ db: Database, in thread: SessionThread) {
let notificationTitle: String?
let previewType: Preferences.NotificationPreviewType = db[.preferencesNotificationPreviewType]
.defaulting(to: .nameAndPreview)
switch previewType {
case .noNameNoPreview: notificationTitle = nil
case .nameNoPreview, .namePreview:
case .nameNoPreview, .nameAndPreview:
notificationTitle = SessionThread.displayName(
threadId: thread.id,
variant: thread.variant,
@ -296,8 +294,6 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
isNoteToSelf: (thread.isNoteToSelf(db) == true),
profile: try? Profile.fetchOne(db, id: thread.id)
)
default: notificationTitle = nil
}
let notificationBody = NotificationStrings.failedToSendBody
@ -411,12 +407,14 @@ class NotificationActionHandler {
}
let promise: Promise<Void> = GRDBStorage.shared.write { db in
let currentUserPublicKey: String = getUserHexEncodedPublicKey(db)
let interaction: Interaction = try Interaction(
threadId: thread.id,
authorId: getUserHexEncodedPublicKey(db),
variant: .standardOutgoing,
body: replyText,
timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000))
timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000)),
hasMention: replyText.contains("@\(currentUserPublicKey)")
).inserted(db)
try Interaction.markAsRead(

View File

@ -1,73 +1,82 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import GRDB
import SessionUIKit
import SessionMessagingKit
import SessionUtilitiesKit
final class JoinOpenGroupVC : BaseVC, UIPageViewControllerDataSource, UIPageViewControllerDelegate, OWSQRScannerDelegate {
final class JoinOpenGroupVC: BaseVC, 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: - Components
// MARK: Components
private lazy var tabBar: TabBar = {
let tabs = [
TabBar.Tab(title: NSLocalizedString("vc_join_public_chat_enter_group_url_tab_title", comment: "")) { [weak self] in
let tabs: [TabBar.Tab] = [
TabBar.Tab(title: "vc_join_public_chat_enter_group_url_tab_title".localized()) { [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("vc_join_public_chat_scan_qr_code_tab_title", comment: "")) { [weak self] in
TabBar.Tab(title: "vc_join_public_chat_scan_qr_code_tab_title".localized()) { [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 enterURLVC: EnterURLVC = {
let result = EnterURLVC()
let result: EnterURLVC = EnterURLVC()
result.joinOpenGroupVC = self
return result
}()
private lazy var scanQRCodePlaceholderVC: ScanQRCodePlaceholderVC = {
let result = ScanQRCodePlaceholderVC()
let result: ScanQRCodePlaceholderVC = ScanQRCodePlaceholderVC()
result.joinOpenGroupVC = self
return result
}()
private lazy var scanQRCodeWrapperVC: ScanQRCodeWrapperVC = {
let message = NSLocalizedString("vc_join_public_chat_scan_qr_code_explanation", comment: "")
let result = ScanQRCodeWrapperVC(message: message)
let result: ScanQRCodeWrapperVC = ScanQRCodeWrapperVC(message: "vc_join_public_chat_scan_qr_code_explanation".localized())
result.delegate = self
return result
}()
// MARK: Lifecycle
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
setUpGradientBackground()
setUpNavBarStyle()
setNavBarTitle(NSLocalizedString("vc_join_public_chat_title", comment: ""))
let navigationBar = navigationController!.navigationBar
setNavBarTitle("vc_join_public_chat_title".localized())
// Navigation bar buttons
let navBarHeight: CGFloat = (navigationController?.navigationBar.height() ?? 0)
let closeButton = UIBarButtonItem(image: #imageLiteral(resourceName: "X"), style: .plain, target: self, action: #selector(close))
closeButton.tintColor = Colors.text
navigationItem.leftBarButtonItem = closeButton
// Page VC
let hasCameraAccess = (AVCaptureDevice.authorizationStatus(for: .video) == .authorized)
pages = [ enterURLVC, (hasCameraAccess ? scanQRCodeWrapperVC : scanQRCodePlaceholderVC) ]
pageVC.dataSource = self
pageVC.delegate = self
pageVC.setViewControllers([ enterURLVC ], direction: .forward, animated: false, completion: nil)
// Tab bar
view.addSubview(tabBar)
tabBar.pin(.leading, to: .leading, of: view)
let tabBarInset: CGFloat
if #available(iOS 13, *) {
tabBarInset = navigationBar.height()
} else {
tabBarInset = 0
}
tabBar.pin(.top, to: .top, of: view, withInset: tabBarInset)
tabBar.pin(.top, to: .top, of: view, withInset: navBarHeight)
view.pin(.trailing, to: .trailing, of: tabBar)
// Page VC constraints
let pageVCView = pageVC.view!
view.addSubview(pageVCView)
@ -75,79 +84,85 @@ final class JoinOpenGroupVC : BaseVC, UIPageViewControllerDataSource, UIPageView
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
let height: CGFloat = ((navigationController?.view.bounds.height ?? 0) - navBarHeight - TabBar.snHeight)
pageVCView.set(.width, to: screen.width)
let height: CGFloat
if #available(iOS 13, *) {
height = navigationController!.view.bounds.height - navigationBar.height() - TabBar.snHeight
} else {
let statusBarHeight = UIApplication.shared.statusBarFrame.height
height = navigationController!.view.bounds.height - navigationBar.height() - TabBar.snHeight - statusBarHeight
}
pageVCView.set(.height, to: height)
enterURLVC.constrainHeight(to: height)
scanQRCodePlaceholderVC.constrainHeight(to: height)
}
// MARK: - General
// 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
// 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
// MARK: Interaction
@objc private func close() {
dismiss(animated: true, completion: nil)
}
func controller(_ controller: OWSQRCodeScanningViewController, didDetectQRCodeWith string: String) {
joinOpenGroup(with: string)
}
fileprivate func joinOpenGroup(with string: String) {
// A V2 open group URL will look like: <optional scheme> + <host> + <optional port> + <room> + <public key>
// The host doesn't parse if no explicit scheme is provided
if let (room, server, publicKey) = OpenGroupManagerV2.parseV2OpenGroup(from: string) {
joinV2OpenGroup(room: room, server: server, publicKey: publicKey)
} else {
let title = NSLocalizedString("invalid_url", comment: "")
let message = "Please check the URL you entered and try again."
showError(title: title, message: message)
guard let (room, server, publicKey) = OpenGroupManagerV2.parseV2OpenGroup(from: string) else {
showError(
title: "invalid_url".localized(),
message: "Please check the URL you entered and try again."
)
return
}
joinV2OpenGroup(room: room, server: server, publicKey: publicKey)
}
fileprivate func joinV2OpenGroup(room: String, server: String, publicKey: String) {
guard !isJoining else { return }
guard !isJoining, let navigationController: UINavigationController = navigationController else { return }
isJoining = true
ModalActivityIndicatorViewController.present(fromViewController: navigationController!, canCancel: false) { [weak self] _ in
Storage.shared.write { transaction in
OpenGroupManagerV2.shared.add(room: room, server: server, publicKey: publicKey, using: transaction)
ModalActivityIndicatorViewController.present(fromViewController: navigationController, canCancel: false) { [weak self] _ in
GRDBStorage.shared
.write { db in OpenGroupManagerV2.shared.add(db, room: room, server: server, publicKey: publicKey) }
.done(on: DispatchQueue.main) { [weak self] _ in
GRDBStorage.shared.write { db in
MessageSender.syncConfiguration(db, forceSyncNow: true).retainUntilComplete() // FIXME: It's probably cleaner to do this inside addOpenGroup(...)
try MessageSender.syncConfiguration(db, forceSyncNow: true).retainUntilComplete() // FIXME: It's probably cleaner to do this inside addOpenGroup(...)
}
self?.presentingViewController?.dismiss(animated: true, completion: nil)
}
.catch(on: DispatchQueue.main) { [weak self] error in
@ -157,22 +172,24 @@ final class JoinOpenGroupVC : BaseVC, UIPageViewControllerDataSource, UIPageView
self?.isJoining = false
self?.showError(title: title, message: message)
}
}
}
}
// MARK: Convenience
// MARK: - Convenience
private func showError(title: String, message: String = "") {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil))
let alert: UIAlertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "BUTTON_OK".localized(), style: .default, handler: nil))
presentAlert(alert)
}
}
private final class EnterURLVC : UIViewController, UIGestureRecognizerDelegate, OpenGroupSuggestionGridDelegate {
weak var joinOpenGroupVC: JoinOpenGroupVC!
private final class EnterURLVC: UIViewController, UIGestureRecognizerDelegate, OpenGroupSuggestionGridDelegate {
weak var joinOpenGroupVC: JoinOpenGroupVC?
// MARK: - UI
// MARK: Components
private lazy var urlTextView: TextView = {
let result = TextView(placeholder: NSLocalizedString("vc_enter_chat_url_text_field_hint", comment: ""))
result.keyboardType = .URL
@ -180,18 +197,21 @@ private final class EnterURLVC : UIViewController, UIGestureRecognizerDelegate,
result.autocorrectionType = .no
return result
}()
private lazy var suggestionGrid: OpenGroupSuggestionGrid = {
let maxWidth = UIScreen.main.bounds.width - Values.largeSpacing * 2
let result = OpenGroupSuggestionGrid(maxWidth: maxWidth)
let maxWidth: CGFloat = (UIScreen.main.bounds.width - Values.largeSpacing * 2)
let result: OpenGroupSuggestionGrid = OpenGroupSuggestionGrid(maxWidth: maxWidth)
result.delegate = self
return result
}()
// MARK: - Lifecycle
// MARK: Lifecycle
override func viewDidLoad() {
// Remove background color
view.backgroundColor = .clear
// Suggestion grid title label
let suggestionGridTitleLabel = UILabel()
suggestionGridTitleLabel.textColor = Colors.text
@ -199,16 +219,19 @@ private final class EnterURLVC : UIViewController, UIGestureRecognizerDelegate,
suggestionGridTitleLabel.text = NSLocalizedString("vc_join_open_group_suggestions_title", comment: "")
suggestionGridTitleLabel.numberOfLines = 0
suggestionGridTitleLabel.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(joinOpenGroup), 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)
// Stack view
let stackView = UIStackView(arrangedSubviews: [ urlTextView, UIView.spacer(withHeight: Values.mediumSpacing), suggestionGridTitleLabel,
UIView.spacer(withHeight: Values.mediumSpacing), suggestionGrid, UIView.vStretchingSpacer(), nextButtonContainer ])
@ -218,45 +241,52 @@ private final class EnterURLVC : UIViewController, UIGestureRecognizerDelegate,
stackView.isLayoutMarginsRelativeArrangement = true
view.addSubview(stackView)
stackView.pin(to: view)
// Constraints
view.set(.width, to: UIScreen.main.bounds.width)
// Dismiss keyboard on tap
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
tapGestureRecognizer.delegate = self
view.addGestureRecognizer(tapGestureRecognizer)
}
// MARK: - General
// MARK: General
func constrainHeight(to height: CGFloat) {
view.set(.height, to: height)
}
@objc private func dismissKeyboard() {
urlTextView.resignFirstResponder()
}
// MARK: - Interaction
// MARK: Interaction
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
let location = gestureRecognizer.location(in: view)
return !suggestionGrid.frame.contains(location)
}
func join(_ room: OpenGroupAPIV2.Info) {
joinOpenGroupVC.joinV2OpenGroup(room: room.id, server: OpenGroupAPIV2.defaultServer, publicKey: OpenGroupAPIV2.defaultServerPublicKey)
joinOpenGroupVC?.joinV2OpenGroup(room: room.id, server: OpenGroupAPIV2.defaultServer, publicKey: OpenGroupAPIV2.defaultServerPublicKey)
}
@objc private func joinOpenGroup() {
let url = urlTextView.text?.trimmingCharacters(in: .whitespaces) ?? ""
joinOpenGroupVC.joinOpenGroup(with: url)
joinOpenGroupVC?.joinOpenGroup(with: url)
}
}
private final class ScanQRCodePlaceholderVC : UIViewController {
weak var joinOpenGroupVC: JoinOpenGroupVC!
private final class ScanQRCodePlaceholderVC: UIViewController {
weak var joinOpenGroupVC: JoinOpenGroupVC?
// MARK: - Lifecycle
override func viewDidLoad() {
// Remove background color
view.backgroundColor = .clear
// Explanation label
let explanationLabel = UILabel()
explanationLabel.textColor = Colors.text
@ -265,34 +295,38 @@ private final class ScanQRCodePlaceholderVC : UIViewController {
explanationLabel.numberOfLines = 0
explanationLabel.textAlignment = .center
explanationLabel.lineBreakMode = .byWordWrapping
// 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("vc_scan_qr_code_grant_camera_access_button_title", comment: ""), for: UIControl.State.normal)
callToActionButton.addTarget(self, action: #selector(requestCameraAccess), for: UIControl.Event.touchUpInside)
// Stack view
let stackView = UIStackView(arrangedSubviews: [ explanationLabel, callToActionButton ])
stackView.axis = .vertical
stackView.spacing = Values.mediumSpacing
stackView.alignment = .center
// 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?.joinOpenGroupVC.handleCameraAccessGranted()
self?.joinOpenGroupVC?.handleCameraAccessGranted()
} else {
// Do nothing
}

View File

@ -30,27 +30,23 @@
OWSTableSection *section = [OWSTableSection new];
// section.footerTitle = NSLocalizedString(@"NOTIFICATIONS_FOOTER_WARNING", nil);
OWSPreferences *prefs = Environment.shared.preferences;
NotificationType selectedNotifType = [prefs notificationPreviewType];
for (NSNumber *option in
@[ @(NotificationNamePreview), @(NotificationNameNoPreview), @(NotificationNoNameNoPreview) ]) {
NotificationType notificationType = (NotificationType)option.intValue;
NSInteger selectedNotifType = [SMKPreferences notificationPreviewType];
for (NSNumber *option in [SMKPreferences notificationTypes]) {
[section addItem:[OWSTableItem
itemWithCustomCellBlock:^{
UITableViewCell *cell = [OWSTableItem newCell];
cell.tintColor = LKColors.accent;
[[cell textLabel] setText:[prefs nameForNotificationPreviewType:notificationType]];
if (selectedNotifType == notificationType) {
[[cell textLabel] setText:[SMKPreferences nameForNotificationPreviewType:option.intValue]];
if (selectedNotifType == option.intValue) {
cell.accessoryType = UITableViewCellAccessoryCheckmark;
}
cell.accessibilityIdentifier
= ACCESSIBILITY_IDENTIFIER_WITH_NAME(NotificationSettingsOptionsViewController,
NSStringForNotificationType(notificationType));
cell.accessibilityIdentifier = ACCESSIBILITY_IDENTIFIER_WITH_NAME(NotificationSettingsOptionsViewController, [SMKPreferences accessibilityIdentifierForNotificationPreviewType:option.intValue]);
return cell;
}
actionBlock:^{
[weakSelf setNotificationType:notificationType];
[SMKPreferences setNotificationPreviewType: option.intValue];
[weakSelf.navigationController popViewControllerAnimated:YES];
}]];
}
[contents addSection:section];
@ -58,11 +54,4 @@
self.contents = contents;
}
- (void)setNotificationType:(NotificationType)notificationType
{
[Environment.shared.preferences setNotificationPreviewType:notificationType];
[self.navigationController popViewControllerAnimated:YES];
}
@end

View File

@ -93,7 +93,7 @@
[backgroundSection
addItem:[OWSTableItem
disclosureItemWithText:NSLocalizedString(@"NOTIFICATIONS_SHOW", nil)
detailText:[prefs nameForNotificationPreviewType:[prefs notificationPreviewType]]
detailText:[SMKPreferences nameForNotificationPreviewType:[SMKPreferences notificationPreviewType]]
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"options")
actionBlock:^{
NotificationSettingsOptionsViewController *vc =

View File

@ -4,112 +4,122 @@ import UIKit
import SessionUIKit
import SignalUtilitiesKit
final class ConversationCell : UITableViewCell {
static let reuseIdentifier = "ConversationCell"
final class ConversationCell: UITableViewCell {
// MARK: - UI
private let accentLineView: UIView = UIView()
// MARK: UI Components
private let accentLineView = UIView()
private lazy var profilePictureView = ProfilePictureView()
private lazy var profilePictureView: ProfilePictureView = ProfilePictureView()
private lazy var displayNameLabel: UILabel = {
let result = UILabel()
let result: UILabel = UILabel()
result.font = .boldSystemFont(ofSize: Values.mediumFontSize)
result.textColor = Colors.text
result.lineBreakMode = .byTruncatingTail
return result
}()
private lazy var unreadCountView: UIView = {
let result = UIView()
let result: UIView = UIView()
result.backgroundColor = Colors.text.withAlphaComponent(Values.veryLowOpacity)
let size = ConversationCell.unreadCountViewSize
result.set(.width, greaterThanOrEqualTo: size)
result.set(.height, to: size)
result.layer.masksToBounds = true
result.layer.cornerRadius = size / 2
result.layer.cornerRadius = (size / 2)
return result
}()
private lazy var unreadCountLabel: UILabel = {
let result = UILabel()
let result: UILabel = UILabel()
result.font = .boldSystemFont(ofSize: Values.verySmallFontSize)
result.textColor = Colors.text
result.textAlignment = .center
return result
}()
private lazy var hasMentionView: UIView = {
let result = UIView()
let result: UIView = UIView()
result.backgroundColor = Colors.accent
let size = ConversationCell.unreadCountViewSize
result.set(.width, to: size)
result.set(.height, to: size)
result.layer.masksToBounds = true
result.layer.cornerRadius = size / 2
result.layer.cornerRadius = (size / 2)
return result
}()
private lazy var hasMentionLabel: UILabel = {
let result = UILabel()
let result: UILabel = UILabel()
result.font = .boldSystemFont(ofSize: Values.verySmallFontSize)
result.textColor = Colors.text
result.text = "@"
result.textAlignment = .center
return result
}()
private lazy var isPinnedIcon: UIImageView = {
let result = UIImageView(image: UIImage(named: "Pin")!.withRenderingMode(.alwaysTemplate))
let result: UIImageView = UIImageView(image: UIImage(named: "Pin")?.withRenderingMode(.alwaysTemplate))
result.contentMode = .scaleAspectFit
let size = ConversationCell.unreadCountViewSize
result.set(.width, to: size)
result.set(.height, to: size)
result.tintColor = Colors.pinIcon
result.layer.masksToBounds = true
return result
}()
private lazy var timestampLabel: UILabel = {
let result = UILabel()
let result: UILabel = UILabel()
result.font = .systemFont(ofSize: Values.smallFontSize)
result.textColor = Colors.text
result.lineBreakMode = .byTruncatingTail
result.alpha = Values.lowOpacity
return result
}()
private lazy var snippetLabel: UILabel = {
let result = UILabel()
let result: UILabel = UILabel()
result.font = .systemFont(ofSize: Values.smallFontSize)
result.textColor = Colors.text
result.lineBreakMode = .byTruncatingTail
return result
}()
private lazy var typingIndicatorView = TypingIndicatorView()
private lazy var statusIndicatorView: UIImageView = {
let result = UIImageView()
let result: UIImageView = UIImageView()
result.contentMode = .scaleAspectFit
result.layer.cornerRadius = ConversationCell.statusIndicatorSize / 2
result.layer.cornerRadius = (ConversationCell.statusIndicatorSize / 2)
result.layer.masksToBounds = true
return result
}()
private lazy var topLabelStackView: UIStackView = {
let result = UIStackView()
let result: UIStackView = UIStackView()
result.axis = .horizontal
result.alignment = .center
result.spacing = Values.smallSpacing / 2 // Effectively Values.smallSpacing because there'll be spacing before and after the invisible spacer
return result
}()
private lazy var bottomLabelStackView: UIStackView = {
let result = UIStackView()
let result: UIStackView = UIStackView()
result.axis = .horizontal
result.alignment = .center
result.spacing = Values.smallSpacing / 2 // Effectively Values.smallSpacing because there'll be spacing before and after the invisible spacer
return result
}()
@ -118,7 +128,8 @@ final class ConversationCell : UITableViewCell {
public static let unreadCountViewSize: CGFloat = 20
private static let statusIndicatorSize: CGFloat = 14
// MARK: Initialization
// MARK: - Initialization
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setUpViewHierarchy()
@ -131,69 +142,88 @@ final class ConversationCell : UITableViewCell {
private func setUpViewHierarchy() {
let cellHeight: CGFloat = 68
// Background color
backgroundColor = Colors.cellBackground
// Highlight color
let selectedBackgroundView = UIView()
selectedBackgroundView.backgroundColor = Colors.cellSelected
self.selectedBackgroundView = selectedBackgroundView
// Accent line view
accentLineView.set(.width, to: Values.accentLineThickness)
accentLineView.set(.height, to: cellHeight)
// Profile picture view
let profilePictureViewSize = Values.mediumProfilePictureSize
profilePictureView.set(.width, to: profilePictureViewSize)
profilePictureView.set(.height, to: profilePictureViewSize)
profilePictureView.size = profilePictureViewSize
// Unread count view
unreadCountView.addSubview(unreadCountLabel)
unreadCountLabel.pin([ VerticalEdge.top, VerticalEdge.bottom ], to: unreadCountView)
unreadCountView.pin(.leading, to: .leading, of: unreadCountLabel, withInset: -4)
unreadCountView.pin(.trailing, to: .trailing, of: unreadCountLabel, withInset: 4)
// Has mention view
hasMentionView.addSubview(hasMentionLabel)
hasMentionLabel.pin(to: hasMentionView)
// Label stack view
let topLabelSpacer = UIView.hStretchingSpacer()
[ displayNameLabel, isPinnedIcon, unreadCountView, hasMentionView, topLabelSpacer, timestampLabel ].forEach{ view in
topLabelStackView.addArrangedSubview(view)
}
let snippetLabelContainer = UIView()
snippetLabelContainer.addSubview(snippetLabel)
snippetLabelContainer.addSubview(typingIndicatorView)
let bottomLabelSpacer = UIView.hStretchingSpacer()
[ snippetLabelContainer, bottomLabelSpacer, statusIndicatorView ].forEach{ view in
bottomLabelStackView.addArrangedSubview(view)
}
let labelContainerView = UIStackView(arrangedSubviews: [ topLabelStackView, bottomLabelStackView ])
labelContainerView.axis = .vertical
labelContainerView.alignment = .leading
labelContainerView.spacing = 6
labelContainerView.isUserInteractionEnabled = false
// Main stack view
let stackView = UIStackView(arrangedSubviews: [ accentLineView, profilePictureView, labelContainerView ])
stackView.axis = .horizontal
stackView.alignment = .center
stackView.spacing = Values.mediumSpacing
contentView.addSubview(stackView)
// Constraints
accentLineView.pin(.top, to: .top, of: contentView)
accentLineView.pin(.bottom, to: .bottom, of: contentView)
timestampLabel.setContentCompressionResistancePriority(.required, for: NSLayoutConstraint.Axis.horizontal)
// HACK: The six lines below are part of a workaround for a weird layout bug
topLabelStackView.set(.width, to: UIScreen.main.bounds.width - Values.accentLineThickness - profilePictureViewSize - 3 * Values.mediumSpacing)
topLabelStackView.set(.height, to: 20)
topLabelSpacer.set(.height, to: 20)
bottomLabelStackView.set(.width, to: UIScreen.main.bounds.width - Values.accentLineThickness - profilePictureViewSize - 3 * Values.mediumSpacing)
bottomLabelStackView.set(.height, to: 18)
bottomLabelSpacer.set(.height, to: 18)
statusIndicatorView.set(.width, to: ConversationCell.statusIndicatorSize)
statusIndicatorView.set(.height, to: ConversationCell.statusIndicatorSize)
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)
// HACK: The two lines below are part of a workaround for a weird layout bug
stackView.set(.width, to: UIScreen.main.bounds.width - Values.mediumSpacing)
stackView.set(.height, to: cellHeight)
@ -286,7 +316,7 @@ final class ConversationCell : UITableViewCell {
}
private func getHighlightedSnippet(snippet: String, searchText: String, fontSize: CGFloat) -> NSMutableAttributedString {
guard snippet != NSLocalizedString("NOTE_TO_SELF", comment: "") else {
guard snippet != "NOTE_TO_SELF".localized() else {
return NSMutableAttributedString(string: snippet, attributes: [.foregroundColor:Colors.text])
}

View File

@ -12,8 +12,7 @@ final class UserSelectionVC: BaseVC, UITableViewDataSource, UITableViewDelegate
private lazy var users: [Profile] = {
return Profile
.fetchAllContactProfiles()
.filter { usersToExclude.contains($0.id) }
.fetchAllContactProfiles(excluding: usersToExclude)
}()
// MARK: - Components

View File

@ -287,5 +287,12 @@ enum _001_InitialSetupMigration: Migration {
t.uniqueKey([.threadId, .variant, .timestampMs])
}
try db.create(table: ThreadTypingIndicator.self) { t in
t.column(.threadId, .text)
.primaryKey()
.references(SessionThread.self, onDelete: .cascade) // Delete if thread deleted
t.column(.timestampMs, .integer).notNull()
}
}
}

View File

@ -1215,8 +1215,8 @@ enum _003_YDBToGRDBMigration: Migration {
legacyPreferences[key] = object
}
// Note: The 'int(forKey:inCollection:)' defaults to `0` which is an incorrect value for the notification
// sound so catch it and default
// Note: The 'int(forKey:inCollection:)' defaults to `0` which is an incorrect value
// for the notification sound so catch it and default
let globalNotificationSoundValue: Int32 = transaction.int(
forKey: Legacy.soundsGlobalNotificationKey,
inCollection: Legacy.soundsStorageNotificationCollection
@ -1226,17 +1226,17 @@ enum _003_YDBToGRDBMigration: Migration {
Preferences.Sound.defaultNotificationSound.rawValue
)
legacyPreferences[Legacy.readReceiptManagerAreReadReceiptsEnabled] = (transaction.bool(
legacyPreferences[Legacy.readReceiptManagerAreReadReceiptsEnabled] = transaction.bool(
forKey: Legacy.readReceiptManagerAreReadReceiptsEnabled,
inCollection: Legacy.readReceiptManagerCollection,
defaultValue: false
) ? 1 : 0)
)
legacyPreferences[Legacy.typingIndicatorsEnabledKey] = (transaction.bool(
legacyPreferences[Legacy.typingIndicatorsEnabledKey] = transaction.bool(
forKey: Legacy.typingIndicatorsEnabledKey,
inCollection: Legacy.typingIndicatorsCollection,
defaultValue: false
) ? 1 : 0)
)
}
db[.preferencesNotificationPreviewType] = Preferences.NotificationPreviewType(rawValue: legacyPreferences[Legacy.preferencesKeyNotificationPreviewType] as? Int ?? -1)
@ -1292,6 +1292,15 @@ enum _003_YDBToGRDBMigration: Migration {
return nil
}
let processedLocalRelativeFilePath: String? = (legacyAttachment as? Legacy.AttachmentStream)?
.localRelativeFilePath
.map { filePath -> String in
// The old 'localRelativeFilePath' seemed to have a leading forward slash (want
// to get rid of it so we can correctly use 'appendingPathComponent')
guard filePath.starts(with: "/") else { return filePath }
return String(filePath.suffix(from: filePath.index(after: filePath.startIndex)))
}
let state: Attachment.State = {
switch legacyAttachment {
case let stream as Legacy.AttachmentStream: // Outgoing or already downloaded
@ -1307,7 +1316,22 @@ enum _003_YDBToGRDBMigration: Migration {
let size: CGSize = {
switch legacyAttachment {
case let stream as Legacy.AttachmentStream:
guard let originalFilePath: String = Attachment.originalFilePath(id: legacyAttachmentId, mimeType: stream.contentType, sourceFilename: stream.localRelativeFilePath) else {
// First try to get an image size using the 'localRelativeFilePath' value
if
let localRelativeFilePath: String = processedLocalRelativeFilePath,
let specificImageSize: CGSize = Attachment.imageSize(
contentType: stream.contentType,
originalFilePath: URL(fileURLWithPath: Attachment.attachmentsFolder)
.appendingPathComponent(localRelativeFilePath)
.path
),
specificImageSize != .zero
{
return specificImageSize
}
// Then fallback to trying to get the size from the 'originalFilePath'
guard let originalFilePath: String = Attachment.originalFilePath(id: legacyAttachmentId, mimeType: stream.contentType, sourceFilename: stream.sourceFilename) else {
return .zero
}
@ -1328,7 +1352,7 @@ enum _003_YDBToGRDBMigration: Migration {
let originalFilePath: String = Attachment.originalFilePath(
id: legacyAttachmentId,
mimeType: stream.contentType,
sourceFilename: stream.localRelativeFilePath
sourceFilename: stream.sourceFilename
)
else {
return (false, nil)
@ -1341,6 +1365,7 @@ enum _003_YDBToGRDBMigration: Migration {
let (isValid, duration): (Bool, TimeInterval?) = Attachment.determineValidityAndDuration(
contentType: stream.contentType,
localRelativeFilePath: processedLocalRelativeFilePath,
originalFilePath: originalFilePath
)
@ -1376,9 +1401,11 @@ enum _003_YDBToGRDBMigration: Migration {
state: state,
contentType: legacyAttachment.contentType,
byteCount: UInt(legacyAttachment.byteCount),
creationTimestamp: (legacyAttachment as? Legacy.AttachmentStream)?.creationTimestamp.timeIntervalSince1970,
creationTimestamp: (legacyAttachment as? Legacy.AttachmentStream)?
.creationTimestamp.timeIntervalSince1970,
sourceFilename: legacyAttachment.sourceFilename,
downloadUrl: legacyAttachment.downloadURL,
localRelativeFilePath: processedLocalRelativeFilePath,
width: (size == .zero ? nil : UInt(size.width)),
height: (size == .zero ? nil : UInt(size.height)),
duration: duration,

View File

@ -179,6 +179,7 @@ public struct Attachment: Codable, Identifiable, Equatable, Hashable, FetchableR
)
let (isValid, duration): (Bool, TimeInterval?) = Attachment.determineValidityAndDuration(
contentType: contentType,
localRelativeFilePath: nil,
originalFilePath: originalFilePath
)
@ -191,7 +192,7 @@ public struct Attachment: Codable, Identifiable, Equatable, Hashable, FetchableR
self.creationTimestamp = nil
self.sourceFilename = nil
self.downloadUrl = nil
self.localRelativeFilePath = nil
self.localRelativeFilePath = URL(fileURLWithPath: originalFilePath).lastPathComponent
self.width = imageSize.map { UInt(floor($0.width)) }
self.height = imageSize.map { UInt(floor($0.height)) }
self.duration = duration
@ -282,6 +283,7 @@ public extension Attachment {
case (_, .downloaded):
return Attachment.determineValidityAndDuration(
contentType: contentType,
localRelativeFilePath: localRelativeFilePath,
originalFilePath: originalFilePath
)
@ -570,7 +572,11 @@ public extension Attachment {
)
}
internal static func determineValidityAndDuration(contentType: String, originalFilePath: String?) -> (isValid: Bool, duration: TimeInterval?) {
internal static func determineValidityAndDuration(
contentType: String,
localRelativeFilePath: String?,
originalFilePath: String?
) -> (isValid: Bool, duration: TimeInterval?) {
guard let originalFilePath: String = originalFilePath else { return (false, nil) }
// Process audio attachments
@ -593,8 +599,23 @@ public extension Attachment {
// Process image attachments
if MIMETypeUtil.isImage(contentType) {
let specificFilePathIsValid: Bool = (
localRelativeFilePath != nil &&
localRelativeFilePath.map {
NSData.ows_isValidImage(
atPath: URL(fileURLWithPath: Attachment.attachmentsFolder)
.appendingPathComponent($0)
.path,
mimeType: contentType
)
} == true
)
return (
NSData.ows_isValidImage(atPath: originalFilePath, mimeType: contentType),
(
specificFilePathIsValid ||
NSData.ows_isValidImage(atPath: originalFilePath, mimeType: contentType)
),
nil
)
}
@ -607,9 +628,22 @@ public extension Attachment {
// Accorting to the CMTime docs "value/timescale = seconds"
(TimeInterval(item.duration.value) / TimeInterval(item.duration.timescale))
}
let specificFilePathIsValid: Bool = (
localRelativeFilePath != nil &&
localRelativeFilePath.map {
OWSMediaUtils.isValidVideo(
path: URL(fileURLWithPath: Attachment.attachmentsFolder)
.appendingPathComponent($0)
.path
)
} == true
)
return (
OWSMediaUtils.isValidVideo(path: originalFilePath),
(
specificFilePathIsValid ||
OWSMediaUtils.isValidVideo(path: originalFilePath)
),
durationSeconds
)
}
@ -637,6 +671,12 @@ extension Attachment {
}
public var originalFilePath: String? {
if let localRelativeFilePath: String = self.localRelativeFilePath {
return URL(fileURLWithPath: Attachment.attachmentsFolder)
.appendingPathComponent(localRelativeFilePath)
.path
}
return Attachment.originalFilePath(
id: self.id,
mimeType: self.contentType,
@ -658,7 +698,7 @@ extension Attachment {
let fileUrl: URL = URL(fileURLWithPath: originalFilePath)
let filename: String = fileUrl.lastPathComponent.filenameWithoutExtension
let containingDir: String = fileUrl.deletingLastPathComponent().absoluteString
let containingDir: String = fileUrl.deletingLastPathComponent().path
return "\(containingDir)/\(filename)-signal-ios-thumbnail.jpg"
}
@ -684,7 +724,7 @@ extension Attachment {
public var isVisualMedia: Bool { isImage || isVideo || isAnimated }
public func readDataFromFile() throws -> Data? {
guard let filePath: String = Attachment.originalFilePath(id: self.id, mimeType: self.contentType, sourceFilename: self.sourceFilename) else {
guard let filePath: String = self.originalFilePath else {
return nil
}
@ -737,27 +777,6 @@ extension Attachment {
loadThumbnail(with: size.dimension, success: success, failure: failure)
}
func thumbnailSync(size: ThumbnailSize) -> UIImage? {
guard isVideo || isImage || isAnimated else { return nil }
let semaphore: DispatchSemaphore = DispatchSemaphore(value: 0)
var image: UIImage?
thumbnail(
size: size,
success: { loadedImage in
image = loadedImage
semaphore.signal()
},
failure: { semaphore.signal() }
)
// Wait up to 5 seconds for the thumbnail to be loaded
_ = semaphore.wait(timeout: .now() + .seconds(5))
return image
}
public func cloneAsThumbnail() -> Attachment {
fatalError("TODO: Add this back")
}

View File

@ -78,13 +78,14 @@ public struct ControlMessageProcessRecord: Codable, FetchableRecord, Persistable
// causing a unique constraint violation
if isRetry { return nil }
// Allow '.new' closed group config message duplicates in this case to avoid
// Allow '.new' and 'encryptionKeyPair' closed group control message duplicates to avoid
// the following situation:
// The app performed a background poll or received a push notification
// This method was invoked and the received message timestamps table was updated
// Processing wasn't finished
// The user doesn't see the new closed group
if case .new = (message as? ClosedGroupControlMessage)?.kind { return nil }
if case .encryptionKeyPair = (message as? ClosedGroupControlMessage)?.kind { return nil }
// For all other cases we want to prevent duplicate handling of the message (this
// can happen in a number of situations, primarily with sync messages though hence

View File

@ -14,6 +14,10 @@ public struct SessionThread: Codable, Identifiable, Equatable, FetchableRecord,
using: DisappearingMessagesConfiguration.threadForeignKey
)
public static let interactions = hasMany(Interaction.self, using: Interaction.threadForeignKey)
public static let typingIndicator = hasOne(
ThreadTypingIndicator.self,
using: ThreadTypingIndicator.threadForeignKey
)
public typealias Columns = CodingKeys
public enum CodingKeys: String, CodingKey, ColumnExpression {
@ -90,6 +94,10 @@ public struct SessionThread: Codable, Identifiable, Equatable, FetchableRecord,
request(for: SessionThread.interactions)
}
public var typingIndicator: QueryInterfaceRequest<ThreadTypingIndicator> {
request(for: SessionThread.typingIndicator)
}
// MARK: - Initialization
public init(
@ -122,6 +130,13 @@ public struct SessionThread: Codable, Identifiable, Equatable, FetchableRecord,
.filter(Job.Columns.threadId == id)
.deleteAll(db)
// Delete any GroupMembers associated to this thread
if variant == .closedGroup || variant == .openGroup {
try GroupMember
.filter(GroupMember.Columns.groupId == id)
.deleteAll(db)
}
return try performDelete(db)
}
}

View File

@ -0,0 +1,30 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import SessionUtilitiesKit
/// This record is created for an incoming typing indicator message
///
/// **Note:** Currently we only support typing indicator on contact thread (one-to-one), to support groups we would need
/// to change the structure of this table (since its primary key is the threadId)
public struct ThreadTypingIndicator: Codable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
public static var databaseTableName: String { "threadTypingIndicator" }
internal static let threadForeignKey = ForeignKey([Columns.threadId], to: [SessionThread.Columns.id])
private static let thread = belongsTo(SessionThread.self, using: threadForeignKey)
public typealias Columns = CodingKeys
public enum CodingKeys: String, CodingKey, ColumnExpression {
case threadId
case timestampMs
}
public let threadId: String
public let timestampMs: Int64
// MARK: - Relationships
public var thread: QueryInterfaceRequest<SessionThread> {
request(for: ThreadTypingIndicator.thread)
}
}

View File

@ -45,7 +45,7 @@ public enum AttachmentDownloadJob: JobExecutor {
}
.defaulting(to: attachment)
let temporaryFilePath: URL = URL(
let temporaryFileUrl: URL = URL(
fileURLWithPath: OWSTemporaryDirectoryAccessibleAfterFirstAuth() + UUID().uuidString
)
let downloadPromise: Promise<Data> = {
@ -66,7 +66,7 @@ public enum AttachmentDownloadJob: JobExecutor {
downloadPromise
.then { data -> Promise<Void> in
try data.write(to: temporaryFilePath, options: .atomic)
try data.write(to: temporaryFileUrl, options: .atomic)
let plaintext: Data = try {
guard
@ -92,7 +92,7 @@ public enum AttachmentDownloadJob: JobExecutor {
}
.done {
// Remove the temporary file
OWSFileSystem.deleteFile(temporaryFilePath.absoluteString)
OWSFileSystem.deleteFile(temporaryFileUrl.path)
// Update the attachment state
GRDBStorage.shared.write { db in
@ -109,7 +109,7 @@ public enum AttachmentDownloadJob: JobExecutor {
success(job, false)
}
.catch { error in
OWSFileSystem.deleteFile(temporaryFilePath.absoluteString)
OWSFileSystem.deleteFile(temporaryFileUrl.path)
switch error {
case OnionRequestAPI.Error.httpRequestFailedAtDestination(let statusCode, _, _) where statusCode == 400:

View File

@ -112,6 +112,9 @@ public enum MessageSendJob: JobExecutor {
}
}
// Add the threadId to the message if there isn't one set
details.message.threadId = (details.message.threadId ?? job.threadId)
// Perform the actual message sending
GRDBStorage.shared.write { db -> Promise<Void> in
try MessageSender.send(

View File

@ -66,8 +66,7 @@ public class Message: Codable {
public func setGroupContextIfNeeded(_ db: Database, on dataMessage: SNProtoDataMessage.SNProtoDataMessageBuilder) throws {
guard
let threadId: String = threadId,
let thread: SessionThread = try? SessionThread.fetchOne(db, id: threadId),
thread.variant == .closedGroup,
(try? ClosedGroup.exists(db, id: threadId)) == true,
let legacyGroupId: Data = "\(Legacy.closedGroupIdPrefix)\(threadId)".data(using: .utf8)
else { return }
@ -76,3 +75,12 @@ public class Message: Codable {
dataMessage.setGroup(try groupProto.build())
}
}
// MARK: - Mutation
internal extension Message {
func with(sentTimestamp: UInt64) -> Message {
self.sentTimestamp = sentTimestamp
return self
}
}

View File

@ -430,8 +430,10 @@ public final class OpenGroupAPIV2 : NSObject {
return send(request).map(on: OpenGroupAPIV2.workQueue) { _ in }
}
public static func isUserModerator(_ publicKey: String, for room: String, on server: String) -> Bool {
return moderators[server]?[room]?.contains(publicKey) ?? false
public static func isUserModerator(_ publicKey: String, for room: String?, on server: String?) -> Bool {
guard let room: String = room, let server: String = server else { return false }
return (moderators[server]?[room]?.contains(publicKey) ?? false)
}
// MARK: General

View File

@ -1,15 +0,0 @@
@objc(LKMention)
public final class Mention : NSObject {
@objc public let publicKey: String
@objc public let displayName: String
@objc public init(publicKey: String, displayName: String) {
self.publicKey = publicKey
self.displayName = displayName
}
@objc public func isContained(in string: String) -> Bool {
return string.contains(displayName)
}
}

View File

@ -1,93 +0,0 @@
import PromiseKit
@objc(LKMentionsManager)
public final class MentionsManager : NSObject {
/// A mapping from thread ID to set of user hex encoded public keys.
///
/// - Note: Should only be accessed from the main queue to avoid race conditions.
@objc public static var userPublicKeyCache: [String:Set<String>] = [:]
internal static var storage: OWSPrimaryStorage { OWSPrimaryStorage.shared() }
// MARK: Settings
private static var userIDScanLimit: UInt = 512
// MARK: Initialization
private override init() { }
// MARK: Implementation
@objc public static func cache(_ publicKey: String, for threadID: String) {
if let cache = userPublicKeyCache[threadID] {
userPublicKeyCache[threadID] = cache.union([ publicKey ])
} else {
userPublicKeyCache[threadID] = [ publicKey ]
}
}
@objc public static func getMentionCandidates(for query: String, in threadID: String) -> [Mention] {
// Prepare
guard let cache = userPublicKeyCache[threadID] else { return [] }
var candidates: [Mention] = []
// Gather candidates
let openGroupV2 = Storage.shared.getV2OpenGroup(for: threadID)
storage.dbReadConnection.read { transaction in
candidates = cache.compactMap { publicKey in
guard let displayName: String = Profile.displayNameNoFallback(id: publicKey, context: (openGroupV2 != nil ? .openGroup : .regular)) else {
return nil
}
guard !displayName.hasPrefix("Anonymous") else { return nil }
return Mention(publicKey: publicKey, displayName: displayName)
}
}
candidates = candidates.filter { $0.publicKey != getUserHexEncodedPublicKey() }
// Sort alphabetically first
candidates.sort { $0.displayName < $1.displayName }
if query.count >= 2 {
// Filter out any non-matching candidates
candidates = candidates.filter { $0.displayName.lowercased().contains(query.lowercased()) }
// Sort based on where in the candidate the query occurs
candidates.sort {
$0.displayName.lowercased().range(of: query.lowercased())!.lowerBound < $1.displayName.lowercased().range(of: query.lowercased())!.lowerBound
}
}
// Return
return candidates
}
@objc public static func populateUserPublicKeyCacheIfNeeded(for threadID: String, in transaction: YapDatabaseReadTransaction? = nil) {
var result: Set<String> = []
func populate(in transaction: YapDatabaseReadTransaction) {
guard let thread = TSThread.fetch(uniqueId: threadID, transaction: transaction) else { return }
if let groupThread = thread as? TSGroupThread, groupThread.groupModel.groupType == .closedGroup {
result = result.union(groupThread.groupModel.groupMemberIds).subtracting([ getUserHexEncodedPublicKey() ])
} else {
let hasOnlyCurrentUser: Bool = (
userPublicKeyCache[threadID]?.count == 1 &&
userPublicKeyCache[threadID]?.first == getUserHexEncodedPublicKey()
)
guard userPublicKeyCache[threadID] == nil || ((thread as? TSGroupThread)?.groupModel.groupType == .openGroup && hasOnlyCurrentUser) else {
return
}
let interactions = transaction.ext(TSMessageDatabaseViewExtensionName) as! YapDatabaseViewTransaction
interactions.enumerateKeysAndObjects(inGroup: threadID) { _, _, object, index, _ in
guard let message = object as? TSIncomingMessage, index < userIDScanLimit else { return }
result.insert(message.authorId)
}
}
result.insert(getUserHexEncodedPublicKey())
}
if let transaction = transaction {
populate(in: transaction)
} else {
storage.dbReadConnection.read { transaction in
populate(in: transaction)
}
}
if !result.isEmpty {
userPublicKeyCache[threadID] = result
}
}
}

View File

@ -96,75 +96,28 @@ extension MessageReceiver {
// MARK: - Typing Indicators
private static func handleTypingIndicator(_ db: Database, message: TypingIndicator) throws {
guard
let senderPublicKey: String = message.sender,
let thread: SessionThread = try SessionThread.fetchOne(db, id: senderPublicKey)
else { return }
switch message.kind {
case .started: try showTypingIndicatorIfNeeded(db, for: message.sender)
case .stopped: try hideTypingIndicatorIfNeeded(db, for: message.sender)
case .started:
TypingIndicators.didStartTyping(
db,
in: thread,
direction: .incoming,
timestampMs: message.sentTimestamp.map { Int64($0) }
)
case .stopped:
TypingIndicators.didStopTyping(db, in: thread, direction: .incoming)
default:
SNLog("Unknown TypingIndicator Kind ignored")
return
}
}
private static func showTypingIndicatorIfNeeded(_ db: Database, for senderPublicKey: String?) throws {
guard let senderPublicKey: String = senderPublicKey else { return }
var threadOrNil: TSContactThread?
Storage.read { transaction in
threadOrNil = TSContactThread.getWithContactSessionID(senderPublicKey, transaction: transaction)
}
guard let thread = threadOrNil else { return }
func showTypingIndicatorsIfNeeded() {
SSKEnvironment.shared.typingIndicators.didReceiveTypingStartedMessage(inThread: thread, recipientId: senderPublicKey, deviceId: 1)
}
if Thread.current.isMainThread {
showTypingIndicatorsIfNeeded()
} else {
DispatchQueue.main.async {
showTypingIndicatorsIfNeeded()
}
}
}
private static func hideTypingIndicatorIfNeeded(_ db: Database, for senderPublicKey: String?) throws {
guard let senderPublicKey: String = senderPublicKey else { return }
var threadOrNil: TSContactThread?
Storage.read { transaction in
threadOrNil = TSContactThread.getWithContactSessionID(senderPublicKey, transaction: transaction)
}
guard let thread = threadOrNil else { return }
func hideTypingIndicatorsIfNeeded() {
SSKEnvironment.shared.typingIndicators.didReceiveTypingStoppedMessage(inThread: thread, recipientId: senderPublicKey, deviceId: 1)
}
if Thread.current.isMainThread {
hideTypingIndicatorsIfNeeded()
} else {
DispatchQueue.main.async {
hideTypingIndicatorsIfNeeded()
}
}
}
public static func cancelTypingIndicatorsIfNeeded(for senderPublicKey: String) {
var threadOrNil: TSContactThread?
Storage.read { transaction in
threadOrNil = TSContactThread.getWithContactSessionID(senderPublicKey, transaction: transaction)
}
guard let thread = threadOrNil else { return }
func cancelTypingIndicatorsIfNeeded() {
SSKEnvironment.shared.typingIndicators.didReceiveIncomingMessage(inThread: thread, recipientId: senderPublicKey, deviceId: 1)
}
if Thread.current.isMainThread {
cancelTypingIndicatorsIfNeeded()
} else {
DispatchQueue.main.async {
cancelTypingIndicatorsIfNeeded()
}
}
}
// MARK: - Data Extraction Notification
@ -549,15 +502,17 @@ extension MessageReceiver {
)
// Parse & persist attachments
let attachments: [Attachment] = dataMessage.attachments
.compactMap { proto in
let attachments: [Attachment] = try dataMessage.attachments
.compactMap { proto -> Attachment? in
let attachment: Attachment = Attachment(proto: proto)
// Attachments on received messages must have a 'downloadUrl' otherwise
// they are invalid and we can ignore them
return (attachment.downloadUrl != nil ? attachment : nil)
}
try attachments.saveAll(db)
.map { attachment in
try attachment.saved(db)
}
message.attachmentIds = attachments.map { $0.id }
@ -615,7 +570,7 @@ extension MessageReceiver {
// Cancel any typing indicators if needed
if isMainAppActive {
cancelTypingIndicatorsIfNeeded(for: message.sender!)
TypingIndicators.didStopTyping(db, in: thread, direction: .incoming)
}
// Update the contact's approval status of the current user if needed (if we are getting messages from
@ -976,7 +931,10 @@ extension MessageReceiver {
body: ClosedGroupControlMessage.Kind
.nameChange(name: name)
.infoMessage(db, sender: sender),
timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000))
timestampMs: (
message.sentTimestamp.map { Int64($0) } ??
Int64(floor(Date().timeIntervalSince1970 * 1000))
)
).inserted(db)
}
}
@ -992,7 +950,7 @@ extension MessageReceiver {
let addedMembers: [String] = membersAsData.map { $0.toHexString() }
let currentMemberIds: Set<String> = groupMembers.map { $0.profileId }.asSet()
let members: Set<String> = currentMemberIds.union(addedMembers)
// Create records for any new members
try addedMembers
.filter { !currentMemberIds.contains($0) }
@ -1025,14 +983,11 @@ extension MessageReceiver {
}
}
// Update zombie members in case the added members are zombies
let zombies: [GroupMember] = ((try? closedGroup.zombies.fetchAll(db)) ?? [])
if !zombies.map { $0.profileId }.asSet().intersection(addedMembers).isEmpty {
try zombies
.filter { !addedMembers.contains($0.profileId) }
.deleteAll(db)
}
// Remove any 'zombie' versions of the added members (in case they were re-added)
_ = try closedGroup
.zombies
.filter(addedMembers.contains(GroupMember.Columns.profileId))
.deleteAll(db)
// Notify the user if needed
guard members != Set(groupMembers.map { $0.profileId }) else { return }
@ -1050,7 +1005,10 @@ extension MessageReceiver {
.map { Data(hex: $0) }
)
.infoMessage(db, sender: sender),
timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000))
timestampMs: (
message.sentTimestamp.map { Int64($0) } ??
Int64(floor(Date().timeIntervalSince1970 * 1000))
)
).inserted(db)
}
}
@ -1124,7 +1082,10 @@ extension MessageReceiver {
.map { Data(hex: $0) }
)
.infoMessage(db, sender: sender),
timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000))
timestampMs: (
message.sentTimestamp.map { Int64($0) } ??
Int64(floor(Date().timeIntervalSince1970 * 1000))
)
).inserted(db)
}
}
@ -1159,6 +1120,14 @@ extension MessageReceiver {
// Remove the group from the database and unsubscribe from PNs
ClosedGroupPoller.shared.stopPolling(for: id)
try closedGroup
.members
.filter(
GroupMember.Columns.role == GroupMember.Role.standard ||
GroupMember.Columns.role == GroupMember.Role.zombie
)
.deleteAll(db)
_ = try closedGroup
.keyPairs
.deleteAll(db)
@ -1183,10 +1152,6 @@ extension MessageReceiver {
).insert(db)
}
// Update the group
try membersToRemove
.deleteAll(db)
// Notify the user if needed
guard updatedMemberIds != Set(members.map { $0.profileId }) else { return }
@ -1198,7 +1163,10 @@ extension MessageReceiver {
body: ClosedGroupControlMessage.Kind
.memberLeft
.infoMessage(db, sender: sender),
timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000))
timestampMs: (
message.sentTimestamp.map { Int64($0) } ??
Int64(floor(Date().timeIntervalSince1970 * 1000))
)
).inserted(db)
}
}

View File

@ -24,13 +24,13 @@ extension MessageSender {
let membersAsData = members.map { Data(hex: $0) }
let admins = [ userPublicKey ]
let adminsAsData = admins.map { Data(hex: $0) }
let formationTimestamp: TimeInterval = Date().timeIntervalSince1970
let thread: SessionThread = try SessionThread
.fetchOrCreate(db, id: groupPublicKey, variant: .closedGroup)
try ClosedGroup(
threadId: groupPublicKey,
name: name,
formationTimestamp: Date().timeIntervalSince1970
formationTimestamp: formationTimestamp
).insert(db)
try admins.forEach { adminId in
@ -74,6 +74,11 @@ extension MessageSender {
admins: adminsAsData,
expirationTimer: 0
)
)
.with(
// Note: We set this here to ensure the value matches the 'ClosedGroup'
// object we created
sentTimestamp: UInt64(floor(formationTimestamp * 1000))
),
interactionId: nil,
in: contactThread
@ -501,23 +506,6 @@ extension MessageSender {
}
let userPublicKey: String = getUserHexEncodedPublicKey(db)
let isCurrentUserAdmin: Bool = allGroupMembers.contains(where: {
$0.role == .admin && $0.profileId == userPublicKey
})
let membersToRemove: [GroupMember] = allGroupMembers
.filter { member in
member.role == .standard && (
isCurrentUserAdmin || // If the admin leaves the group is disbanded
member.profileId == userPublicKey
)
}
let adminsToRemove: [GroupMember] = allGroupMembers
.filter { member in
member.role == .admin && (
isCurrentUserAdmin || // If the admin leaves the group is disbanded
member.profileId == userPublicKey
)
}
// Notify the user
let interaction: Interaction = try Interaction(
@ -563,8 +551,9 @@ extension MessageSender {
.map { _ in }
// Update the group
try membersToRemove.deleteAll(db)
try adminsToRemove.deleteAll(db)
_ = try closedGroup
.allMembers
.deleteAll(db)
// Return
return promise

View File

@ -30,7 +30,6 @@ extension MessageSender {
}
public static func send(_ db: Database, message: Message, interactionId: Int64?, in thread: SessionThread) throws {
JobRunner.add(
db,
job: Job(

View File

@ -93,8 +93,8 @@ public final class MessageSender : NSObject {
// Set the timestamp, sender and recipient
if message.sentTimestamp == nil { // Visible messages will already have their sent timestamp set
message.sentTimestamp = NSDate.millisecondTimestamp()
}
message.sentTimestamp = UInt64(floor(Date().timeIntervalSince1970 * 1000))
let isSelfSend: Bool = (message.recipient == userPublicKey)
message.sender = userPublicKey
@ -340,7 +340,7 @@ public final class MessageSender : NSObject {
// Set the timestamp, sender and recipient
if message.sentTimestamp == nil { // Visible messages will already have their sent timestamp set
message.sentTimestamp = NSDate.millisecondTimestamp()
message.sentTimestamp = UInt64(floor(Date().timeIntervalSince1970 * 1000))
}
message.sender = getUserHexEncodedPublicKey()
@ -552,18 +552,8 @@ public final class MessageSender : NSObject {
if let interactionId: Int64 = interactionId {
return try Interaction.fetchOne(db, id: interactionId)
}
else if let sentTimestamp: Double = message.sentTimestamp.map({ Double($0) }) {
// If we have a threadId then include that in the filter to make the request smaller
if
let threadId: String = message.threadId,
!threadId.isEmpty,
let thread: SessionThread = try? SessionThread.fetchOne(db, id: threadId)
{
return try thread.interactions
.filter(Interaction.Columns.timestampMs == sentTimestamp)
.fetchOne(db)
}
if let sentTimestamp: Double = message.sentTimestamp.map({ Double($0) }) {
return try Interaction
.filter(Interaction.Columns.timestampMs == sentTimestamp)
.fetchOne(db)

View File

@ -9,7 +9,6 @@ public struct QuotedReplyModel {
public let timestampMs: Int64
public let body: String?
public let attachment: Attachment?
public let thumbnailImage: UIImage?
public let contentType: String?
public let sourceFileName: String?
public let thumbnailDownloadFailed: Bool
@ -22,7 +21,6 @@ public struct QuotedReplyModel {
timestampMs: Int64,
body: String?,
attachment: Attachment?,
thumbnailImage: UIImage?,
contentType: String?,
sourceFileName: String?,
thumbnailDownloadFailed: Bool
@ -32,23 +30,26 @@ public struct QuotedReplyModel {
self.authorId = authorId
self.timestampMs = timestampMs
self.body = body
self.thumbnailImage = thumbnailImage
self.contentType = contentType
self.sourceFileName = sourceFileName
self.thumbnailDownloadFailed = thumbnailDownloadFailed
}
public static func quotedReplyForSending(
_ db: Database,
interaction: Interaction,
threadId: String,
authorId: String,
variant: Interaction.Variant,
body: String?,
timestampMs: Int64,
attachments: [Attachment]?,
linkPreview: LinkPreview?
) -> QuotedReplyModel? {
guard interaction.variant == .standardOutgoing || interaction.variant == .standardOutgoing else {
guard variant == .standardOutgoing || variant == .standardIncoming else {
return nil
}
var quotedText: String? = interaction.body
var quotedAttachment: Attachment? = try? interaction.attachments.fetchOne(db)
var quotedText: String? = body
var quotedAttachment: Attachment? = attachments?.first
// If the attachment is "oversize text", try the quote as a reply to text, not as
// a reply to an attachment
@ -57,7 +58,7 @@ public struct QuotedReplyModel {
let attachment: Attachment = quotedAttachment,
attachment.contentType == OWSMimeTypeOversizeTextMessage,
(
(interaction.variant == .standardIncoming && attachment.state == .downloaded) ||
(variant == .standardIncoming && attachment.state == .downloaded) ||
attachment.state != .failed
),
let originalFilePath: String = attachment.originalFilePath
@ -100,12 +101,11 @@ public struct QuotedReplyModel {
}
return QuotedReplyModel(
threadId: interaction.threadId,
authorId: interaction.authorId,
timestampMs: interaction.timestampMs,
threadId: threadId,
authorId: authorId,
timestampMs: timestampMs,
body: (quotedText == nil && quotedAttachment == nil ? "" : quotedText),
attachment: quotedAttachment,
thumbnailImage: quotedAttachment?.thumbnailImageSmallSync(),
contentType: quotedAttachment?.contentType,
sourceFileName: quotedAttachment?.sourceFilename,
thumbnailDownloadFailed: false

View File

@ -1,388 +1,166 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import SessionUtilitiesKit
@objc(OWSTypingIndicators)
public protocol TypingIndicators : AnyObject {
@objc
func didStartTypingOutgoingInput(inThread thread: TSThread)
@objc
func didStopTypingOutgoingInput(inThread thread: TSThread)
@objc
func didSendOutgoingMessage(inThread thread: TSThread)
@objc
func didReceiveTypingStartedMessage(inThread thread: TSThread, recipientId: String, deviceId: UInt)
@objc
func didReceiveTypingStoppedMessage(inThread thread: TSThread, recipientId: String, deviceId: UInt)
@objc
func didReceiveIncomingMessage(inThread thread: TSThread, recipientId: String, deviceId: UInt)
// Returns the recipient id of the user who should currently be shown typing for a given thread.
//
// If no one is typing in that thread, returns nil.
// If multiple users are typing in that thread, returns the user to show.
//
// TODO: Use this method.
@objc
func typingRecipientId(forThread thread: TSThread) -> String?
@objc
func setTypingIndicatorsEnabled(value: Bool)
@objc
func areTypingIndicatorsEnabled() -> Bool
}
// MARK: -
@objc(OWSTypingIndicatorsImpl)
public class TypingIndicatorsImpl : NSObject, TypingIndicators {
@objc
public static let typingIndicatorStateDidChange = Notification.Name("typingIndicatorStateDidChange")
private let kDatabaseCollection = "TypingIndicators"
private let kDatabaseKey_TypingIndicatorsEnabled = "kDatabaseKey_TypingIndicatorsEnabled"
private var _areTypingIndicatorsEnabled = false
public override init() {
super.init()
AppReadiness.runNowOrWhenAppWillBecomeReady {
self.setup()
}
public class TypingIndicators {
// MARK: - Direction
public enum Direction {
case outgoing
case incoming
}
private func setup() {
_areTypingIndicatorsEnabled = OWSPrimaryStorage.shared().dbReadConnection.bool(forKey: kDatabaseKey_TypingIndicatorsEnabled, inCollection: kDatabaseCollection, defaultValue: false)
}
// MARK: -
@objc
public func setTypingIndicatorsEnabled(value: Bool) {
_areTypingIndicatorsEnabled = value
OWSPrimaryStorage.shared().dbReadWriteConnection.setBool(value, forKey: kDatabaseKey_TypingIndicatorsEnabled, inCollection: kDatabaseCollection)
NotificationCenter.default.postNotificationNameAsync(TypingIndicatorsImpl.typingIndicatorStateDidChange, object: nil)
}
@objc
public func areTypingIndicatorsEnabled() -> Bool {
return _areTypingIndicatorsEnabled
}
// MARK: -
@objc
public func didStartTypingOutgoingInput(inThread thread: TSThread) {
guard let outgoingIndicators = ensureOutgoingIndicators(forThread: thread), !thread.isMessageRequest() else {
return
}
outgoingIndicators.didStartTypingOutgoingInput()
}
@objc
public func didStopTypingOutgoingInput(inThread thread: TSThread) {
guard let outgoingIndicators = ensureOutgoingIndicators(forThread: thread), !thread.isMessageRequest() else {
return
}
outgoingIndicators.didStopTypingOutgoingInput()
}
@objc
public func didSendOutgoingMessage(inThread thread: TSThread) {
guard let outgoingIndicators = ensureOutgoingIndicators(forThread: thread) else {
return
}
outgoingIndicators.didSendOutgoingMessage()
}
@objc
public func didReceiveTypingStartedMessage(inThread thread: TSThread, recipientId: String, deviceId: UInt) {
let incomingIndicators = ensureIncomingIndicators(forThread: thread, recipientId: recipientId, deviceId: deviceId)
incomingIndicators.didReceiveTypingStartedMessage()
}
@objc
public func didReceiveTypingStoppedMessage(inThread thread: TSThread, recipientId: String, deviceId: UInt) {
let incomingIndicators = ensureIncomingIndicators(forThread: thread, recipientId: recipientId, deviceId: deviceId)
incomingIndicators.didReceiveTypingStoppedMessage()
}
@objc
public func didReceiveIncomingMessage(inThread thread: TSThread, recipientId: String, deviceId: UInt) {
let incomingIndicators = ensureIncomingIndicators(forThread: thread, recipientId: recipientId, deviceId: deviceId)
incomingIndicators.didReceiveIncomingMessage()
}
@objc
public func typingRecipientId(forThread thread: TSThread) -> String? {
guard areTypingIndicatorsEnabled() else {
return nil
}
var firstRecipientId: String?
var firstTimestamp: UInt64?
let threadKey = incomingIndicatorsKey(forThread: thread)
guard let deviceMap = incomingIndicatorsMap[threadKey] else {
// No devices are typing in this thread.
return nil
}
for incomingIndicators in deviceMap.values {
guard incomingIndicators.isTyping else {
continue
private class Indicator {
fileprivate let thread: SessionThread
fileprivate let direction: Direction
fileprivate let timestampMs: Int64
fileprivate var refreshTimer: Timer?
fileprivate var stopTimer: Timer?
init?(thread: SessionThread, direction: Direction, timestampMs: Int64?) {
// The `typingIndicatorsEnabled` flag reflects the user-facing setting in the app
// preferences, if it's disabled we don't want to emit "typing indicator" messages
// or show typing indicators for other users
//
// We also don't want to show/send typing indicators for message requests
guard GRDBStorage.shared.read({ db in
(
db[.typingIndicatorsEnabled] == true &&
thread.isMessageRequest(db) == false
)
}) == true else {
return nil
}
guard let startedTypingTimestamp = incomingIndicators.startedTypingTimestamp else {
continue
}
if let firstTimestamp = firstTimestamp,
firstTimestamp < startedTypingTimestamp {
// More than one recipient/device is typing in this conversation;
// prefer the one that started typing first.
continue
}
firstRecipientId = incomingIndicators.recipientId
firstTimestamp = startedTypingTimestamp
}
return firstRecipientId
}
// MARK: -
// Map of thread id-to-OutgoingIndicators.
private var outgoingIndicatorsMap = [String: OutgoingIndicators]()
private func ensureOutgoingIndicators(forThread thread: TSThread) -> OutgoingIndicators? {
guard let threadId = thread.uniqueId else {
return nil
}
if let outgoingIndicators = outgoingIndicatorsMap[threadId] {
return outgoingIndicators
}
let outgoingIndicators = OutgoingIndicators(delegate: self, thread: thread)
outgoingIndicatorsMap[threadId] = outgoingIndicators
return outgoingIndicators
}
// The sender maintains two timers per chat:
//
// A sendPause timer
// A sendRefresh timer
private class OutgoingIndicators {
private weak var delegate: TypingIndicators?
private let thread: TSThread
private var sendPauseTimer: Timer?
private var sendRefreshTimer: Timer?
init(delegate: TypingIndicators, thread: TSThread) {
self.delegate = delegate
// Don't send typing indicators in group threads
guard thread.variant != .closedGroup && thread.variant != .openGroup else { return nil }
self.thread = thread
self.direction = direction
self.timestampMs = (timestampMs ?? Int64(floor(Date().timeIntervalSince1970 * 1000)))
}
// MARK: -
func didStartTypingOutgoingInput() {
if sendRefreshTimer == nil {
// If the user types a character into the compose box, and the sendRefresh timer isnt running:
sendTypingMessageIfNecessary(forThread: thread, action: .started)
sendRefreshTimer?.invalidate()
sendRefreshTimer = Timer.weakScheduledTimer(withTimeInterval: 10,
target: self,
selector: #selector(OutgoingIndicators.sendRefreshTimerDidFire),
userInfo: nil,
repeats: false)
} else {
// If the user types a character into the compose box, and the sendRefresh timer is running:
fileprivate func starting(_ db: Database) -> Indicator {
let thread: SessionThread = self.thread
let direction: Direction = self.direction
let timestampMs: Int64 = self.timestampMs
// Start the typing indicator
switch direction {
case .outgoing:
scheduleRefreshCallback(db, shouldSend: (refreshTimer == nil))
case .incoming:
try? ThreadTypingIndicator(
threadId: thread.id,
timestampMs: timestampMs
)
.save(db)
}
sendPauseTimer?.invalidate()
sendPauseTimer = Timer.weakScheduledTimer(withTimeInterval: 3,
target: self,
selector: #selector(OutgoingIndicators.sendPauseTimerDidFire),
userInfo: nil,
repeats: false)
}
func didStopTypingOutgoingInput() {
sendTypingMessageIfNecessary(forThread: thread, action: .stopped)
sendRefreshTimer?.invalidate()
sendRefreshTimer = nil
sendPauseTimer?.invalidate()
sendPauseTimer = nil
}
@objc
func sendPauseTimerDidFire() {
sendTypingMessageIfNecessary(forThread: thread, action: .stopped)
sendRefreshTimer?.invalidate()
sendRefreshTimer = nil
sendPauseTimer?.invalidate()
sendPauseTimer = nil
}
@objc
func sendRefreshTimerDidFire() {
sendTypingMessageIfNecessary(forThread: thread, action: .started)
sendRefreshTimer?.invalidate()
sendRefreshTimer = Timer.weakScheduledTimer(withTimeInterval: 10,
target: self,
selector: #selector(sendRefreshTimerDidFire),
userInfo: nil,
repeats: false)
}
func didSendOutgoingMessage() {
sendRefreshTimer?.invalidate()
sendRefreshTimer = nil
sendPauseTimer?.invalidate()
sendPauseTimer = nil
}
private func sendTypingMessageIfNecessary(forThread thread: TSThread, action: TypingIndicator.Kind) {
guard let delegate = delegate else {
return
// Schedule the 'stopCallback' to cancel the typing indicator
stopTimer?.invalidate()
stopTimer = Timer.scheduledTimerOnMainThread(
withTimeInterval: (direction == .outgoing ? 3 : 5),
repeats: false
) { [weak self] _ in
GRDBStorage.shared.write { db in
self?.stoping(db)
}
}
// `areTypingIndicatorsEnabled` reflects the user-facing setting in the app preferences.
// If it's disabled we don't want to emit "typing indicator" messages
// or show typing indicators for other users.
guard delegate.areTypingIndicatorsEnabled() else {
return
return self
}
@discardableResult fileprivate func stoping(_ db: Database) -> Indicator? {
self.refreshTimer?.invalidate()
self.refreshTimer = nil
self.stopTimer?.invalidate()
self.stopTimer = nil
switch direction {
case .outgoing:
try? MessageSender.send(
db,
message: TypingIndicator(kind: .stopped),
interactionId: nil,
in: thread
)
case .incoming:
_ = try? ThreadTypingIndicator
.filter(ThreadTypingIndicator.Columns.threadId == thread.id)
.deleteAll(db)
}
if thread.isGroupThread() { return } // Don't send typing indicators in group threads
let typingIndicator = TypingIndicator()
typingIndicator.kind = action
SNMessagingKitConfiguration.shared.storage.write { transaction in
MessageSender.send(typingIndicator, in: thread, using: transaction as! YapDatabaseReadWriteTransaction)
return nil
}
private func scheduleRefreshCallback(_ db: Database, shouldSend: Bool = true) {
if shouldSend {
try? MessageSender.send(
db,
message: TypingIndicator(kind: .started),
interactionId: nil,
in: thread
)
}
}
}
// MARK: -
// Map of (thread id)-to-(recipient id and device id)-to-IncomingIndicators.
private var incomingIndicatorsMap = [String: [String: IncomingIndicators]]()
private func incomingIndicatorsKey(forThread thread: TSThread) -> String {
return String(describing: thread.uniqueId)
}
private func incomingIndicatorsKey(recipientId: String, deviceId: UInt) -> String {
return "\(recipientId) \(deviceId)"
}
private func ensureIncomingIndicators(forThread thread: TSThread, recipientId: String, deviceId: UInt) -> IncomingIndicators {
let threadKey = incomingIndicatorsKey(forThread: thread)
let deviceKey = incomingIndicatorsKey(recipientId: recipientId, deviceId: deviceId)
guard let deviceMap = incomingIndicatorsMap[threadKey] else {
let incomingIndicators = IncomingIndicators(delegate: self, thread: thread, recipientId: recipientId, deviceId: deviceId)
incomingIndicatorsMap[threadKey] = [deviceKey: incomingIndicators]
return incomingIndicators
}
guard let incomingIndicators = deviceMap[deviceKey] else {
let incomingIndicators = IncomingIndicators(delegate: self, thread: thread, recipientId: recipientId, deviceId: deviceId)
var deviceMapCopy = deviceMap
deviceMapCopy[deviceKey] = incomingIndicators
incomingIndicatorsMap[threadKey] = deviceMapCopy
return incomingIndicators
}
return incomingIndicators
}
// The receiver maintains one timer for each (sender, device) in a chat:
private class IncomingIndicators {
private weak var delegate: TypingIndicators?
private let thread: TSThread
fileprivate let recipientId: String
private let deviceId: UInt
private var displayTypingTimer: Timer?
fileprivate var startedTypingTimestamp: UInt64?
var isTyping = false {
didSet {
let didChange = oldValue != isTyping
if didChange {
notifyIfNecessary()
refreshTimer?.invalidate()
refreshTimer = Timer.scheduledTimerOnMainThread(
withTimeInterval: 10,
repeats: false
) { [weak self] _ in
GRDBStorage.shared.write { db in
self?.scheduleRefreshCallback(db)
}
}
}
init(delegate: TypingIndicators, thread: TSThread,
recipientId: String, deviceId: UInt) {
self.delegate = delegate
self.thread = thread
self.recipientId = recipientId
self.deviceId = deviceId
}
// MARK: - Variables
public static let shared: TypingIndicators = TypingIndicators()
private static var outgoing: Atomic<[String: Indicator]> = Atomic([:])
private static var incoming: Atomic<[String: Indicator]> = Atomic([:])
// MARK: - Functions
public static func didStartTyping(_ db: Database, in thread: SessionThread, direction: Direction, timestampMs: Int64?) {
switch direction {
case .outgoing:
let updatedIndicator: Indicator? = (
outgoing.wrappedValue[thread.id] ??
Indicator(thread: thread, direction: direction, timestampMs: timestampMs)
)?.starting(db)
outgoing.mutate { $0[thread.id] = updatedIndicator }
case .incoming:
let updatedIndicator: Indicator? = (
incoming.wrappedValue[thread.id] ??
Indicator(thread: thread, direction: direction, timestampMs: timestampMs)
)?.starting(db)
incoming.mutate { $0[thread.id] = updatedIndicator }
}
func didReceiveTypingStartedMessage() {
displayTypingTimer?.invalidate()
displayTypingTimer = Timer.weakScheduledTimer(withTimeInterval: 5,
target: self,
selector: #selector(IncomingIndicators.displayTypingTimerDidFire),
userInfo: nil,
repeats: false)
if !isTyping {
startedTypingTimestamp = NSDate.ows_millisecondTimeStamp()
}
isTyping = true
}
func didReceiveTypingStoppedMessage() {
clearTyping()
}
@objc
func displayTypingTimerDidFire() {
clearTyping()
}
func didReceiveIncomingMessage() {
clearTyping()
}
private func clearTyping() {
displayTypingTimer?.invalidate()
displayTypingTimer = nil
startedTypingTimestamp = nil
isTyping = false
}
private func notifyIfNecessary() {
guard let delegate = delegate else {
return
}
// `areTypingIndicatorsEnabled` reflects the user-facing setting in the app preferences.
// If it's disabled we don't want to emit "typing indicator" messages
// or show typing indicators for other users.
guard delegate.areTypingIndicatorsEnabled() else {
return
}
guard let threadId = thread.uniqueId else {
return
}
NotificationCenter.default.postNotificationNameAsync(TypingIndicatorsImpl.typingIndicatorStateDidChange, object: threadId)
}
public static func didStopTyping(_ db: Database, in thread: SessionThread, direction: Direction) {
switch direction {
case .outgoing:
let updatedIndicator: Indicator? = outgoing.wrappedValue[thread.id]?.stoping(db)
outgoing.mutate { $0[thread.id] = updatedIndicator }
case .incoming:
let updatedIndicator: Indicator? = incoming.wrappedValue[thread.id]?.stoping(db)
incoming.mutate { $0[thread.id] = updatedIndicator }
}
}
}

View File

@ -6,18 +6,6 @@
NS_ASSUME_NONNULL_BEGIN
NSString *NSStringForNotificationType(NotificationType value)
{
switch (value) {
case NotificationNamePreview:
return @"NotificationNamePreview";
case NotificationNameNoPreview:
return @"NotificationNameNoPreview";
case NotificationNoNameNoPreview:
return @"NotificationNoNameNoPreview";
}
}
NSString *const OWSPreferencesSignalDatabaseCollection = @"SignalPreferences";
NSString *const OWSPreferencesCallLoggingDidChangeNotification = @"OWSPreferencesCallLoggingDidChangeNotification";
NSString *const OWSPreferencesKeyScreenSecurity = @"Screen Security Key";

View File

@ -43,7 +43,7 @@ public extension Setting.StringKey {
}
public enum Preferences {
public enum NotificationPreviewType: Int, EnumSetting {
public enum NotificationPreviewType: Int, CaseIterable, EnumSetting {
/// Notifications should include both the sender name and a preview of the message content
case nameAndPreview
@ -60,10 +60,6 @@ public enum Preferences {
case .noNameNoPreview: return "NOTIFICATIONS_NONE".localized()
}
}
var accessibilityIdentifier: String {
return "NotificationSettingsOptionsViewController.\(name)"
}
}
public enum Sound: Int, Codable, DatabaseValueConvertible, EnumSetting {
@ -265,7 +261,45 @@ public enum Preferences {
// MARK: - Objective C Support
// FIXME: Remove this once the 'NotificationSettingsViewController' and 'OWSSoundSettingsViewController' have been refactored to Swift
// FIXME: Remove the below the 'NotificationSettingsViewController' and 'OWSSoundSettingsViewController' have been refactored to Swift
@objc(SMKPreferences)
public class SMKPreferences: NSObject {
@objc public static let notificationTypes: [Int] = Preferences.NotificationPreviewType
.allCases
.map { $0.rawValue }
@objc public static func nameForNotificationPreviewType(_ previewType: Int) -> String {
return Preferences.NotificationPreviewType(rawValue: previewType)
.defaulting(to: .nameAndPreview)
.name
}
@objc public static func notificationPreviewType() -> Int {
return GRDBStorage.shared[.preferencesNotificationPreviewType]
.defaulting(to: Preferences.NotificationPreviewType.nameAndPreview)
.rawValue
}
@objc public static func setNotificationPreviewType(_ previewType: Int) {
GRDBStorage.shared.write { db in
db[.preferencesNotificationPreviewType] = Preferences.NotificationPreviewType(rawValue: previewType)
.defaulting(to: .nameAndPreview)
}
}
@objc public static func accessibilityIdentifierForNotificationPreviewType(_ previewType: Int) -> String {
let notificationPreviewType: Preferences.NotificationPreviewType = Preferences.NotificationPreviewType(rawValue: previewType)
.defaulting(to: .nameAndPreview)
switch notificationPreviewType {
case .nameAndPreview: return "NotificationNamePreview"
case .nameNoPreview: return "NotificationNameNoPreview"
case .noNameNoPreview: return "NotificationNoNameNoPreview"
}
}
}
@objc(SMKSound)
public class SMKSound: NSObject {
@objc public static var notificationSounds: [Int] = Preferences.Sound.notificationSounds.map { $0.rawValue }
@ -285,7 +319,7 @@ public class SMKSound: NSObject {
}
@objc public static var defaultNotificationSound: Int {
GRDBStorage.shared[.defaultNotificationSound]
return GRDBStorage.shared[.defaultNotificationSound]
.defaulting(to: Preferences.Sound.defaultNotificationSound)
.rawValue
}

View File

@ -85,18 +85,21 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol {
CurrentAppContext().appUserDefaults().set(newBadgeNumber, forKey: "currentBadgeNumber")
// Title & body
let notificationsPreference = Environment.shared.preferences!.notificationPreviewType()
switch notificationsPreference {
case .namePreview:
notificationContent.title = notificationTitle
notificationContent.body = snippet
case .nameNoPreview:
notificationContent.title = notificationTitle
notificationContent.body = NotificationStrings.incomingMessageBody
case .noNameNoPreview:
notificationContent.title = "Session"
notificationContent.body = NotificationStrings.incomingMessageBody
default: break
let previewType: Preferences.NotificationPreviewType = db[.preferencesNotificationPreviewType]
.defaulting(to: .nameAndPreview)
switch previewType {
case .nameAndPreview:
notificationContent.title = notificationTitle
notificationContent.body = snippet
case .nameNoPreview:
notificationContent.title = notificationTitle
notificationContent.body = NotificationStrings.incomingMessageBody
case .noNameNoPreview:
notificationContent.title = "Session"
notificationContent.body = NotificationStrings.incomingMessageBody
}
// If it's a message request then overwrite the body to be something generic (only show a notification

View File

@ -167,7 +167,7 @@ public extension Database {
return T(rawValue: rawValue)
}
set { self[key.rawValue] = Setting(key: key.rawValue, value: newValue) }
set { self[key.rawValue] = Setting(key: key.rawValue, value: newValue?.rawValue) }
}
/// Value will be stored as a timestamp in seconds since 1970

View File

@ -1,16 +0,0 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
public extension Array where Element: PersistableRecord {
@discardableResult func deleteAll(_ db: Database) throws -> Bool {
return try self.reduce(true) { prev, next in
try (prev && next.delete(db))
}
}
func saveAll(_ db: Database) throws {
try forEach { try $0.save(db) }
}
}

View File

@ -34,7 +34,7 @@ import SessionMessagingKit
handler: { _ in
GRDBStorage.shared.writeAsync(
updates: { db in
try? Contact
try Contact
.fetchOrCreate(db, id: threadId)
.with(isBlocked: true)
.save(db)
@ -85,7 +85,7 @@ import SessionMessagingKit
handler: { _ in
GRDBStorage.shared.writeAsync(
updates: { db in
try? Contact
try Contact
.fetchOrCreate(db, id: threadId)
.with(isBlocked: false)
.save(db)

View File

@ -1,25 +0,0 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@class TSMessage;
@class TSThread;
@class YapDatabaseReadTransaction;
@interface OWSMessageUtils : NSObject
- (instancetype)init NS_UNAVAILABLE;
+ (instancetype)sharedManager;
- (NSUInteger)unreadMessagesCount;
- (NSUInteger)unreadMessagesCountExcept:(TSThread *)thread;
- (void)updateApplicationBadgeCount;
@end
NS_ASSUME_NONNULL_END

View File

@ -1,120 +0,0 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "OWSMessageUtils.h"
#import "AppContext.h"
#import "MIMETypeUtil.h"
#import "OWSPrimaryStorage.h"
#import "TSAccountManager.h"
#import "TSAttachment.h"
#import "TSAttachmentStream.h"
#import "TSDatabaseView.h"
#import "TSIncomingMessage.h"
#import "TSMessage.h"
#import "TSOutgoingMessage.h"
#import "TSQuotedMessage.h"
#import "TSThread.h"
#import "UIImage+OWS.h"
#import <YapDatabase/YapDatabase.h>
#import "SSKAsserts.h"
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
NS_ASSUME_NONNULL_BEGIN
@interface OWSMessageUtils ()
@property (nonatomic, readonly) YapDatabaseConnection *dbConnection;
@end
#pragma mark -
@implementation OWSMessageUtils
+ (instancetype)sharedManager
{
static OWSMessageUtils *sharedMyManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedMyManager = [[self alloc] initDefault];
});
return sharedMyManager;
}
- (instancetype)initDefault
{
OWSPrimaryStorage *primaryStorage = [OWSPrimaryStorage sharedManager];
return [self initWithPrimaryStorage:primaryStorage];
}
- (instancetype)initWithPrimaryStorage:(OWSPrimaryStorage *)primaryStorage
{
self = [super init];
if (!self) {
return self;
}
_dbConnection = primaryStorage.newDatabaseConnection;
OWSSingletonAssert();
return self;
}
- (NSUInteger)unreadMessagesCount
{
__block NSUInteger count = 0;
[LKStorage readWithBlock:^(YapDatabaseReadTransaction *transaction) {
YapDatabaseViewTransaction *unreadMessages = [transaction ext:TSUnreadDatabaseViewExtensionName];
NSArray<NSString *> *allGroups = [unreadMessages allGroups];
// FIXME: Confusingly, `allGroups` includes contact threads as well
for (NSString *groupID in allGroups) {
TSThread *thread = [TSThread fetchObjectWithUniqueID:groupID transaction:transaction];
// Don't increase the count for muted threads or message requests
if (thread.isMuted || thread.isMessageRequest) { continue; }
BOOL isGroupThread = thread.isGroupThread;
// For groups that only notifiy for mentions
if (isGroupThread && ((TSGroupThread *)thread).isOnlyNotifyingForMentions) {
count += [thread unreadMentionMessageCountWithTransaction:transaction];
} else {
count += [thread unreadMessageCountWithTransaction:transaction];
}
}
}];
return count;
}
- (NSUInteger)unreadMessagesCountExcept:(TSThread *)thread
{
__block NSUInteger numberOfItems;
[self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
id databaseView = [transaction ext:TSUnreadDatabaseViewExtensionName];
OWSAssertDebug(databaseView);
numberOfItems = ([databaseView numberOfItemsInAllGroups] - [databaseView numberOfItemsInGroup:thread.uniqueId]);
}];
return numberOfItems;
}
- (void)updateApplicationBadgeCount
{
if (!CurrentAppContext().isMainApp) {
return;
}
NSUInteger numberOfItems = [self unreadMessagesCount];
[CurrentAppContext() setMainAppBadgeNumber:numberOfItems];
}
@end
NS_ASSUME_NONNULL_END

View File

@ -11,8 +11,6 @@ FOUNDATION_EXPORT const unsigned char SignalUtilitiesKitVersionString[];
#import <SignalUtilitiesKit/AppVersion.h>
#import <SignalUtilitiesKit/AttachmentSharing.h>
#import <SignalUtilitiesKit/ByteParser.h>
#import <SignalUtilitiesKit/ContactCellView.h>
#import <SignalUtilitiesKit/ContactTableViewCell.h>
#import <SignalUtilitiesKit/FunctionalUtil.h>
#import <SignalUtilitiesKit/NSArray+OWS.h>
#import <SignalUtilitiesKit/NSAttributedString+OWS.h>
@ -25,7 +23,6 @@ FOUNDATION_EXPORT const unsigned char SignalUtilitiesKitVersionString[];
#import <SignalUtilitiesKit/OWSDispatch.h>
#import <SignalUtilitiesKit/OWSError.h>
#import <SignalUtilitiesKit/OWSFormat.h>
#import <SignalUtilitiesKit/OWSMessageUtils.h>
#import <SignalUtilitiesKit/OWSNavigationController.h>
#import <SignalUtilitiesKit/OWSOperation.h>
#import <SignalUtilitiesKit/OWSPrimaryStorage+keyFromIntLong.h>

View File

@ -65,22 +65,7 @@ public final class ProfilePictureView: UIView {
additionalImageView.layer.cornerRadius = additionalImageViewSize / 2
}
// MARK: - Updating
@objc(updateForContact:)
public func update(for publicKey: String?) {
guard let publicKey: String = publicKey else { return }
let profile: Profile? = GRDBStorage.shared.read { db in
try? Profile.fetchOne(db, id: publicKey)
}
update(
publicKey: publicKey,
profile: profile,
threadVariant: .contact
)
}
// FIXME: Look to deprecate this and replace it with the pattern in HomeViewModel (screen should fetch only the required info)
@objc(updateForThreadId:)
public func update(forThreadId threadId: String?) {
guard

View File

@ -1,31 +0,0 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
NS_ASSUME_NONNULL_BEGIN
extern const CGFloat kContactCellAvatarTextMargin;
@class TSThread;
@interface ContactCellView : UIStackView
@property (nonatomic, nullable) NSString *accessoryMessage;
- (void)configureWithRecipientId:(NSString *)recipientId;
- (void)configureWithThread:(TSThread *)thread;
- (void)prepareForReuse;
- (NSAttributedString *)verifiedSubtitle;
- (void)setAttributedSubtitle:(nullable NSAttributedString *)attributedSubtitle;
- (BOOL)hasAccessoryText;
- (void)setAccessoryView:(UIView *)accessoryView;
@end
NS_ASSUME_NONNULL_END

View File

@ -1,295 +0,0 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
#import "ContactCellView.h"
#import "UIFont+OWS.h"
#import "UIView+OWS.h"
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
#import <SessionMessagingKit/OWSPrimaryStorage.h>
#import <SignalUtilitiesKit/SignalAccount.h>
#import <SessionMessagingKit/TSContactThread.h>
#import <SessionMessagingKit/TSGroupThread.h>
#import <SessionMessagingKit/TSThread.h>
#import <SessionUIKit/SessionUIKit.h>
NS_ASSUME_NONNULL_BEGIN
const CGFloat kContactCellAvatarTextMargin = 12;
@interface ContactCellView ()
@property (nonatomic) UILabel *nameLabel;
@property (nonatomic) UILabel *profileNameLabel;
@property (nonatomic) LKProfilePictureView *profilePictureView;
@property (nonatomic) UILabel *subtitleLabel;
@property (nonatomic) UILabel *accessoryLabel;
@property (nonatomic) UIStackView *nameContainerView;
@property (nonatomic) UIView *accessoryViewContainer;
@property (nonatomic, nullable) TSThread *thread;
@property (nonatomic) NSString *recipientId;
@end
#pragma mark -
@implementation ContactCellView
- (instancetype)init
{
if (self = [super init]) {
[self configure];
}
return self;
}
#pragma mark - Dependencies
- (OWSPrimaryStorage *)primaryStorage
{
OWSAssertDebug(SSKEnvironment.shared.primaryStorage);
return SSKEnvironment.shared.primaryStorage;
}
- (TSAccountManager *)tsAccountManager
{
OWSAssertDebug(SSKEnvironment.shared.tsAccountManager);
return SSKEnvironment.shared.tsAccountManager;
}
#pragma mark -
- (void)configure
{
OWSAssertDebug(!self.nameLabel);
self.layoutMargins = UIEdgeInsetsZero;
_profilePictureView = [LKProfilePictureView new];
CGFloat profilePictureSize = LKValues.mediumProfilePictureSize;
[self.profilePictureView autoSetDimension:ALDimensionWidth toSize:profilePictureSize];
[self.profilePictureView autoSetDimension:ALDimensionHeight toSize:profilePictureSize];
self.profilePictureView.size = profilePictureSize;
self.nameLabel = [UILabel new];
self.nameLabel.lineBreakMode = NSLineBreakByTruncatingTail;
self.profileNameLabel = [UILabel new];
self.profileNameLabel.lineBreakMode = NSLineBreakByTruncatingTail;
self.subtitleLabel = [UILabel new];
self.accessoryLabel = [[UILabel alloc] init];
self.accessoryLabel.textAlignment = NSTextAlignmentRight;
self.accessoryViewContainer = [UIView containerView];
self.nameContainerView = [[UIStackView alloc] initWithArrangedSubviews:@[
self.nameLabel,
self.profileNameLabel,
self.subtitleLabel,
]];
self.nameContainerView.axis = UILayoutConstraintAxisVertical;
[self.nameContainerView setContentHuggingHorizontalLow];
[self.accessoryViewContainer setContentHuggingHorizontalHigh];
self.axis = UILayoutConstraintAxisHorizontal;
self.spacing = LKValues.mediumSpacing;
self.alignment = UIStackViewAlignmentCenter;
[self addArrangedSubview:self.profilePictureView];
[self addArrangedSubview:self.nameContainerView];
[self addArrangedSubview:self.accessoryViewContainer];
[self configureFontsAndColors];
}
- (void)configureFontsAndColors
{
self.nameLabel.font = [UIFont boldSystemFontOfSize:15];
self.profileNameLabel.font = [UIFont ows_regularFontWithSize:11.f];
self.subtitleLabel.font = [UIFont ows_regularFontWithSize:11.f];
self.accessoryLabel.font = [UIFont ows_mediumFontWithSize:13.f];
self.nameLabel.textColor = LKColors.text;
self.profileNameLabel.textColor = LKColors.separator;
self.subtitleLabel.textColor = LKColors.separator;
self.accessoryLabel.textColor = [UIColor colorWithWhite:0.5f alpha:1.f];
}
- (void)configureWithRecipientId:(NSString *)recipientId
{
OWSAssertDebug(recipientId.length > 0);
// Update fonts to reflect changes to dynamic type.
[self configureFontsAndColors];
self.recipientId = recipientId;
[self.primaryStorage.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
self.thread = [TSContactThread getThreadWithContactSessionID:recipientId transaction:transaction];
}];
BOOL isNoteToSelf = (IsNoteToSelfEnabled() && [recipientId isEqualToString:self.tsAccountManager.localNumber]);
if (isNoteToSelf) {
self.nameLabel.attributedText = [[NSAttributedString alloc]
initWithString:NSLocalizedString(@"NOTE_TO_SELF", @"Label for 1:1 conversation with yourself.")
attributes:@{
NSFontAttributeName : self.nameLabel.font,
}];
} else {
self.nameLabel.text = [SMKProfile displayNameWithId:recipientId thread:self.thread];
}
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(otherUsersProfileDidChange:)
name:NSNotification.otherUsersProfileDidChange
object:nil];
[self updateProfileName];
[self updateAvatar];
if (self.accessoryMessage) {
self.accessoryLabel.text = self.accessoryMessage;
[self setAccessoryView:self.accessoryLabel];
}
// Force layout, since imageView isn't being initally rendered on App Store optimized build.
[self layoutSubviews];
}
- (void)configureWithThread:(TSThread *)thread
{
OWSAssertDebug(thread);
self.thread = thread;
// Update fonts to reflect changes to dynamic type.
[self configureFontsAndColors];
NSString *threadName = thread.name;
if (threadName.length == 0 && [thread isKindOfClass:[TSGroupThread class]]) {
threadName = [MessageStrings newGroupDefaultTitle];
}
BOOL isNoteToSelf
= ([thread isKindOfClass:TSContactThread.class] && [((TSContactThread *)thread).contactSessionID isEqualToString:self.tsAccountManager.localNumber]);
if (isNoteToSelf) {
threadName = NSLocalizedString(@"NOTE_TO_SELF", @"Label for 1:1 conversation with yourself.");
}
if ([thread isKindOfClass:[TSContactThread class]]) {
self.recipientId = ((TSContactThread *)thread).contactSessionID;
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(otherUsersProfileDidChange:)
name:NSNotification.otherUsersProfileDidChange
object:nil];
[self updateProfileName];
} else {
self.nameLabel.text = thread.name;
[self.nameLabel setNeedsLayout];
}
[self updateAvatar];
if (self.accessoryMessage) {
self.accessoryLabel.text = self.accessoryMessage;
[self setAccessoryView:self.accessoryLabel];
}
// Force layout, since imageView isn't being initally rendered on App Store optimized build.
[self layoutSubviews];
}
- (void)updateAvatar
{
[LKStorage readWithBlock:^(YapDatabaseReadTransaction *transaction) {
[LKMentionsManager populateUserPublicKeyCacheIfNeededFor:self.thread.uniqueId in:transaction]; // FIXME: This is a terrible place to do this
}];
if (self.thread != nil) {
[self.profilePictureView updateForThread:self.thread];
} else {
[self.profilePictureView updateForContact:self.recipientId];
}
}
- (void)updateProfileName
{
self.nameLabel.text = [SMKProfile displayNameWithId:self.recipientId thread:self.thread];
[self.nameLabel setNeedsLayout];
}
- (void)prepareForReuse
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
self.thread = nil;
self.accessoryMessage = nil;
self.nameLabel.text = nil;
self.subtitleLabel.text = nil;
self.profileNameLabel.text = nil;
self.accessoryLabel.text = nil;
for (UIView *subview in self.accessoryViewContainer.subviews) {
[subview removeFromSuperview];
}
}
- (void)otherUsersProfileDidChange:(NSNotification *)notification
{
OWSAssertIsOnMainThread();
NSString *recipientId = notification.userInfo[NSNotification.profileRecipientIdKey];
OWSAssertDebug(recipientId.length > 0);
if (recipientId.length > 0 && [self.recipientId isEqualToString:recipientId]) {
[self updateProfileName];
[self updateAvatar];
}
}
- (NSAttributedString *)verifiedSubtitle
{
NSMutableAttributedString *text = [NSMutableAttributedString new];
// "checkmark"
[text appendAttributedString:[[NSAttributedString alloc]
initWithString:@"\uf00c "
attributes:@{
NSFontAttributeName :
[UIFont ows_fontAwesomeFont:self.subtitleLabel.font.pointSize],
}]];
[text appendAttributedString:[[NSAttributedString alloc]
initWithString:NSLocalizedString(@"PRIVACY_IDENTITY_IS_VERIFIED_BADGE",
@"Badge indicating that the user is verified.")]];
return [text copy];
}
- (void)setAttributedSubtitle:(nullable NSAttributedString *)attributedSubtitle
{
self.subtitleLabel.attributedText = attributedSubtitle;
}
- (BOOL)hasAccessoryText
{
return self.accessoryMessage.length > 0;
}
- (void)setAccessoryView:(UIView *)accessoryView
{
OWSAssertDebug(accessoryView);
OWSAssertDebug(self.accessoryViewContainer);
OWSAssertDebug(self.accessoryViewContainer.subviews.count < 1);
[self.accessoryViewContainer addSubview:accessoryView];
// Trailing-align the accessory view.
[accessoryView autoPinEdgeToSuperviewMargin:ALEdgeTop];
[accessoryView autoPinEdgeToSuperviewMargin:ALEdgeBottom];
[accessoryView autoPinEdgeToSuperviewMargin:ALEdgeTrailing];
[accessoryView autoPinEdgeToSuperviewMargin:ALEdgeLeading relation:NSLayoutRelationGreaterThanOrEqual];
}
@end
NS_ASSUME_NONNULL_END

View File

@ -1,31 +0,0 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
NS_ASSUME_NONNULL_BEGIN
@class TSThread;
@interface ContactTableViewCell : UITableViewCell
+ (NSString *)reuseIdentifier;
- (void)configureWithRecipientId:(NSString *)recipientId;
- (void)configureWithThread:(TSThread *)thread;
// This method should be called _before_ the configure... methods.
- (void)setAccessoryMessage:(nullable NSString *)accessoryMessage;
// This method should be called _after_ the configure... methods.
- (void)setAttributedSubtitle:(nullable NSAttributedString *)attributedSubtitle;
- (NSAttributedString *)verifiedSubtitle;
- (BOOL)hasAccessoryText;
- (void)ows_setAccessoryView:(UIView *)accessoryView;
@end
NS_ASSUME_NONNULL_END

View File

@ -1,116 +0,0 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "ContactTableViewCell.h"
#import "ContactCellView.h"
#import "OWSTableViewController.h"
#import "UIColor+OWS.h"
#import "UIFont+OWS.h"
#import "UIView+OWS.h"
#import <SignalUtilitiesKit/SignalAccount.h>
NS_ASSUME_NONNULL_BEGIN
@interface ContactTableViewCell ()
@property (nonatomic) ContactCellView *cellView;
@end
#pragma mark -
@implementation ContactTableViewCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(nullable NSString *)reuseIdentifier
{
if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
[self configure];
}
return self;
}
+ (NSString *)reuseIdentifier
{
return NSStringFromClass(self.class);
}
- (void)setAccessoryView:(nullable UIView *)accessoryView
{
OWSFailDebug(@"use ows_setAccessoryView instead.");
}
- (void)configure
{
OWSAssertDebug(!self.cellView);
self.preservesSuperviewLayoutMargins = YES;
self.contentView.preservesSuperviewLayoutMargins = YES;
self.cellView = [ContactCellView new];
[self.contentView addSubview:self.cellView];
[self.cellView autoPinEdgesToSuperviewMargins];
self.cellView.userInteractionEnabled = NO;
}
- (void)configureWithRecipientId:(NSString *)recipientId
{
[OWSTableItem configureCell:self];
[self.cellView configureWithRecipientId:recipientId];
// Force layout, since imageView isn't being initally rendered on App Store optimized build.
[self layoutSubviews];
}
- (void)configureWithThread:(TSThread *)thread
{
OWSAssertDebug(thread);
[OWSTableItem configureCell:self];
[self.cellView configureWithThread:thread];
// Force layout, since imageView isn't being initally rendered on App Store optimized build.
[self layoutSubviews];
}
- (void)setAccessoryMessage:(nullable NSString *)accessoryMessage
{
OWSAssertDebug(self.cellView);
self.cellView.accessoryMessage = accessoryMessage;
}
- (NSAttributedString *)verifiedSubtitle
{
return self.cellView.verifiedSubtitle;
}
- (void)setAttributedSubtitle:(nullable NSAttributedString *)attributedSubtitle
{
[self.cellView setAttributedSubtitle:attributedSubtitle];
}
- (void)prepareForReuse
{
[super prepareForReuse];
[self.cellView prepareForReuse];
self.accessoryType = UITableViewCellAccessoryNone;
}
- (BOOL)hasAccessoryText
{
return [self.cellView hasAccessoryText];
}
- (void)ows_setAccessoryView:(UIView *)accessoryView
{
return [self.cellView setAccessoryView:accessoryView];
}
@end
NS_ASSUME_NONNULL_END