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

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

View file

@ -335,8 +335,6 @@
C32C5CAD256DD1DF003C73A2 /* TSAccountManager.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB94255A581300E217F9 /* TSAccountManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; 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 */; }; C32C5CF0256DD3E4003C73A2 /* Storage+Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3F0A5FD255C988A007BE2A3 /* Storage+Shared.swift */; };
C32C5D19256DD493003C73A2 /* OWSLinkPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBA8255A581500E217F9 /* OWSLinkPreview.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 */; }; C32C5D83256DD5B6003C73A2 /* SSKKeychainStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBBC255A581600E217F9 /* SSKKeychainStorage.swift */; };
C32C5DBF256DD743003C73A2 /* ClosedGroupPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB34255A580B00E217F9 /* ClosedGroupPoller.swift */; }; C32C5DBF256DD743003C73A2 /* ClosedGroupPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB34255A580B00E217F9 /* ClosedGroupPoller.swift */; };
C32C5DC0256DD743003C73A2 /* Poller.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB3A255A580B00E217F9 /* Poller.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, ); }; }; 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 */; }; C33FDC98255A582000E217F9 /* SwiftSingletons.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDADE255A580400E217F9 /* SwiftSingletons.swift */; };
C33FDC9A255A582000E217F9 /* ByteParser.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAE0255A580400E217F9 /* ByteParser.m */; }; 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 */; }; C33FDCC7255A582000E217F9 /* NSArray+OWS.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB0D255A580800E217F9 /* NSArray+OWS.m */; };
C33FDCD1255A582000E217F9 /* FunctionalUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB17255A580800E217F9 /* FunctionalUtil.m */; }; C33FDCD1255A582000E217F9 /* FunctionalUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB17255A580800E217F9 /* FunctionalUtil.m */; };
C33FDCD3255A582000E217F9 /* GroupUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB19255A580900E217F9 /* GroupUtilities.swift */; }; 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, ); }; }; 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, ); }; }; 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 */; }; 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 */; }; 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, ); }; }; 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, ); }; }; 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 */; }; 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, ); }; }; 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 */; }; 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, ); }; }; 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, ); }; }; 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 */; }; 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 */; }; C38EF400255B6DF7007E1867 /* GalleryRailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF3E2255B6DF3007E1867 /* GalleryRailView.swift */; };
C38EF401255B6DF7007E1867 /* VideoPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF3E3255B6DF4007E1867 /* VideoPlayerView.swift */; }; C38EF401255B6DF7007E1867 /* VideoPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF3E3255B6DF4007E1867 /* VideoPlayerView.swift */; };
C38EF402255B6DF7007E1867 /* CommonStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF3E4255B6DF4007E1867 /* CommonStrings.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 */; }; C38EF405255B6DF7007E1867 /* OWSButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF3E7255B6DF5007E1867 /* OWSButton.swift */; };
C38EF407255B6DF7007E1867 /* Toast.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF3E9255B6DF6007E1867 /* Toast.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 */; }; C38EF40A255B6DF7007E1867 /* OWSFlatButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF3EC255B6DF6007E1867 /* OWSFlatButton.swift */; };
C38EF40B255B6DF7007E1867 /* TappableStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF3ED255B6DF6007E1867 /* TappableStackView.swift */; }; C38EF40B255B6DF7007E1867 /* TappableStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF3ED255B6DF6007E1867 /* TappableStackView.swift */; };
C38EF40C255B6DF7007E1867 /* GradientView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF3EE255B6DF6007E1867 /* GradientView.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 */; }; FD09C5E428237209000CE219 /* MessageRequestsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09C5E328237209000CE219 /* MessageRequestsViewModel.swift */; };
FD09C5E628260FF9000CE219 /* MediaGalleryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09C5E528260FF9000CE219 /* MediaGalleryViewModel.swift */; }; FD09C5E628260FF9000CE219 /* MediaGalleryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09C5E528260FF9000CE219 /* MediaGalleryViewModel.swift */; };
FD09C5E828264937000CE219 /* MediaDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09C5E728264937000CE219 /* MediaDetailViewController.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 */; }; FD17D79927F40AB800122BE0 /* _003_YDBToGRDBMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D79827F40AB800122BE0 /* _003_YDBToGRDBMigration.swift */; };
FD17D79C27F40B2E00122BE0 /* SMKLegacyModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D79B27F40B2E00122BE0 /* SMKLegacyModels.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 */; }; 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 */; }; FDF0B7472804F0CE004C14C5 /* DisappearingMessagesJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B7462804F0CE004C14C5 /* DisappearingMessagesJob.swift */; };
FDF0B74928060D13004C14C5 /* QuotedReplyModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B74828060D13004C14C5 /* QuotedReplyModel.swift */; }; FDF0B74928060D13004C14C5 /* QuotedReplyModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B74828060D13004C14C5 /* QuotedReplyModel.swift */; };
FDF0B74B28061F7A004C14C5 /* InteractionAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B74A28061F7A004C14C5 /* InteractionAttachment.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 */; }; FDF0B74F28079E5E004C14C5 /* SendReadReceiptsJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B74E28079E5E004C14C5 /* SendReadReceiptsJob.swift */; };
FDF0B7512807BA56004C14C5 /* NotificationsProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B7502807BA56004C14C5 /* NotificationsProtocol.swift */; }; FDF0B7512807BA56004C14C5 /* NotificationsProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B7502807BA56004C14C5 /* NotificationsProtocol.swift */; };
FDF0B7552807C4BB004C14C5 /* SSKEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B7542807C4BB004C14C5 /* SSKEnvironment.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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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; }; 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; }; 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; }; 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; }; 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; }; 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; }; 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; }; 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; }; 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; }; 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; }; 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; }; 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; }; 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; }; 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; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; FDF0B7542807C4BB004C14C5 /* SSKEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSKEnvironment.swift; sourceTree = "<group>"; };
@ -2521,7 +2505,6 @@
C3D9E3B52567685D0040E4F3 /* Attachments */, C3D9E3B52567685D0040E4F3 /* Attachments */,
B8F5F61925EDE4B0003BF8D4 /* Data Extraction */, B8F5F61925EDE4B0003BF8D4 /* Data Extraction */,
C32C5D22256DD496003C73A2 /* Link Previews */, C32C5D22256DD496003C73A2 /* Link Previews */,
C32C5D2D256DD4C4003C73A2 /* Mentions */,
C379DC6825672B5E0002D4EB /* Notifications */, C379DC6825672B5E0002D4EB /* Notifications */,
C32C59F8256DB5A6003C73A2 /* Pollers */, C32C59F8256DB5A6003C73A2 /* Pollers */,
C32C5B1B256DC160003C73A2 /* Quotes */, C32C5B1B256DC160003C73A2 /* Quotes */,
@ -2712,15 +2695,6 @@
path = "Link Previews"; path = "Link Previews";
sourceTree = "<group>"; sourceTree = "<group>";
}; };
C32C5D2D256DD4C4003C73A2 /* Mentions */ = {
isa = PBXGroup;
children = (
C33FDA7E255A57FB00E217F9 /* Mention.swift */,
C33FDA81255A57FC00E217F9 /* MentionsManager.swift */,
);
path = Mentions;
sourceTree = "<group>";
};
C331FF1C2558F9D300070591 /* SessionUIKit */ = { C331FF1C2558F9D300070591 /* SessionUIKit */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -3097,10 +3071,6 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
C33FDB19255A580900E217F9 /* GroupUtilities.swift */, C33FDB19255A580900E217F9 /* GroupUtilities.swift */,
C38EF3E5255B6DF4007E1867 /* ContactCellView.h */,
C38EF3D6255B6DEF007E1867 /* ContactCellView.m */,
C38EF3E6255B6DF4007E1867 /* ContactTableViewCell.h */,
C38EF3EB255B6DF6007E1867 /* ContactTableViewCell.m */,
C38EF2D2255B6DAF007E1867 /* OWSProfileManager.h */, C38EF2D2255B6DAF007E1867 /* OWSProfileManager.h */,
C38EF2CF255B6DAE007E1867 /* OWSProfileManager.m */, C38EF2CF255B6DAE007E1867 /* OWSProfileManager.m */,
); );
@ -3117,8 +3087,6 @@
C38EF2E4255B6DB9007E1867 /* FullTextSearcher.swift */, C38EF2E4255B6DB9007E1867 /* FullTextSearcher.swift */,
C38EF2E9255B6DBA007E1867 /* OWSUnreadIndicator.h */, C38EF2E9255B6DBA007E1867 /* OWSUnreadIndicator.h */,
C38EF2E3255B6DB9007E1867 /* OWSUnreadIndicator.m */, C38EF2E3255B6DB9007E1867 /* OWSUnreadIndicator.m */,
C33FDAE8255A580500E217F9 /* OWSMessageUtils.h */,
C33FDBD7255A581900E217F9 /* OWSMessageUtils.m */,
); );
path = Messaging; path = Messaging;
sourceTree = "<group>"; sourceTree = "<group>";
@ -3598,6 +3566,7 @@
FD09799827FFC1A300936362 /* Attachment.swift */, FD09799827FFC1A300936362 /* Attachment.swift */,
FD09799A27FFC82D00936362 /* Quote.swift */, FD09799A27FFC82D00936362 /* Quote.swift */,
FDF0B73B27FFD3D6004C14C5 /* LinkPreview.swift */, FDF0B73B27FFD3D6004C14C5 /* LinkPreview.swift */,
FD09C5E9282A1BB2000CE219 /* ThreadTypingIndicator.swift */,
FDE77F6A280FEB28002CFC5D /* ControlMessageProcessRecord.swift */, FDE77F6A280FEB28002CFC5D /* ControlMessageProcessRecord.swift */,
); );
path = Models; path = Models;
@ -3687,7 +3656,6 @@
FD17D7C227F5204C00122BE0 /* Database+Utilities.swift */, FD17D7C227F5204C00122BE0 /* Database+Utilities.swift */,
FD17D7C627F5207C00122BE0 /* DatabaseMigrator+Utilities.swift */, FD17D7C627F5207C00122BE0 /* DatabaseMigrator+Utilities.swift */,
FD17D7BC27F51F6900122BE0 /* GRDB+Notifications.swift */, FD17D7BC27F51F6900122BE0 /* GRDB+Notifications.swift */,
FDF0B74C280664E9004C14C5 /* PersistableRecord+Utilities.swift */,
FDF22210281B5E0B000A4995 /* TableRecord+Utilities.swift */, FDF22210281B5E0B000A4995 /* TableRecord+Utilities.swift */,
FDF2220E281B55E6000A4995 /* QueryInterfaceRequest+Utilities.swift */, FDF2220E281B55E6000A4995 /* QueryInterfaceRequest+Utilities.swift */,
); );
@ -3838,11 +3806,9 @@
C33FDDD3255A582000E217F9 /* OWSQueues.h in Headers */, C33FDDD3255A582000E217F9 /* OWSQueues.h in Headers */,
C33FDC96255A582000E217F9 /* NSObject+Casting.h in Headers */, C33FDC96255A582000E217F9 /* NSObject+Casting.h in Headers */,
C33FDDB3255A582000E217F9 /* OWSError.h in Headers */, C33FDDB3255A582000E217F9 /* OWSError.h in Headers */,
C38EF403255B6DF7007E1867 /* ContactCellView.h in Headers */,
C33FDD68255A582000E217F9 /* SignalAccount.h in Headers */, C33FDD68255A582000E217F9 /* SignalAccount.h in Headers */,
C38EF35E255B6DCC007E1867 /* OWSViewController.h in Headers */, C38EF35E255B6DCC007E1867 /* OWSViewController.h in Headers */,
C37F5396255B95BD002AEA92 /* OWSAnyTouchGestureRecognizer.h in Headers */, C37F5396255B95BD002AEA92 /* OWSAnyTouchGestureRecognizer.h in Headers */,
C38EF404255B6DF7007E1867 /* ContactTableViewCell.h in Headers */,
C33FDDB2255A582000E217F9 /* NSArray+OWS.h in Headers */, C33FDDB2255A582000E217F9 /* NSArray+OWS.h in Headers */,
C38EF2D7255B6DAF007E1867 /* OWSProfileManager.h in Headers */, C38EF2D7255B6DAF007E1867 /* OWSProfileManager.h in Headers */,
C38EF367255B6DCC007E1867 /* OWSTableViewController.h in Headers */, C38EF367255B6DCC007E1867 /* OWSTableViewController.h in Headers */,
@ -3850,7 +3816,6 @@
C33FD9AF255A548A00E217F9 /* SignalUtilitiesKit.h in Headers */, C33FD9AF255A548A00E217F9 /* SignalUtilitiesKit.h in Headers */,
C33FDC50255A582000E217F9 /* OWSDispatch.h in Headers */, C33FDC50255A582000E217F9 /* OWSDispatch.h in Headers */,
C33FDD06255A582000E217F9 /* AppVersion.h in Headers */, C33FDD06255A582000E217F9 /* AppVersion.h in Headers */,
C33FDCA2255A582000E217F9 /* OWSMessageUtils.h in Headers */,
C38EF28F255B6D86007E1867 /* VersionMigrations.h in Headers */, C38EF28F255B6D86007E1867 /* VersionMigrations.h in Headers */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@ -4657,7 +4622,6 @@
C33FDC29255A581F00E217F9 /* ReachabilityManager.swift in Sources */, C33FDC29255A581F00E217F9 /* ReachabilityManager.swift in Sources */,
C38EF407255B6DF7007E1867 /* Toast.swift in Sources */, C38EF407255B6DF7007E1867 /* Toast.swift in Sources */,
C38EF38C255B6DD2007E1867 /* ApprovalRailCellView.swift in Sources */, C38EF38C255B6DD2007E1867 /* ApprovalRailCellView.swift in Sources */,
C38EF409255B6DF7007E1867 /* ContactTableViewCell.m in Sources */,
FD3C907327E8387300CD579F /* OpenGroupServerIdLookupMigration.swift in Sources */, FD3C907327E8387300CD579F /* OpenGroupServerIdLookupMigration.swift in Sources */,
C38EF32A255B6DBF007E1867 /* UIUtil.m in Sources */, C38EF32A255B6DBF007E1867 /* UIUtil.m in Sources */,
C38EF2A6255B6D93007E1867 /* PlaceholderIcon.swift in Sources */, C38EF2A6255B6D93007E1867 /* PlaceholderIcon.swift in Sources */,
@ -4705,7 +4669,6 @@
C33FDC9A255A582000E217F9 /* ByteParser.m in Sources */, C33FDC9A255A582000E217F9 /* ByteParser.m in Sources */,
C33FDDC0255A582000E217F9 /* SignalAccount.m in Sources */, C33FDDC0255A582000E217F9 /* SignalAccount.m in Sources */,
C33FDC58255A582000E217F9 /* ReverseDispatchQueue.swift in Sources */, C33FDC58255A582000E217F9 /* ReverseDispatchQueue.swift in Sources */,
C38EF3F4255B6DF7007E1867 /* ContactCellView.m in Sources */,
C33FDC78255A582000E217F9 /* TSConstants.m in Sources */, C33FDC78255A582000E217F9 /* TSConstants.m in Sources */,
C38EF324255B6DBF007E1867 /* Bench.swift in Sources */, C38EF324255B6DBF007E1867 /* Bench.swift in Sources */,
FDCDB8DE2810F73B00352A0C /* Differentiable+Utilities.swift in Sources */, FDCDB8DE2810F73B00352A0C /* Differentiable+Utilities.swift in Sources */,
@ -4721,7 +4684,6 @@
FD88BADB27A750F200BBC442 /* MessageRequestsMigration.swift in Sources */, FD88BADB27A750F200BBC442 /* MessageRequestsMigration.swift in Sources */,
C38EF326255B6DBF007E1867 /* ConversationStyle.swift in Sources */, C38EF326255B6DBF007E1867 /* ConversationStyle.swift in Sources */,
C38EF3B8255B6DE7007E1867 /* ImageEditorTextViewController.swift in Sources */, C38EF3B8255B6DE7007E1867 /* ImageEditorTextViewController.swift in Sources */,
C33FDD91255A582000E217F9 /* OWSMessageUtils.m in Sources */,
B8C2B332256376F000551B4D /* ThreadUtil.m in Sources */, B8C2B332256376F000551B4D /* ThreadUtil.m in Sources */,
C38EF40B255B6DF7007E1867 /* TappableStackView.swift in Sources */, C38EF40B255B6DF7007E1867 /* TappableStackView.swift in Sources */,
C38EF31D255B6DBF007E1867 /* UIImage+OWS.swift in Sources */, C38EF31D255B6DBF007E1867 /* UIImage+OWS.swift in Sources */,
@ -4796,7 +4758,6 @@
FD09C5E2282212B3000CE219 /* JobDependencies.swift in Sources */, FD09C5E2282212B3000CE219 /* JobDependencies.swift in Sources */,
FD09796727F6B0B600936362 /* Sodium+Conversion.swift in Sources */, FD09796727F6B0B600936362 /* Sodium+Conversion.swift in Sources */,
FD9004122818ABDC00ABAAF6 /* Job.swift in Sources */, FD9004122818ABDC00ABAAF6 /* Job.swift in Sources */,
FDF0B74D280664E9004C14C5 /* PersistableRecord+Utilities.swift in Sources */,
FD09797927FAB7E800936362 /* ImageFormat.swift in Sources */, FD09797927FAB7E800936362 /* ImageFormat.swift in Sources */,
C32C5DC9256DD935003C73A2 /* ProxiedContentDownloader.swift in Sources */, C32C5DC9256DD935003C73A2 /* ProxiedContentDownloader.swift in Sources */,
C3D9E4C02567767F0040E4F3 /* DataSource.m in Sources */, C3D9E4C02567767F0040E4F3 /* DataSource.m in Sources */,
@ -4937,7 +4898,6 @@
C3A71D0B2558989C0043A11F /* MessageWrapper.swift in Sources */, C3A71D0B2558989C0043A11F /* MessageWrapper.swift in Sources */,
FDE77F6B280FEB28002CFC5D /* ControlMessageProcessRecord.swift in Sources */, FDE77F6B280FEB28002CFC5D /* ControlMessageProcessRecord.swift in Sources */,
B8F5F60325EDE16F003BF8D4 /* DataExtractionNotification.swift in Sources */, B8F5F60325EDE16F003BF8D4 /* DataExtractionNotification.swift in Sources */,
C32C5D24256DD4C0003C73A2 /* MentionsManager.swift in Sources */,
C3A71D1E25589AC30043A11F /* WebSocketProto.swift in Sources */, C3A71D1E25589AC30043A11F /* WebSocketProto.swift in Sources */,
C3C2A7852553AAF300C340D1 /* SessionProtos.pb.swift in Sources */, C3C2A7852553AAF300C340D1 /* SessionProtos.pb.swift in Sources */,
B8566C63256F55930045A0B9 /* OWSLinkPreview+Conversion.swift in Sources */, B8566C63256F55930045A0B9 /* OWSLinkPreview+Conversion.swift in Sources */,
@ -5014,7 +4974,6 @@
C3A3A13C256E1B27004D228D /* OWSMediaGalleryFinder.m in Sources */, C3A3A13C256E1B27004D228D /* OWSMediaGalleryFinder.m in Sources */,
FD09797027FA6FF300936362 /* Profile.swift in Sources */, FD09797027FA6FF300936362 /* Profile.swift in Sources */,
C32C5AAE256DBE8F003C73A2 /* TSInteraction.m in Sources */, C32C5AAE256DBE8F003C73A2 /* TSInteraction.m in Sources */,
C32C5D23256DD4C0003C73A2 /* Mention.swift in Sources */,
C32C5FD6256E0346003C73A2 /* Notification+Thread.swift in Sources */, C32C5FD6256E0346003C73A2 /* Notification+Thread.swift in Sources */,
FD09798B27FD1CFE00936362 /* Capability.swift in Sources */, FD09798B27FD1CFE00936362 /* Capability.swift in Sources */,
C3BBE0C72554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift in Sources */, C3BBE0C72554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift in Sources */,
@ -5026,6 +4985,7 @@
C32C5A75256DBBCF003C73A2 /* TSAttachmentPointer+Conversion.swift in Sources */, C32C5A75256DBBCF003C73A2 /* TSAttachmentPointer+Conversion.swift in Sources */,
C32C5EBA256DE130003C73A2 /* OWSQuotedReplyModel.m in Sources */, C32C5EBA256DE130003C73A2 /* OWSQuotedReplyModel.m in Sources */,
C32C59C4256DB41F003C73A2 /* TSContactThread.m in Sources */, C32C59C4256DB41F003C73A2 /* TSContactThread.m in Sources */,
FD09C5EA282A1BB2000CE219 /* ThreadTypingIndicator.swift in Sources */,
FDF0B75E280AAF35004C14C5 /* Preferences.swift in Sources */, FDF0B75E280AAF35004C14C5 /* Preferences.swift in Sources */,
C32C5AB0256DBE8F003C73A2 /* TSOutgoingMessage.m in Sources */, C32C5AB0256DBE8F003C73A2 /* TSOutgoingMessage.m in Sources */,
B82A0C3826B9098200C1BCE3 /* MessageInvalidator.swift in Sources */, B82A0C3826B9098200C1BCE3 /* MessageInvalidator.swift in Sources */,

View file

@ -1,98 +1,127 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
extension ContextMenuVC { extension ContextMenuVC {
struct Action { struct Action {
let icon: UIImage let icon: UIImage?
let title: String let title: String
let work: () -> Void let work: () -> Void
static func reply(_ viewItem: ConversationViewItem, _ delegate: ContextMenuActionDelegate?) -> Action { static func reply(_ item: ConversationViewModel.Item, _ delegate: ContextMenuActionDelegate?) -> Action {
let title = NSLocalizedString("context_menu_reply", comment: "") return Action(
return Action(icon: UIImage(named: "ic_reply")!, title: title) { delegate?.reply(viewItem) } icon: UIImage(named: "ic_reply"),
title: "context_menu_reply".localized()
) { delegate?.reply(item) }
} }
static func copy(_ viewItem: ConversationViewItem, _ delegate: ContextMenuActionDelegate?) -> Action { static func copy(_ item: ConversationViewModel.Item, _ delegate: ContextMenuActionDelegate?) -> Action {
let title = NSLocalizedString("copy", comment: "") return Action(
return Action(icon: UIImage(named: "ic_copy")!, title: title) { delegate?.copy(viewItem) } icon: UIImage(named: "ic_copy"),
title: "copy".localized()
) { delegate?.copy(item) }
} }
static func copySessionID(_ viewItem: ConversationViewItem, _ delegate: ContextMenuActionDelegate?) -> Action { static func copySessionID(_ item: ConversationViewModel.Item, _ delegate: ContextMenuActionDelegate?) -> Action {
let title = NSLocalizedString("vc_conversation_settings_copy_session_id_button_title", comment: "") return Action(
return Action(icon: UIImage(named: "ic_copy")!, title: title) { delegate?.copySessionID(viewItem) } 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 { static func delete(_ item: ConversationViewModel.Item, _ delegate: ContextMenuActionDelegate?) -> Action {
let title = NSLocalizedString("TXT_DELETE_TITLE", comment: "") return Action(
return Action(icon: UIImage(named: "ic_trash")!, title: title) { delegate?.delete(viewItem) } icon: UIImage(named: "ic_trash"),
title: "TXT_DELETE_TITLE".localized()
) { delegate?.delete(item) }
} }
static func save(_ viewItem: ConversationViewItem, _ delegate: ContextMenuActionDelegate?) -> Action { static func save(_ item: ConversationViewModel.Item, _ delegate: ContextMenuActionDelegate?) -> Action {
let title = NSLocalizedString("context_menu_save", comment: "") return Action(
return Action(icon: UIImage(named: "ic_download")!, title: title) { delegate?.save(viewItem) } icon: UIImage(named: "ic_download"),
title: "context_menu_save".localized()
) { delegate?.save(item) }
} }
static func ban(_ viewItem: ConversationViewItem, _ delegate: ContextMenuActionDelegate?) -> Action { static func ban(_ item: ConversationViewModel.Item, _ delegate: ContextMenuActionDelegate?) -> Action {
let title = NSLocalizedString("context_menu_ban_user", comment: "") return Action(
return Action(icon: UIImage(named: "ic_block")!, title: title) { delegate?.ban(viewItem) } icon: UIImage(named: "ic_block"),
title: "context_menu_ban_user".localized()
) { delegate?.ban(item) }
} }
static func banAndDeleteAllMessages(_ viewItem: ConversationViewItem, _ delegate: ContextMenuActionDelegate?) -> Action { static func banAndDeleteAllMessages(_ item: ConversationViewModel.Item, _ delegate: ContextMenuActionDelegate?) -> Action {
let title = NSLocalizedString("context_menu_ban_and_delete_all", comment: "") return Action(
return Action(icon: UIImage(named: "ic_block")!, title: title) { delegate?.banAndDeleteAllMessages(viewItem) } 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] { static func actions(for item: ConversationViewModel.Item, currentUserIsOpenGroupModerator: Bool, delegate: ContextMenuActionDelegate?) -> [Action]? {
func isReplyingAllowed() -> Bool { // No context items for info messages
guard let message = viewItem.interaction as? TSOutgoingMessage else { return true } guard item.interactionVariant == .standardOutgoing || item.interactionVariant == .standardIncoming else {
switch message.messageState { return nil
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 []
} }
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 // MARK: - Delegate
protocol ContextMenuActionDelegate : AnyObject {
protocol ContextMenuActionDelegate {
func reply(_ viewItem: ConversationViewItem) func reply(_ item: ConversationViewModel.Item)
func copy(_ viewItem: ConversationViewItem) func copy(_ item: ConversationViewModel.Item)
func copySessionID(_ viewItem: ConversationViewItem) func copySessionID(_ item: ConversationViewModel.Item)
func delete(_ viewItem: ConversationViewItem) func delete(_ item: ConversationViewModel.Item)
func save(_ viewItem: ConversationViewItem) func save(_ item: ConversationViewModel.Item)
func ban(_ viewItem: ConversationViewItem) func ban(_ item: ConversationViewModel.Item)
func banAndDeleteAllMessages(_ viewItem: ConversationViewItem) func banAndDeleteAllMessages(_ item: ConversationViewModel.Item)
} }

View file

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

View file

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

View file

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

View file

@ -39,9 +39,8 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
var contextMenuVC: ContextMenuVC? var contextMenuVC: ContextMenuVC?
// Mentions // Mentions
var oldText = ""
var currentMentionStartIndex: String.Index? var currentMentionStartIndex: String.Index?
var mentions: [Mention] = [] var mentions: [ConversationViewModel.MentionInfo] = []
// Scrolling & paging // Scrolling & paging
var isUserScrolling = false var isUserScrolling = false
@ -321,6 +320,13 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
// Nav bar // Nav bar
setUpNavBarStyle() setUpNavBarStyle()
navigationItem.titleView = titleView 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) updateNavBarButtons(viewData: viewModel.viewData)
// Constraints // Constraints

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1215,8 +1215,8 @@ enum _003_YDBToGRDBMigration: Migration {
legacyPreferences[key] = object legacyPreferences[key] = object
} }
// Note: The 'int(forKey:inCollection:)' defaults to `0` which is an incorrect value for the notification // Note: The 'int(forKey:inCollection:)' defaults to `0` which is an incorrect value
// sound so catch it and default // for the notification sound so catch it and default
let globalNotificationSoundValue: Int32 = transaction.int( let globalNotificationSoundValue: Int32 = transaction.int(
forKey: Legacy.soundsGlobalNotificationKey, forKey: Legacy.soundsGlobalNotificationKey,
inCollection: Legacy.soundsStorageNotificationCollection inCollection: Legacy.soundsStorageNotificationCollection
@ -1226,17 +1226,17 @@ enum _003_YDBToGRDBMigration: Migration {
Preferences.Sound.defaultNotificationSound.rawValue Preferences.Sound.defaultNotificationSound.rawValue
) )
legacyPreferences[Legacy.readReceiptManagerAreReadReceiptsEnabled] = (transaction.bool( legacyPreferences[Legacy.readReceiptManagerAreReadReceiptsEnabled] = transaction.bool(
forKey: Legacy.readReceiptManagerAreReadReceiptsEnabled, forKey: Legacy.readReceiptManagerAreReadReceiptsEnabled,
inCollection: Legacy.readReceiptManagerCollection, inCollection: Legacy.readReceiptManagerCollection,
defaultValue: false defaultValue: false
) ? 1 : 0) )
legacyPreferences[Legacy.typingIndicatorsEnabledKey] = (transaction.bool( legacyPreferences[Legacy.typingIndicatorsEnabledKey] = transaction.bool(
forKey: Legacy.typingIndicatorsEnabledKey, forKey: Legacy.typingIndicatorsEnabledKey,
inCollection: Legacy.typingIndicatorsCollection, inCollection: Legacy.typingIndicatorsCollection,
defaultValue: false defaultValue: false
) ? 1 : 0) )
} }
db[.preferencesNotificationPreviewType] = Preferences.NotificationPreviewType(rawValue: legacyPreferences[Legacy.preferencesKeyNotificationPreviewType] as? Int ?? -1) db[.preferencesNotificationPreviewType] = Preferences.NotificationPreviewType(rawValue: legacyPreferences[Legacy.preferencesKeyNotificationPreviewType] as? Int ?? -1)
@ -1292,6 +1292,15 @@ enum _003_YDBToGRDBMigration: Migration {
return nil 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 = { let state: Attachment.State = {
switch legacyAttachment { switch legacyAttachment {
case let stream as Legacy.AttachmentStream: // Outgoing or already downloaded case let stream as Legacy.AttachmentStream: // Outgoing or already downloaded
@ -1307,7 +1316,22 @@ enum _003_YDBToGRDBMigration: Migration {
let size: CGSize = { let size: CGSize = {
switch legacyAttachment { switch legacyAttachment {
case let stream as Legacy.AttachmentStream: 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 return .zero
} }
@ -1328,7 +1352,7 @@ enum _003_YDBToGRDBMigration: Migration {
let originalFilePath: String = Attachment.originalFilePath( let originalFilePath: String = Attachment.originalFilePath(
id: legacyAttachmentId, id: legacyAttachmentId,
mimeType: stream.contentType, mimeType: stream.contentType,
sourceFilename: stream.localRelativeFilePath sourceFilename: stream.sourceFilename
) )
else { else {
return (false, nil) return (false, nil)
@ -1341,6 +1365,7 @@ enum _003_YDBToGRDBMigration: Migration {
let (isValid, duration): (Bool, TimeInterval?) = Attachment.determineValidityAndDuration( let (isValid, duration): (Bool, TimeInterval?) = Attachment.determineValidityAndDuration(
contentType: stream.contentType, contentType: stream.contentType,
localRelativeFilePath: processedLocalRelativeFilePath,
originalFilePath: originalFilePath originalFilePath: originalFilePath
) )
@ -1376,9 +1401,11 @@ enum _003_YDBToGRDBMigration: Migration {
state: state, state: state,
contentType: legacyAttachment.contentType, contentType: legacyAttachment.contentType,
byteCount: UInt(legacyAttachment.byteCount), byteCount: UInt(legacyAttachment.byteCount),
creationTimestamp: (legacyAttachment as? Legacy.AttachmentStream)?.creationTimestamp.timeIntervalSince1970, creationTimestamp: (legacyAttachment as? Legacy.AttachmentStream)?
.creationTimestamp.timeIntervalSince1970,
sourceFilename: legacyAttachment.sourceFilename, sourceFilename: legacyAttachment.sourceFilename,
downloadUrl: legacyAttachment.downloadURL, downloadUrl: legacyAttachment.downloadURL,
localRelativeFilePath: processedLocalRelativeFilePath,
width: (size == .zero ? nil : UInt(size.width)), width: (size == .zero ? nil : UInt(size.width)),
height: (size == .zero ? nil : UInt(size.height)), height: (size == .zero ? nil : UInt(size.height)),
duration: duration, duration: duration,

View file

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

View file

@ -78,13 +78,14 @@ public struct ControlMessageProcessRecord: Codable, FetchableRecord, Persistable
// causing a unique constraint violation // causing a unique constraint violation
if isRetry { return nil } 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 following situation:
// The app performed a background poll or received a push notification // The app performed a background poll or received a push notification
// This method was invoked and the received message timestamps table was updated // This method was invoked and the received message timestamps table was updated
// Processing wasn't finished // Processing wasn't finished
// The user doesn't see the new closed group // The user doesn't see the new closed group
if case .new = (message as? ClosedGroupControlMessage)?.kind { return nil } 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 // 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 // can happen in a number of situations, primarily with sync messages though hence

View file

@ -14,6 +14,10 @@ public struct SessionThread: Codable, Identifiable, Equatable, FetchableRecord,
using: DisappearingMessagesConfiguration.threadForeignKey using: DisappearingMessagesConfiguration.threadForeignKey
) )
public static let interactions = hasMany(Interaction.self, using: Interaction.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 typealias Columns = CodingKeys
public enum CodingKeys: String, CodingKey, ColumnExpression { public enum CodingKeys: String, CodingKey, ColumnExpression {
@ -90,6 +94,10 @@ public struct SessionThread: Codable, Identifiable, Equatable, FetchableRecord,
request(for: SessionThread.interactions) request(for: SessionThread.interactions)
} }
public var typingIndicator: QueryInterfaceRequest<ThreadTypingIndicator> {
request(for: SessionThread.typingIndicator)
}
// MARK: - Initialization // MARK: - Initialization
public init( public init(
@ -122,6 +130,13 @@ public struct SessionThread: Codable, Identifiable, Equatable, FetchableRecord,
.filter(Job.Columns.threadId == id) .filter(Job.Columns.threadId == id)
.deleteAll(db) .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) return try performDelete(db)
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -24,13 +24,13 @@ extension MessageSender {
let membersAsData = members.map { Data(hex: $0) } let membersAsData = members.map { Data(hex: $0) }
let admins = [ userPublicKey ] let admins = [ userPublicKey ]
let adminsAsData = admins.map { Data(hex: $0) } let adminsAsData = admins.map { Data(hex: $0) }
let formationTimestamp: TimeInterval = Date().timeIntervalSince1970
let thread: SessionThread = try SessionThread let thread: SessionThread = try SessionThread
.fetchOrCreate(db, id: groupPublicKey, variant: .closedGroup) .fetchOrCreate(db, id: groupPublicKey, variant: .closedGroup)
try ClosedGroup( try ClosedGroup(
threadId: groupPublicKey, threadId: groupPublicKey,
name: name, name: name,
formationTimestamp: Date().timeIntervalSince1970 formationTimestamp: formationTimestamp
).insert(db) ).insert(db)
try admins.forEach { adminId in try admins.forEach { adminId in
@ -74,6 +74,11 @@ extension MessageSender {
admins: adminsAsData, admins: adminsAsData,
expirationTimer: 0 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, interactionId: nil,
in: contactThread in: contactThread
@ -501,23 +506,6 @@ extension MessageSender {
} }
let userPublicKey: String = getUserHexEncodedPublicKey(db) 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 // Notify the user
let interaction: Interaction = try Interaction( let interaction: Interaction = try Interaction(
@ -563,8 +551,9 @@ extension MessageSender {
.map { _ in } .map { _ in }
// Update the group // Update the group
try membersToRemove.deleteAll(db) _ = try closedGroup
try adminsToRemove.deleteAll(db) .allMembers
.deleteAll(db)
// Return // Return
return promise return promise

View file

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

View file

@ -93,8 +93,8 @@ public final class MessageSender : NSObject {
// Set the timestamp, sender and recipient // Set the timestamp, sender and recipient
if message.sentTimestamp == nil { // Visible messages will already have their sent timestamp set 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) let isSelfSend: Bool = (message.recipient == userPublicKey)
message.sender = userPublicKey message.sender = userPublicKey
@ -340,7 +340,7 @@ public final class MessageSender : NSObject {
// Set the timestamp, sender and recipient // Set the timestamp, sender and recipient
if message.sentTimestamp == nil { // Visible messages will already have their sent timestamp set 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() message.sender = getUserHexEncodedPublicKey()
@ -552,18 +552,8 @@ public final class MessageSender : NSObject {
if let interactionId: Int64 = interactionId { if let interactionId: Int64 = interactionId {
return try Interaction.fetchOne(db, id: 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 sentTimestamp: Double = message.sentTimestamp.map({ Double($0) }) {
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)
}
return try Interaction return try Interaction
.filter(Interaction.Columns.timestampMs == sentTimestamp) .filter(Interaction.Columns.timestampMs == sentTimestamp)
.fetchOne(db) .fetchOne(db)

View file

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

View file

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

View file

@ -6,18 +6,6 @@
NS_ASSUME_NONNULL_BEGIN 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 OWSPreferencesSignalDatabaseCollection = @"SignalPreferences";
NSString *const OWSPreferencesCallLoggingDidChangeNotification = @"OWSPreferencesCallLoggingDidChangeNotification"; NSString *const OWSPreferencesCallLoggingDidChangeNotification = @"OWSPreferencesCallLoggingDidChangeNotification";
NSString *const OWSPreferencesKeyScreenSecurity = @"Screen Security Key"; NSString *const OWSPreferencesKeyScreenSecurity = @"Screen Security Key";

View file

@ -43,7 +43,7 @@ public extension Setting.StringKey {
} }
public enum Preferences { 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 /// Notifications should include both the sender name and a preview of the message content
case nameAndPreview case nameAndPreview
@ -60,10 +60,6 @@ public enum Preferences {
case .noNameNoPreview: return "NOTIFICATIONS_NONE".localized() case .noNameNoPreview: return "NOTIFICATIONS_NONE".localized()
} }
} }
var accessibilityIdentifier: String {
return "NotificationSettingsOptionsViewController.\(name)"
}
} }
public enum Sound: Int, Codable, DatabaseValueConvertible, EnumSetting { public enum Sound: Int, Codable, DatabaseValueConvertible, EnumSetting {
@ -265,7 +261,45 @@ public enum Preferences {
// MARK: - Objective C Support // 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) @objc(SMKSound)
public class SMKSound: NSObject { public class SMKSound: NSObject {
@objc public static var notificationSounds: [Int] = Preferences.Sound.notificationSounds.map { $0.rawValue } @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 { @objc public static var defaultNotificationSound: Int {
GRDBStorage.shared[.defaultNotificationSound] return GRDBStorage.shared[.defaultNotificationSound]
.defaulting(to: Preferences.Sound.defaultNotificationSound) .defaulting(to: Preferences.Sound.defaultNotificationSound)
.rawValue .rawValue
} }

View file

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

View file

@ -167,7 +167,7 @@ public extension Database {
return T(rawValue: rawValue) 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 /// Value will be stored as a timestamp in seconds since 1970

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -65,22 +65,7 @@ public final class ProfilePictureView: UIView {
additionalImageView.layer.cornerRadius = additionalImageViewSize / 2 additionalImageView.layer.cornerRadius = additionalImageViewSize / 2
} }
// MARK: - Updating // FIXME: Look to deprecate this and replace it with the pattern in HomeViewModel (screen should fetch only the required info)
@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
)
}
@objc(updateForThreadId:) @objc(updateForThreadId:)
public func update(forThreadId threadId: String?) { public func update(forThreadId threadId: String?) {
guard guard

View file

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

View file

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

View file

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

View file

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