mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
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:
parent
333849c32e
commit
06eef99766
|
@ -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 */,
|
||||
|
|
|
@ -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 {
|
||||
// MARK: - Delegate
|
||||
|
||||
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)
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -1,43 +1,59 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import SessionUIKit
|
||||
|
||||
final class ContextMenuVC: UIViewController {
|
||||
private static let actionViewHeight: CGFloat = 40
|
||||
private static let menuCornerRadius: CGFloat = 8
|
||||
|
||||
final class ContextMenuVC : UIViewController {
|
||||
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: - Initialization
|
||||
|
||||
// MARK: Lifecycle
|
||||
init(snapshot: UIView, viewItem: ConversationViewItem, frame: CGRect, delegate: ContextMenuActionDelegate, dismiss: @escaping () -> Void) {
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -49,32 +65,41 @@ final class ContextMenuVC : UIViewController {
|
|||
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()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import SignalUtilitiesKit
|
|||
extension ConversationVC:
|
||||
InputViewDelegate,
|
||||
MessageCellDelegate,
|
||||
ContextMenuActionDelegate,
|
||||
ScrollToBottomButtonDelegate,
|
||||
SendMediaNavDelegate,
|
||||
UIDocumentPickerDelegate,
|
||||
|
@ -50,8 +51,9 @@ 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,
|
||||
|
@ -59,22 +61,13 @@ extension ConversationVC:
|
|||
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()
|
||||
GRDBStorage.shared.write { db in
|
||||
try Contact
|
||||
.filter(id: publicKey)
|
||||
.updateAll(db, Contact.Columns.isBlocked.set(to: true))
|
||||
|
||||
default: break
|
||||
}
|
||||
}
|
||||
)
|
||||
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()
|
||||
|
@ -502,44 +486,81 @@ extension ConversationVC:
|
|||
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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
@ -33,7 +36,7 @@ final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate,
|
|||
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)
|
||||
|
@ -45,7 +48,6 @@ final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate,
|
|||
return result
|
||||
}()
|
||||
|
||||
|
||||
private lazy var sendButton: InputViewButton = {
|
||||
let result = InputViewButton(icon: #imageLiteral(resourceName: "ArrowUp"), isSendButton: true, delegate: self)
|
||||
result.isHidden = true
|
||||
|
@ -55,22 +57,25 @@ 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
|
||||
}()
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,28 +84,26 @@ 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]
|
||||
}
|
||||
|
||||
|
@ -105,18 +112,22 @@ final class JoinOpenGroupVC : BaseVC, UIPageViewControllerDataSource, UIPageView
|
|||
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)
|
||||
}
|
||||
|
@ -128,24 +139,28 @@ final class JoinOpenGroupVC : BaseVC, UIPageViewControllerDataSource, UIPageView
|
|||
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)
|
||||
|
@ -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
|
||||
|
@ -182,16 +199,19 @@ private final class EnterURLVC : UIViewController, UIGestureRecognizerDelegate,
|
|||
}()
|
||||
|
||||
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,15 +241,18 @@ 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)
|
||||
}
|
||||
|
@ -235,28 +261,32 @@ private final class EnterURLVC : UIViewController, UIGestureRecognizerDelegate,
|
|||
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,22 +295,26 @@ 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
|
||||
}
|
||||
|
@ -292,7 +326,7 @@ private final class ScanQRCodePlaceholderVC : UIViewController {
|
|||
@objc private func requestCameraAccess() {
|
||||
ows_ask(forCameraPermissions: { [weak self] hasCameraAccess in
|
||||
if hasCameraAccess {
|
||||
self?.joinOpenGroupVC.handleCameraAccessGranted()
|
||||
self?.joinOpenGroupVC?.handleCameraAccessGranted()
|
||||
} else {
|
||||
// Do nothing
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -4,112 +4,122 @@ import UIKit
|
|||
import SessionUIKit
|
||||
import SignalUtilitiesKit
|
||||
|
||||
final class ConversationCell : UITableViewCell {
|
||||
static let reuseIdentifier = "ConversationCell"
|
||||
final class ConversationCell: UITableViewCell {
|
||||
// MARK: - UI
|
||||
|
||||
// MARK: UI Components
|
||||
private let accentLineView = UIView()
|
||||
private let accentLineView: UIView = 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])
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 it’s 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)
|
||||
}
|
||||
}
|
|
@ -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:
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -96,9 +96,22 @@ 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")
|
||||
|
@ -106,66 +119,6 @@ extension MessageReceiver {
|
|||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
private static func handleDataExtractionNotification(_ db: Database, message: DataExtractionNotification) throws {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
public class TypingIndicators {
|
||||
// MARK: - Direction
|
||||
|
||||
@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 enum Direction {
|
||||
case outgoing
|
||||
case incoming
|
||||
}
|
||||
|
||||
private func setup() {
|
||||
_areTypingIndicatorsEnabled = OWSPrimaryStorage.shared().dbReadConnection.bool(forKey: kDatabaseKey_TypingIndicatorsEnabled, inCollection: kDatabaseCollection, defaultValue: false)
|
||||
}
|
||||
private class Indicator {
|
||||
fileprivate let thread: SessionThread
|
||||
fileprivate let direction: Direction
|
||||
fileprivate let timestampMs: Int64
|
||||
|
||||
// MARK: -
|
||||
fileprivate var refreshTimer: Timer?
|
||||
fileprivate var stopTimer: Timer?
|
||||
|
||||
@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
|
||||
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: -
|
||||
// Don't send typing indicators in group threads
|
||||
guard thread.variant != .closedGroup && thread.variant != .openGroup else { return nil }
|
||||
|
||||
// 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
|
||||
self.thread = thread
|
||||
self.direction = direction
|
||||
self.timestampMs = (timestampMs ?? Int64(floor(Date().timeIntervalSince1970 * 1000)))
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
fileprivate func starting(_ db: Database) -> Indicator {
|
||||
let thread: SessionThread = self.thread
|
||||
let direction: Direction = self.direction
|
||||
let timestampMs: Int64 = self.timestampMs
|
||||
|
||||
func didStartTypingOutgoingInput() {
|
||||
if sendRefreshTimer == nil {
|
||||
// If the user types a character into the compose box, and the sendRefresh timer isn’t running:
|
||||
// Start the typing indicator
|
||||
switch direction {
|
||||
case .outgoing:
|
||||
scheduleRefreshCallback(db, shouldSend: (refreshTimer == nil))
|
||||
|
||||
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:
|
||||
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
|
||||
}
|
||||
// `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
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
if thread.isGroupThread() { return } // Don't send typing indicators in group threads
|
||||
return self
|
||||
}
|
||||
|
||||
let typingIndicator = TypingIndicator()
|
||||
typingIndicator.kind = action
|
||||
SNMessagingKitConfiguration.shared.storage.write { transaction in
|
||||
MessageSender.send(typingIndicator, in: thread, using: transaction as! YapDatabaseReadWriteTransaction)
|
||||
@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)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
private func scheduleRefreshCallback(_ db: Database, shouldSend: Bool = true) {
|
||||
if shouldSend {
|
||||
try? MessageSender.send(
|
||||
db,
|
||||
message: TypingIndicator(kind: .started),
|
||||
interactionId: nil,
|
||||
in: thread
|
||||
)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
public static func didStopTyping(_ db: Database, in thread: SessionThread, direction: Direction) {
|
||||
switch direction {
|
||||
case .outgoing:
|
||||
let updatedIndicator: Indicator? = outgoing.wrappedValue[thread.id]?.stoping(db)
|
||||
|
||||
func didReceiveTypingStoppedMessage() {
|
||||
clearTyping()
|
||||
}
|
||||
outgoing.mutate { $0[thread.id] = updatedIndicator }
|
||||
|
||||
@objc
|
||||
func displayTypingTimerDidFire() {
|
||||
clearTyping()
|
||||
}
|
||||
case .incoming:
|
||||
let updatedIndicator: Indicator? = incoming.wrappedValue[thread.id]?.stoping(db)
|
||||
|
||||
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)
|
||||
incoming.mutate { $0[thread.id] = updatedIndicator }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) }
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
Loading…
Reference in a new issue