Finished off the MediaGallery logic

Updated the config message generation for GRDB
Migrated more preferences into GRDB
Added paging to the MediaTileViewController and sorted out the various animations/transitions
Fixed an issue where the 'recipientState' for the 'baseQuery' on the ConversationCell.ViewModel wasn't grouping correctly
Fixed an issue where the MediaZoomAnimationController could fail if the contextual info wasn't available
Fixed an issue where the MediaZoomAnimationController bounce looked odd when returning to the detail screen from the tile screen
Fixed an issue where the MediaZoomAnimationController didn't work for videos
Fixed a bug where the YDB to GRDB migration wasn't properly handling video files
Fixed a number of minor UI bugs with the GalleryRailView
Deleted a bunch of legacy code
This commit is contained in:
Morgan Pretty 2022-05-20 17:58:39 +10:00
parent a6c7e252a7
commit aabf656d89
81 changed files with 3413 additions and 4412 deletions

View File

@ -35,6 +35,7 @@ public enum SNMessagingKit { // Just to make the external API nice
JobRunner.add(executor: FailedAttachmentDownloadsJob.self, for: .failedAttachmentDownloads)
JobRunner.add(executor: UpdateProfilePictureJob.self, for: .updateProfilePicture)
JobRunner.add(executor: RetrieveDefaultOpenGroupRoomsJob.self, for: .retrieveDefaultOpenGroupRooms)
JobRunner.add(executor: GarbageCollectionJob.self, for: .garbageCollection)
JobRunner.add(executor: MessageSendJob.self, for: .messageSend)
JobRunner.add(executor: MessageReceiveJob.self, for: .messageReceive)
JobRunner.add(executor: NotifyPushServerJob.self, for: .notifyPushServer)

View File

@ -59,7 +59,6 @@
451A13B11E13DED2000A50FD /* AppNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 451A13B01E13DED2000A50FD /* AppNotifications.swift */; };
4520D8D51D417D8E00123472 /* Photos.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4520D8D41D417D8E00123472 /* Photos.framework */; };
4521C3C01F59F3BA00B4C582 /* TextFieldHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4521C3BF1F59F3BA00B4C582 /* TextFieldHelper.swift */; };
452EC6DF205E9E30000E787C /* MediaGalleryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452EC6DE205E9E30000E787C /* MediaGalleryViewController.swift */; };
4535186E1FC635DD00210559 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4535186C1FC635DD00210559 /* MainInterface.storyboard */; };
453518721FC635DD00210559 /* SessionShareExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 453518681FC635DD00210559 /* SessionShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
4539B5861F79348F007141FF /* PushRegistrationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4539B5851F79348F007141FF /* PushRegistrationManager.swift */; };
@ -101,7 +100,6 @@
45CB2FA81CB7146C00E1B343 /* Launch Screen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 45CB2FA71CB7146C00E1B343 /* Launch Screen.storyboard */; };
45CD81EF1DC030E7004C9430 /* SyncPushTokensJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45CD81EE1DC030E7004C9430 /* SyncPushTokensJob.swift */; };
45E5A6991F61E6DE001E4A8A /* MarqueeLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45E5A6981F61E6DD001E4A8A /* MarqueeLabel.swift */; };
45F32C222057297A00A300D5 /* MediaDetailViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 45B9EE9B200E91FB005D2F2D /* MediaDetailViewController.m */; };
45F32C232057297A00A300D5 /* MediaPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45F32C1D205718B000A300D5 /* MediaPageViewController.swift */; };
4C090A1B210FD9C7001FD7F9 /* HapticFeedback.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C090A1A210FD9C7001FD7F9 /* HapticFeedback.swift */; };
4C1885D2218F8E1C00B67051 /* PhotoGridViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1885D1218F8E1C00B67051 /* PhotoGridViewCell.swift */; };
@ -159,7 +157,6 @@
B8269D2925C7A4B400488AB4 /* InputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8269D2825C7A4B400488AB4 /* InputView.swift */; };
B8269D3325C7A8C600488AB4 /* InputViewButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8269D3225C7A8C600488AB4 /* InputViewButton.swift */; };
B8269D3D25C7B34D00488AB4 /* InputTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8269D3C25C7B34D00488AB4 /* InputTextView.swift */; };
B82A0C3826B9098200C1BCE3 /* MessageInvalidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B82A0C3726B9098200C1BCE3 /* MessageInvalidator.swift */; };
B82B40882399EB0E00A248E7 /* LandingVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B82B40872399EB0E00A248E7 /* LandingVC.swift */; };
B82B408A2399EC0600A248E7 /* FakeChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B82B40892399EC0600A248E7 /* FakeChatView.swift */; };
B82B408C239A068800A248E7 /* RegisterVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B82B408B239A068800A248E7 /* RegisterVC.swift */; };
@ -322,7 +319,6 @@
C32C5B51256DC219003C73A2 /* NSNotificationCenter+OWS.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB3B255A580B00E217F9 /* NSNotificationCenter+OWS.h */; settings = {ATTRIBUTES = (Public, ); }; };
C32C5B84256DC54F003C73A2 /* SSKEnvironment.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAF4255A580600E217F9 /* SSKEnvironment.m */; };
C32C5B8D256DC565003C73A2 /* SSKEnvironment.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB31255A580A00E217F9 /* SSKEnvironment.h */; settings = {ATTRIBUTES = (Public, ); }; };
C32C5BBA256DC7E3003C73A2 /* ProfileManagerProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBB9255A581600E217F9 /* ProfileManagerProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; };
C32C5BCC256DC830003C73A2 /* Storage+ClosedGroups.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D8F1372566120F0092EF10 /* Storage+ClosedGroups.swift */; };
C32C5C3D256DCBAF003C73A2 /* AppReadiness.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB75255A581000E217F9 /* AppReadiness.m */; };
C32C5C46256DCBB2003C73A2 /* AppReadiness.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB01255A580700E217F9 /* AppReadiness.h */; settings = {ATTRIBUTES = (Public, ); }; };
@ -340,7 +336,6 @@
C32C5DD2256DD9E5003C73A2 /* LRUCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAFD255A580600E217F9 /* LRUCache.swift */; };
C32C5DDB256DD9FF003C73A2 /* ContentProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB68255A580F00E217F9 /* ContentProxy.swift */; };
C32C5E0C256DDAFA003C73A2 /* NSRegularExpression+SSK.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA7A255A57FB00E217F9 /* NSRegularExpression+SSK.swift */; };
C32C5E15256DDC78003C73A2 /* SSKPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA69255A57F900E217F9 /* SSKPreferences.swift */; };
C32C5E5B256DDF45003C73A2 /* OWSStorage.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAB1255A580000E217F9 /* OWSStorage.m */; };
C32C5E64256DDFD6003C73A2 /* OWSStorage.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDAFE255A580600E217F9 /* OWSStorage.h */; settings = {ATTRIBUTES = (Public, ); }; };
C32C5E75256DE020003C73A2 /* YapDatabaseTransaction+OWS.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB5B255A580E00E217F9 /* YapDatabaseTransaction+OWS.m */; };
@ -417,7 +412,6 @@
C33FDD49255A582000E217F9 /* ParamParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB8F255A581200E217F9 /* ParamParser.swift */; };
C33FDD53255A582000E217F9 /* OWSPrimaryStorage+keyFromIntLong.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB99255A581300E217F9 /* OWSPrimaryStorage+keyFromIntLong.m */; };
C33FDD5B255A582000E217F9 /* OWSOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBA1255A581400E217F9 /* OWSOperation.h */; settings = {ATTRIBUTES = (Public, ); }; };
C33FDD68255A582000E217F9 /* SignalAccount.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBAE255A581500E217F9 /* SignalAccount.h */; settings = {ATTRIBUTES = (Public, ); }; };
C33FDD6E255A582000E217F9 /* NSURLSessionDataTask+StatusCode.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBB4255A581600E217F9 /* NSURLSessionDataTask+StatusCode.m */; };
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, ); }; };
@ -428,7 +422,6 @@
C33FDDB3255A582000E217F9 /* OWSError.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBF9255A581C00E217F9 /* OWSError.h */; settings = {ATTRIBUTES = (Public, ); }; };
C33FDDB8255A582000E217F9 /* NSSet+Functional.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBFE255A581C00E217F9 /* NSSet+Functional.h */; settings = {ATTRIBUTES = (Public, ); }; };
C33FDDBD255A582000E217F9 /* ByteParser.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDC03255A581D00E217F9 /* ByteParser.h */; settings = {ATTRIBUTES = (Public, ); }; };
C33FDDC0255A582000E217F9 /* SignalAccount.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDC06255A581D00E217F9 /* SignalAccount.m */; };
C33FDDC5255A582000E217F9 /* OWSError.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDC0B255A581D00E217F9 /* OWSError.m */; };
C33FDDCC255A582000E217F9 /* TSConstants.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDC12255A581E00E217F9 /* TSConstants.h */; settings = {ATTRIBUTES = (Public, ); }; };
C33FDDD0255A582000E217F9 /* FunctionalUtil.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDC16255A581E00E217F9 /* FunctionalUtil.h */; settings = {ATTRIBUTES = (Public, ); }; };
@ -581,13 +574,7 @@
C3A3A111256E1A93004D228D /* OWSIncomingMessageFinder.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDAC0255A580100E217F9 /* OWSIncomingMessageFinder.h */; settings = {ATTRIBUTES = (Public, ); }; };
C3A3A122256E1A97004D228D /* OWSDisappearingMessagesFinder.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDC05255A581D00E217F9 /* OWSDisappearingMessagesFinder.h */; settings = {ATTRIBUTES = (Public, ); }; };
C3A3A12B256E1AD5004D228D /* TSDatabaseSecondaryIndexes.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB25255A580900E217F9 /* TSDatabaseSecondaryIndexes.h */; settings = {ATTRIBUTES = (Public, ); }; };
C3A3A13C256E1B27004D228D /* OWSMediaGalleryFinder.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB71255A581000E217F9 /* OWSMediaGalleryFinder.m */; };
C3A3A145256E1B49004D228D /* OWSMediaGalleryFinder.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB67255A580F00E217F9 /* OWSMediaGalleryFinder.h */; settings = {ATTRIBUTES = (Public, ); }; };
C3A3A156256E1B91004D228D /* ProtoUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA6C255A57FA00E217F9 /* ProtoUtils.m */; };
C3A3A15F256E1BB4004D228D /* ProtoUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB91255A581200E217F9 /* ProtoUtils.h */; settings = {ATTRIBUTES = (Public, ); }; };
C3A3A171256E1D25004D228D /* SSKReachabilityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A3A170256E1D25004D228D /* SSKReachabilityManager.swift */; };
C3A3A18A256E2092004D228D /* SignalRecipient.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBB7255A581600E217F9 /* SignalRecipient.m */; };
C3A3A193256E20D4004D228D /* SignalRecipient.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDAEC255A580500E217F9 /* SignalRecipient.h */; settings = {ATTRIBUTES = (Public, ); }; };
C3A71D0B2558989C0043A11F /* MessageWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A71D0A2558989C0043A11F /* MessageWrapper.swift */; };
C3A71D1E25589AC30043A11F /* WebSocketProto.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A71D1C25589AC30043A11F /* WebSocketProto.swift */; };
C3A71D1F25589AC30043A11F /* WebSocketResources.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A71D1D25589AC30043A11F /* WebSocketResources.pb.swift */; };
@ -756,6 +743,7 @@
FD3C907327E8387300CD579F /* OpenGroupServerIdLookupMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C907227E8387300CD579F /* OpenGroupServerIdLookupMigration.swift */; };
FD3C907527E83AC200CD579F /* OpenGroupServerIdLookup.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C907427E83AC200CD579F /* OpenGroupServerIdLookup.swift */; };
FD4B200C283367410034334B /* ConversationCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD4B200B283367410034334B /* ConversationCellViewModel.swift */; };
FD4B200E283492210034334B /* InsetLockableTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD4B200D283492210034334B /* InsetLockableTableView.swift */; };
FD5D200F27AA2B6000FEA984 /* MessageRequestResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5D200E27AA2B6000FEA984 /* MessageRequestResponse.swift */; };
FD5D201127AA331F00FEA984 /* ConfigurationMessage+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5D201027AA331F00FEA984 /* ConfigurationMessage+Convenience.swift */; };
FD659AC027A7649600F12C02 /* MessageRequestsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD659ABF27A7649600F12C02 /* MessageRequestsViewController.swift */; };
@ -785,6 +773,9 @@
FDC4389E27BA2B8A00C60D73 /* SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; };
FDCDB8DE2810F73B00352A0C /* Differentiable+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDCDB8DD2810F73B00352A0C /* Differentiable+Utilities.swift */; };
FDCDB8E02811007F00352A0C /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDCDB8DF2811007F00352A0C /* HomeViewModel.swift */; };
FDD2506E283711D600198BDA /* DifferenceKit+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD2506D283711D600198BDA /* DifferenceKit+Utilities.swift */; };
FDD250702837199200198BDA /* GarbageCollectionJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD2506F2837199200198BDA /* GarbageCollectionJob.swift */; };
FDD250722837234B00198BDA /* MediaGalleryNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD250712837234B00198BDA /* MediaGalleryNavigationController.swift */; };
FDE77F6B280FEB28002CFC5D /* ControlMessageProcessRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE77F6A280FEB28002CFC5D /* ControlMessageProcessRecord.swift */; };
FDED2E3C282E1B5D00B2CD2A /* UICollectionView+ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDED2E3B282E1B5D00B2CD2A /* UICollectionView+ReusableView.swift */; };
FDF0B73C27FFD3D6004C14C5 /* LinkPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B73B27FFD3D6004C14C5 /* LinkPreview.swift */; };
@ -1047,7 +1038,6 @@
451A13B01E13DED2000A50FD /* AppNotifications.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = AppNotifications.swift; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
4520D8D41D417D8E00123472 /* Photos.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Photos.framework; path = System/Library/Frameworks/Photos.framework; sourceTree = SDKROOT; };
4521C3BF1F59F3BA00B4C582 /* TextFieldHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextFieldHelper.swift; sourceTree = "<group>"; };
452EC6DE205E9E30000E787C /* MediaGalleryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaGalleryViewController.swift; sourceTree = "<group>"; };
453518681FC635DD00210559 /* SessionShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SessionShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
4535186D1FC635DD00210559 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; };
4535186F1FC635DD00210559 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@ -1085,8 +1075,6 @@
45B74A702044AAB500CD42F8 /* circles-quiet.aifc */ = {isa = PBXFileReference; lastKnownFileType = file; path = "circles-quiet.aifc"; sourceTree = "<group>"; };
45B74A722044AAB600CD42F8 /* synth.aifc */ = {isa = PBXFileReference; lastKnownFileType = file; path = synth.aifc; sourceTree = "<group>"; };
45B74A732044AAB600CD42F8 /* input-quiet.aifc */ = {isa = PBXFileReference; lastKnownFileType = file; path = "input-quiet.aifc"; sourceTree = "<group>"; };
45B9EE9A200E91FB005D2F2D /* MediaDetailViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MediaDetailViewController.h; sourceTree = "<group>"; };
45B9EE9B200E91FB005D2F2D /* MediaDetailViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MediaDetailViewController.m; sourceTree = "<group>"; };
45BD60811DE9547E00A8F436 /* Contacts.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Contacts.framework; path = System/Library/Frameworks/Contacts.framework; sourceTree = SDKROOT; };
45C0DC1A1E68FE9000E04C47 /* UIApplication+OWS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIApplication+OWS.swift"; sourceTree = "<group>"; };
45C0DC1D1E69011F00E04C47 /* UIStoryboard+OWS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIStoryboard+OWS.swift"; sourceTree = "<group>"; };
@ -1194,7 +1182,6 @@
B8269D2825C7A4B400488AB4 /* InputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputView.swift; sourceTree = "<group>"; };
B8269D3225C7A8C600488AB4 /* InputViewButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputViewButton.swift; sourceTree = "<group>"; };
B8269D3C25C7B34D00488AB4 /* InputTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputTextView.swift; sourceTree = "<group>"; };
B82A0C3726B9098200C1BCE3 /* MessageInvalidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageInvalidator.swift; sourceTree = "<group>"; };
B82B40872399EB0E00A248E7 /* LandingVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LandingVC.swift; sourceTree = "<group>"; };
B82B40892399EC0600A248E7 /* FakeChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakeChatView.swift; sourceTree = "<group>"; };
B82B408B239A068800A248E7 /* RegisterVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisterVC.swift; sourceTree = "<group>"; };
@ -1328,8 +1315,6 @@
C33FD9AD255A548A00E217F9 /* SignalUtilitiesKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SignalUtilitiesKit.h; sourceTree = "<group>"; };
C33FD9AE255A548A00E217F9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
C33FDA67255A57F900E217F9 /* OWSPrimaryStorage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSPrimaryStorage.h; sourceTree = "<group>"; };
C33FDA69255A57F900E217F9 /* SSKPreferences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SSKPreferences.swift; sourceTree = "<group>"; };
C33FDA6C255A57FA00E217F9 /* ProtoUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ProtoUtils.m; sourceTree = "<group>"; };
C33FDA6D255A57FA00E217F9 /* YapDatabase+Promise.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "YapDatabase+Promise.swift"; sourceTree = "<group>"; };
C33FDA6F255A57FA00E217F9 /* ReachabilityManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReachabilityManager.swift; sourceTree = "<group>"; };
C33FDA70255A57FA00E217F9 /* TSMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSMessage.h; sourceTree = "<group>"; };
@ -1369,7 +1354,6 @@
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>"; };
C33FDAEA255A580500E217F9 /* OWSBackupFragment.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBackupFragment.h; sourceTree = "<group>"; };
C33FDAEC255A580500E217F9 /* SignalRecipient.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SignalRecipient.h; sourceTree = "<group>"; };
C33FDAEF255A580500E217F9 /* NSData+Image.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSData+Image.m"; sourceTree = "<group>"; };
C33FDAF1255A580500E217F9 /* OWSThumbnailService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSThumbnailService.swift; sourceTree = "<group>"; };
C33FDAF2255A580500E217F9 /* ProxiedContentDownloader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProxiedContentDownloader.swift; sourceTree = "<group>"; };
@ -1415,12 +1399,10 @@
C33FDB5C255A580E00E217F9 /* NSArray+Functional.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSArray+Functional.h"; sourceTree = "<group>"; };
C33FDB5F255A580E00E217F9 /* YapDatabaseConnection+OWS.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "YapDatabaseConnection+OWS.h"; sourceTree = "<group>"; };
C33FDB60255A580E00E217F9 /* TSMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSMessage.m; sourceTree = "<group>"; };
C33FDB67255A580F00E217F9 /* OWSMediaGalleryFinder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSMediaGalleryFinder.h; sourceTree = "<group>"; };
C33FDB68255A580F00E217F9 /* ContentProxy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentProxy.swift; sourceTree = "<group>"; };
C33FDB69255A580F00E217F9 /* FeatureFlags.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeatureFlags.swift; sourceTree = "<group>"; };
C33FDB6B255A580F00E217F9 /* SNUserDefaults.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SNUserDefaults.swift; sourceTree = "<group>"; };
C33FDB6C255A580F00E217F9 /* NSNotificationCenter+OWS.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSNotificationCenter+OWS.m"; sourceTree = "<group>"; };
C33FDB71255A581000E217F9 /* OWSMediaGalleryFinder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSMediaGalleryFinder.m; sourceTree = "<group>"; };
C33FDB73255A581000E217F9 /* TSGroupModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSGroupModel.m; sourceTree = "<group>"; };
C33FDB75255A581000E217F9 /* AppReadiness.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppReadiness.m; sourceTree = "<group>"; };
C33FDB77255A581000E217F9 /* NSUserDefaults+OWS.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSUserDefaults+OWS.m"; sourceTree = "<group>"; };
@ -1433,7 +1415,6 @@
C33FDB88255A581200E217F9 /* TSAccountManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSAccountManager.m; sourceTree = "<group>"; };
C33FDB8A255A581200E217F9 /* AppContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppContext.h; sourceTree = "<group>"; };
C33FDB8F255A581200E217F9 /* ParamParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParamParser.swift; sourceTree = "<group>"; };
C33FDB91255A581200E217F9 /* ProtoUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ProtoUtils.h; sourceTree = "<group>"; };
C33FDB94255A581300E217F9 /* TSAccountManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSAccountManager.h; sourceTree = "<group>"; };
C33FDB99255A581300E217F9 /* OWSPrimaryStorage+keyFromIntLong.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "OWSPrimaryStorage+keyFromIntLong.m"; sourceTree = "<group>"; };
C33FDB9C255A581300E217F9 /* TSIncomingMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSIncomingMessage.h; sourceTree = "<group>"; };
@ -1441,12 +1422,9 @@
C33FDBA1255A581400E217F9 /* OWSOperation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSOperation.h; sourceTree = "<group>"; };
C33FDBA8255A581500E217F9 /* OWSLinkPreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSLinkPreview.swift; sourceTree = "<group>"; };
C33FDBAB255A581500E217F9 /* OWSFileSystem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSFileSystem.h; sourceTree = "<group>"; };
C33FDBAE255A581500E217F9 /* SignalAccount.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SignalAccount.h; sourceTree = "<group>"; };
C33FDBB4255A581600E217F9 /* NSURLSessionDataTask+StatusCode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSURLSessionDataTask+StatusCode.m"; sourceTree = "<group>"; };
C33FDBB6255A581600E217F9 /* DataSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DataSource.m; sourceTree = "<group>"; };
C33FDBB7255A581600E217F9 /* SignalRecipient.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SignalRecipient.m; sourceTree = "<group>"; };
C33FDBB8255A581600E217F9 /* TSThread.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSThread.m; sourceTree = "<group>"; };
C33FDBB9255A581600E217F9 /* ProfileManagerProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ProfileManagerProtocol.h; sourceTree = "<group>"; };
C33FDBBA255A581600E217F9 /* OWSPrimaryStorage+keyFromIntLong.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "OWSPrimaryStorage+keyFromIntLong.h"; sourceTree = "<group>"; };
C33FDBBC255A581600E217F9 /* SSKKeychainStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SSKKeychainStorage.swift; sourceTree = "<group>"; };
C33FDBC2255A581700E217F9 /* SSKAsserts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SSKAsserts.h; sourceTree = "<group>"; };
@ -1465,7 +1443,6 @@
C33FDC02255A581D00E217F9 /* OWSPrimaryStorage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSPrimaryStorage.m; sourceTree = "<group>"; };
C33FDC03255A581D00E217F9 /* ByteParser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ByteParser.h; sourceTree = "<group>"; };
C33FDC05255A581D00E217F9 /* OWSDisappearingMessagesFinder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSDisappearingMessagesFinder.h; sourceTree = "<group>"; };
C33FDC06255A581D00E217F9 /* SignalAccount.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SignalAccount.m; sourceTree = "<group>"; };
C33FDC0B255A581D00E217F9 /* OWSError.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSError.m; sourceTree = "<group>"; };
C33FDC0C255A581E00E217F9 /* TSInfoMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSInfoMessage.m; sourceTree = "<group>"; };
C33FDC12255A581E00E217F9 /* TSConstants.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSConstants.h; sourceTree = "<group>"; };
@ -1812,6 +1789,7 @@
FD3C907227E8387300CD579F /* OpenGroupServerIdLookupMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupServerIdLookupMigration.swift; sourceTree = "<group>"; };
FD3C907427E83AC200CD579F /* OpenGroupServerIdLookup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupServerIdLookup.swift; sourceTree = "<group>"; };
FD4B200B283367410034334B /* ConversationCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationCellViewModel.swift; sourceTree = "<group>"; };
FD4B200D283492210034334B /* InsetLockableTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsetLockableTableView.swift; sourceTree = "<group>"; };
FD5D200E27AA2B6000FEA984 /* MessageRequestResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestResponse.swift; sourceTree = "<group>"; };
FD5D201027AA331F00FEA984 /* ConfigurationMessage+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConfigurationMessage+Convenience.swift"; sourceTree = "<group>"; };
FD659ABF27A7649600F12C02 /* MessageRequestsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestsViewController.swift; sourceTree = "<group>"; };
@ -1836,6 +1814,9 @@
FDA8EB0F280F8238002B68E5 /* Codable+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Codable+Utilities.swift"; sourceTree = "<group>"; };
FDCDB8DD2810F73B00352A0C /* Differentiable+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Differentiable+Utilities.swift"; sourceTree = "<group>"; };
FDCDB8DF2811007F00352A0C /* HomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewModel.swift; sourceTree = "<group>"; };
FDD2506D283711D600198BDA /* DifferenceKit+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DifferenceKit+Utilities.swift"; sourceTree = "<group>"; };
FDD2506F2837199200198BDA /* GarbageCollectionJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GarbageCollectionJob.swift; sourceTree = "<group>"; };
FDD250712837234B00198BDA /* MediaGalleryNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaGalleryNavigationController.swift; sourceTree = "<group>"; };
FDE77F68280F9EDA002CFC5D /* JobRunnerError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JobRunnerError.swift; sourceTree = "<group>"; };
FDE77F6A280FEB28002CFC5D /* ControlMessageProcessRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlMessageProcessRecord.swift; sourceTree = "<group>"; };
FDED2E3B282E1B5D00B2CD2A /* UICollectionView+ReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UICollectionView+ReusableView.swift"; sourceTree = "<group>"; };
@ -2076,6 +2057,7 @@
4CC1ECFA211A553000CC13BE /* AppUpdateNag.swift */,
B90418E4183E9DD40038554A /* DateUtil.h */,
B90418E5183E9DD40038554A /* DateUtil.m */,
FDD2506D283711D600198BDA /* DifferenceKit+Utilities.swift */,
4C21D5D5223A9DC500EF8A77 /* UIAlerts+iOS9.m */,
45C0DC1A1E68FE9000E04C47 /* UIApplication+OWS.swift */,
45C0DC1D1E69011F00E04C47 /* UIStoryboard+OWS.swift */,
@ -2245,6 +2227,7 @@
B82149C025D605C6009C0F2A /* InfoBanner.swift */,
C374EEEA25DA3CA70073A857 /* ConversationTitleView.swift */,
B848A4C4269EAAA200617031 /* UserDetailsSheet.swift */,
FD4B200D283492210034334B /* InsetLockableTableView.swift */,
);
path = "Views & Modals";
sourceTree = "<group>";
@ -2656,9 +2639,6 @@
C33FDBEC255A581B00E217F9 /* OWSRecipientIdentity.m */,
C38EF2D3255B6DAF007E1867 /* OWSUserProfile.h */,
C38EF2D1255B6DAF007E1867 /* OWSUserProfile.m */,
C33FDBB9255A581600E217F9 /* ProfileManagerProtocol.h */,
C33FDAEC255A580500E217F9 /* SignalRecipient.h */,
C33FDBB7255A581600E217F9 /* SignalRecipient.m */,
C33FDB94255A581300E217F9 /* TSAccountManager.h */,
C33FDB88255A581200E217F9 /* TSAccountManager.m */,
);
@ -2684,7 +2664,6 @@
B8D8F19225661BF80092EF10 /* Storage+Messaging.swift */,
B8D8F18825661BA50092EF10 /* Storage+OpenGroups.swift */,
C3F0A5FD255C988A007BE2A3 /* Storage+Shared.swift */,
C33FDA69255A57F900E217F9 /* SSKPreferences.swift */,
C33FDB25255A580900E217F9 /* TSDatabaseSecondaryIndexes.h */,
C33FDB20255A580900E217F9 /* TSDatabaseSecondaryIndexes.m */,
C33FDB2C255A580A00E217F9 /* TSDatabaseView.h */,
@ -2909,16 +2888,14 @@
children = (
FDFDE122282D04E30098B17F /* Transitions */,
C36096B925AD1ACF008B62B2 /* GIFs */,
FD09C5E728264937000CE219 /* MediaDetailViewController.swift */,
FD09C5E528260FF9000CE219 /* MediaGalleryViewModel.swift */,
FDD250712837234B00198BDA /* MediaGalleryNavigationController.swift */,
45F32C1D205718B000A300D5 /* MediaPageViewController.swift */,
454A84032059C787008B8C75 /* MediaTileViewController.swift */,
34E3E5671EC4B19400495BAC /* AudioProgressView.swift */,
346B66301F4E29B200E5122F /* CropScaleImageViewController.swift */,
34969559219B605E00DCFE74 /* ImagePickerController.swift */,
45B9EE9A200E91FB005D2F2D /* MediaDetailViewController.h */,
45B9EE9B200E91FB005D2F2D /* MediaDetailViewController.m */,
FD09C5E728264937000CE219 /* MediaDetailViewController.swift */,
FD09C5E528260FF9000CE219 /* MediaGalleryViewModel.swift */,
452EC6DE205E9E30000E787C /* MediaGalleryViewController.swift */,
45F32C1D205718B000A300D5 /* MediaPageViewController.swift */,
454A84032059C787008B8C75 /* MediaTileViewController.swift */,
34A6C27F21E503E600B5B12E /* OWSImagePickerController.swift */,
3496955A219B605E00DCFE74 /* PhotoCollectionPickerController.swift */,
3496955B219B605E00DCFE74 /* PhotoLibrary.swift */,
@ -3145,7 +3122,6 @@
C37F5402255BA9ED002AEA92 /* Environment.m */,
C3BBE0C62554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift */,
C33FDB7F255A581100E217F9 /* FullTextSearchFinder.swift */,
B82A0C3726B9098200C1BCE3 /* MessageInvalidator.swift */,
C3A71D0A2558989C0043A11F /* MessageWrapper.swift */,
C3A71D4E25589FF30043A11F /* NSData+messagePadding.h */,
C3A71D4825589FF20043A11F /* NSData+messagePadding.m */,
@ -3159,8 +3135,6 @@
C33FDA86255A57FC00E217F9 /* OWSDisappearingMessagesFinder.m */,
C33FDAC0255A580100E217F9 /* OWSIncomingMessageFinder.h */,
C33FDB1E255A580900E217F9 /* OWSIncomingMessageFinder.m */,
C33FDB67255A580F00E217F9 /* OWSMediaGalleryFinder.h */,
C33FDB71255A581000E217F9 /* OWSMediaGalleryFinder.m */,
C38EF2F1255B6DBB007E1867 /* OWSPreferences.h */,
C38EF308255B6DBE007E1867 /* OWSPreferences.m */,
FDF0B75D280AAF35004C14C5 /* Preferences.swift */,
@ -3169,8 +3143,6 @@
FD09797327FAB3E200936362 /* ProfileManager.swift */,
C38EF2EC255B6DBA007E1867 /* ProximityMonitoringManager.swift */,
C3BBE0B42554F0E10050F1E3 /* ProofOfWork.swift */,
C33FDB91255A581200E217F9 /* ProtoUtils.h */,
C33FDA6C255A57FA00E217F9 /* ProtoUtils.m */,
C38EEF09255B49A8007E1867 /* SNProtoEnvelope+Conversion.swift */,
C33FDB31255A580A00E217F9 /* SSKEnvironment.h */,
C33FDAF4255A580600E217F9 /* SSKEnvironment.m */,
@ -3344,8 +3316,6 @@
C33FDC19255A581F00E217F9 /* OWSQueues.h */,
C33FDBD3255A581800E217F9 /* OWSSignalAddress.swift */,
C33FDA6F255A57FA00E217F9 /* ReachabilityManager.swift */,
C33FDBAE255A581500E217F9 /* SignalAccount.h */,
C33FDC06255A581D00E217F9 /* SignalAccount.m */,
C33FDBD8255A581900E217F9 /* SignalIOS.pb.swift */,
C33FDB40255A580C00E217F9 /* SignalIOSProto.swift */,
C33FDC12255A581E00E217F9 /* TSConstants.h */,
@ -3761,6 +3731,7 @@
FDA8EAFF280E8D58002B68E5 /* FailedAttachmentDownloadsJob.swift */,
FD6A7A6A2818C17C00035AC1 /* UpdateProfilePictureJob.swift */,
FD6A7A682818BE7300035AC1 /* RetrieveDefaultOpenGroupRoomsJob.swift */,
FDD2506F2837199200198BDA /* GarbageCollectionJob.swift */,
C352A2FE25574B6300338F3E /* MessageSendJob.swift */,
C352A31225574F5200338F3E /* MessageReceiveJob.swift */,
C352A32E2557549C00338F3E /* NotifyPushServerJob.swift */,
@ -3835,7 +3806,6 @@
C33FDDD3255A582000E217F9 /* OWSQueues.h in Headers */,
C33FDC96255A582000E217F9 /* NSObject+Casting.h in Headers */,
C33FDDB3255A582000E217F9 /* OWSError.h in Headers */,
C33FDD68255A582000E217F9 /* SignalAccount.h in Headers */,
C38EF35E255B6DCC007E1867 /* OWSViewController.h in Headers */,
C37F5396255B95BD002AEA92 /* OWSAnyTouchGestureRecognizer.h in Headers */,
C33FDDB2255A582000E217F9 /* NSArray+OWS.h in Headers */,
@ -3910,8 +3880,6 @@
C32C5EC3256DE133003C73A2 /* OWSQuotedReplyModel.h in Headers */,
C32C5AAA256DBE8F003C73A2 /* TSIncomingMessage.h in Headers */,
B8856D72256F1421001CE70E /* OWSWindowManager.h in Headers */,
C32C5BBA256DC7E3003C73A2 /* ProfileManagerProtocol.h in Headers */,
C3A3A193256E20D4004D228D /* SignalRecipient.h in Headers */,
B8856CF7256F105E001CE70E /* OWSAudioPlayer.h in Headers */,
B8856D3D256F11B2001CE70E /* Environment.h in Headers */,
C32C5E7E256DE023003C73A2 /* YapDatabaseTransaction+OWS.h in Headers */,
@ -3922,8 +3890,6 @@
C32C5EA0256DE0D6003C73A2 /* OWSPrimaryStorage.h in Headers */,
C3A3A111256E1A93004D228D /* OWSIncomingMessageFinder.h in Headers */,
C32C5AAC256DBE8F003C73A2 /* TSInfoMessage.h in Headers */,
C3A3A15F256E1BB4004D228D /* ProtoUtils.h in Headers */,
C3A3A145256E1B49004D228D /* OWSMediaGalleryFinder.h in Headers */,
B8856E33256F18D5001CE70E /* OWSStorage+Subclass.h in Headers */,
C32C5E64256DDFD6003C73A2 /* OWSStorage.h in Headers */,
);
@ -4697,7 +4663,6 @@
C38EF3BD255B6DE7007E1867 /* ImageEditorTransform.swift in Sources */,
C38EF24F255B6D67007E1867 /* UIColor+OWS.m in Sources */,
C33FDC9A255A582000E217F9 /* ByteParser.m in Sources */,
C33FDDC0255A582000E217F9 /* SignalAccount.m in Sources */,
C33FDC58255A582000E217F9 /* ReverseDispatchQueue.swift in Sources */,
C33FDC78255A582000E217F9 /* TSConstants.m in Sources */,
C38EF324255B6DBF007E1867 /* Bench.swift in Sources */,
@ -4869,7 +4834,6 @@
B8856D08256F10F1001CE70E /* DeviceSleepManager.swift in Sources */,
C3471F4C25553AB000297E91 /* MessageReceiver+Decryption.swift in Sources */,
C300A5D32554B05A00555489 /* TypingIndicator.swift in Sources */,
C3A3A156256E1B91004D228D /* ProtoUtils.m in Sources */,
FD09799927FFC1A300936362 /* Attachment.swift in Sources */,
C3471ECB2555356A00297E91 /* MessageSender+Encryption.swift in Sources */,
FDF0B74928060D13004C14C5 /* QuotedReplyModel.swift in Sources */,
@ -4880,6 +4844,7 @@
C32C5AAD256DBE8F003C73A2 /* TSInfoMessage.m in Sources */,
FD5D201127AA331F00FEA984 /* ConfigurationMessage+Convenience.swift in Sources */,
C32C5A13256DB7A5003C73A2 /* PushNotificationAPI.swift in Sources */,
FDD250702837199200198BDA /* GarbageCollectionJob.swift in Sources */,
C32A026325A801AA000ED5D4 /* NSData+messagePadding.m in Sources */,
FD6A7A6B2818C17C00035AC1 /* UpdateProfilePictureJob.swift in Sources */,
C32C5B84256DC54F003C73A2 /* SSKEnvironment.m in Sources */,
@ -4945,7 +4910,6 @@
FDF0B7582807F368004C14C5 /* MessageReceiverError.swift in Sources */,
C32C5AB5256DBE8F003C73A2 /* TSOutgoingMessage+Conversion.swift in Sources */,
C32C5E5B256DDF45003C73A2 /* OWSStorage.m in Sources */,
C32C5E15256DDC78003C73A2 /* SSKPreferences.swift in Sources */,
C32C5C4F256DCC36003C73A2 /* Storage+OpenGroups.swift in Sources */,
FD6A7A692818BE7300035AC1 /* RetrieveDefaultOpenGroupRoomsJob.swift in Sources */,
C3DA9C0725AE7396008F7C7E /* ConfigurationMessage.swift in Sources */,
@ -4995,13 +4959,11 @@
C32C5C3D256DCBAF003C73A2 /* AppReadiness.m in Sources */,
B87EF17126367CF800124B3C /* FileServerAPIV2.swift in Sources */,
FD09799B27FFC82D00936362 /* Quote.swift in Sources */,
C3A3A18A256E2092004D228D /* SignalRecipient.m in Sources */,
C3C2A74425539EB700C340D1 /* Message.swift in Sources */,
FD09798527FD1A6500936362 /* ClosedGroupKeyPair.swift in Sources */,
C32C5DBF256DD743003C73A2 /* ClosedGroupPoller.swift in Sources */,
C32C5EEE256DF54E003C73A2 /* TSDatabaseView.m in Sources */,
C352A35B2557824E00338F3E /* AttachmentUploadJob.swift in Sources */,
C3A3A13C256E1B27004D228D /* OWSMediaGalleryFinder.m in Sources */,
FD09797027FA6FF300936362 /* Profile.swift in Sources */,
C32C5AAE256DBE8F003C73A2 /* TSInteraction.m in Sources */,
C32C5FD6256E0346003C73A2 /* Notification+Thread.swift in Sources */,
@ -5018,7 +4980,6 @@
FD09C5EA282A1BB2000CE219 /* ThreadTypingIndicator.swift in Sources */,
FDF0B75E280AAF35004C14C5 /* Preferences.swift in Sources */,
C32C5AB0256DBE8F003C73A2 /* TSOutgoingMessage.m in Sources */,
B82A0C3826B9098200C1BCE3 /* MessageInvalidator.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -5030,7 +4991,6 @@
B8041AA725C90927003C2166 /* TypingIndicatorCell.swift in Sources */,
B8CCF63723961D6D0091D419 /* NewDMVC.swift in Sources */,
FDFDE12A282D056B0098B17F /* MediaZoomAnimationController.swift in Sources */,
452EC6DF205E9E30000E787C /* MediaGalleryViewController.swift in Sources */,
4C1885D2218F8E1C00B67051 /* PhotoGridViewCell.swift in Sources */,
34D1F0501F7D45A60066283D /* GifPickerCell.swift in Sources */,
C3E5C2FA251DBABB0040DFFC /* EditClosedGroupVC.swift in Sources */,
@ -5119,7 +5079,6 @@
4C586926224FAB83003FD070 /* AVAudioSession+OWS.m in Sources */,
C331FFF42558FF0300070591 /* PNOptionView.swift in Sources */,
4C4AE6A1224AF35700D4AF6F /* SendMediaNavigationController.swift in Sources */,
45F32C222057297A00A300D5 /* MediaDetailViewController.m in Sources */,
B82149C125D605C6009C0F2A /* InfoBanner.swift in Sources */,
C3DAB3242480CB2B00725F25 /* SRCopyableLabel.swift in Sources */,
B8CCF63F23975CFB0091D419 /* JoinOpenGroupVC.swift in Sources */,
@ -5152,6 +5111,7 @@
C31D1DE32521718E005D4DA8 /* UserSelectionVC.swift in Sources */,
34A6C28021E503E700B5B12E /* OWSImagePickerController.swift in Sources */,
C31A6C5C247F2CF3001123EF /* CGRect+Utilities.swift in Sources */,
FD4B200E283492210034334B /* InsetLockableTableView.swift in Sources */,
4C21D5D6223A9DC500EF8A77 /* UIAlerts+iOS9.m in Sources */,
B8269D3325C7A8C600488AB4 /* InputViewButton.swift in Sources */,
B8269D3D25C7B34D00488AB4 /* InputTextView.swift in Sources */,
@ -5174,6 +5134,8 @@
B8D0A25025E3678700C1835E /* LinkDeviceVC.swift in Sources */,
B894D0752339EDCF00B4D94D /* NukeDataModal.swift in Sources */,
7B4C75CD26BB92060000AC89 /* DeletedMessageView.swift in Sources */,
FDD250722837234B00198BDA /* MediaGalleryNavigationController.swift in Sources */,
FDD2506E283711D600198BDA /* DifferenceKit+Utilities.swift in Sources */,
B897621C25D201F7004F83B2 /* ScrollToBottomButton.swift in Sources */,
346B66311F4E29B200E5122F /* CropScaleImageViewController.swift in Sources */,
45E5A6991F61E6DE001E4A8A /* MarqueeLabel.swift in Sources */,

View File

@ -462,7 +462,7 @@ extension ConversationVC:
resetMentions()
if Environment.shared.preferences.soundInForeground() {
if GRDBStorage.shared[.playNotificationSoundInForeground] {
let soundID = Preferences.Sound.systemSoundId(for: .messageSent, quiet: true)
AudioServicesPlaySystemSound(soundID)
}
@ -697,13 +697,31 @@ extension ConversationVC:
default:
let viewController: UIViewController? = MediaGalleryViewModel.createDetailViewController(
for: self.viewModel.viewData.thread.id,
item: item,
threadVariant: self.viewModel.viewData.thread.variant,
interactionId: item.interactionId,
selectedAttachmentId: mediaView.attachment.id,
options: [ .sliderEnabled, .showAllMediaButton ]
)
if let viewController: UIViewController = viewController {
self.present(viewController, animated: true, completion: nil)
/// Delay becoming the first responder to make the return transition a little nicer (allows
/// for the footer on the detail view to slide out rather than instantly vanish)
self.delayFirstResponder = true
/// Dismiss the input before starting the presentation to make everything look smoother
self.resignFirstResponder()
/// Delay the actual presentation to give the 'resignFirstResponder' call the chance to complete
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200)) { [weak self] in
/// Lock the contentOffset of the tableView so the transition doesn't look buggy
self?.tableView.lockContentOffset = true
self?.present(viewController, animated: true) { [weak self] in
// Unlock the contentOffset so everything will be in the right
// place when we return
self?.tableView.lockContentOffset = false
}
}
}
}
@ -1527,7 +1545,7 @@ extension ConversationVC {
{
var newViewControllers = viewControllers
newViewControllers.remove(at: messageRequestsIndex)
self?.navigationController?.setViewControllers(newViewControllers, animated: false)
self?.navigationController?.viewControllers = newViewControllers
}
}
}
@ -1604,6 +1622,7 @@ extension ConversationVC {
extension ConversationVC: MediaPresentationContextProvider {
func mediaPresentationContext(mediaItem: Media, in coordinateSpace: UICoordinateSpace) -> MediaPresentationContext? {
guard case let .gallery(galleryItem) = mediaItem else { return nil }
// Note: According to Apple's docs the 'indexPathsForVisibleRows' method returns an
// unsorted array which means we can't use it to determine the desired 'visibleCell'
// we are after, due to this we will need to iterate all of the visible cells to find
@ -1632,7 +1651,8 @@ extension ConversationVC: MediaPresentationContextProvider {
let cornerRadius: CGFloat
let cornerMask: CACornerMask
let presentationFrame = coordinateSpace.convert(targetView.frame, from: mediaSuperview)
let presentationFrame: CGRect = coordinateSpace.convert(targetView.frame, from: mediaSuperview)
let frameInBubble: CGRect = messageCell.bubbleView.convert(targetView.frame, from: mediaSuperview)
if messageCell.bubbleView.bounds == targetView.bounds {
cornerRadius = messageCell.bubbleView.layer.cornerRadius
@ -1648,39 +1668,39 @@ extension ConversationVC: MediaPresentationContextProvider {
if
cellMaskedCorners.contains(.layerMinXMinYCorner) &&
targetView.frame.minX < CGFloat.leastNonzeroMagnitude &&
targetView.frame.minY < CGFloat.leastNonzeroMagnitude
frameInBubble.minX < CGFloat.leastNonzeroMagnitude &&
frameInBubble.minY < CGFloat.leastNonzeroMagnitude
{
newCornerMask.insert(.layerMinXMinYCorner)
}
if
cellMaskedCorners.contains(.layerMaxXMinYCorner) &&
abs(targetView.frame.maxX - messageCell.bubbleView.bounds.width) < CGFloat.leastNonzeroMagnitude &&
targetView.frame.minY < CGFloat.leastNonzeroMagnitude
abs(frameInBubble.maxX - messageCell.bubbleView.bounds.width) < CGFloat.leastNonzeroMagnitude &&
frameInBubble.minY < CGFloat.leastNonzeroMagnitude
{
newCornerMask.insert(.layerMaxXMinYCorner)
}
if
cellMaskedCorners.contains(.layerMinXMaxYCorner) &&
targetView.frame.minX < CGFloat.leastNonzeroMagnitude &&
abs(targetView.frame.maxY - messageCell.bubbleView.bounds.height) < CGFloat.leastNonzeroMagnitude
frameInBubble.minX < CGFloat.leastNonzeroMagnitude &&
abs(frameInBubble.maxY - messageCell.bubbleView.bounds.height) < CGFloat.leastNonzeroMagnitude
{
newCornerMask.insert(.layerMinXMaxYCorner)
}
if
cellMaskedCorners.contains(.layerMaxXMaxYCorner) &&
abs(targetView.frame.maxX - messageCell.bubbleView.bounds.width) < CGFloat.leastNonzeroMagnitude &&
abs(targetView.frame.maxY - messageCell.bubbleView.bounds.height) < CGFloat.leastNonzeroMagnitude
abs(frameInBubble.maxX - messageCell.bubbleView.bounds.width) < CGFloat.leastNonzeroMagnitude &&
abs(frameInBubble.maxY - messageCell.bubbleView.bounds.height) < CGFloat.leastNonzeroMagnitude
{
newCornerMask.insert(.layerMaxXMaxYCorner)
}
cornerMask = newCornerMask
}
return MediaPresentationContext(
mediaView: targetView,
presentationFrame: presentationFrame,

View File

@ -18,6 +18,10 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
private var dataChangeObservable: DatabaseCancellable?
private var hasLoadedInitialData: Bool = false
/// This flag indicates whether the data has been reloaded after a disappearance (it defaults to true as it will never
/// have disappeared before)
private var hasReloadedDataAfterDisappearance: Bool = true
var focusedMessageIndexPath: IndexPath?
var initialUnreadCount: UInt = 0
var unreadViewItems: [ConversationViewItem] = []
@ -50,7 +54,11 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
var baselineKeyboardHeight: CGFloat = 0
var audioSession: OWSAudioSession { Environment.shared.audioSession }
override var canBecomeFirstResponder: Bool { true }
/// This flag is used to temporarily prevent the ConversationVC from becoming the first responder (primarily used with
/// custom transitions from preventing them from being buggy
var delayFirstResponder: Bool = false
override var canBecomeFirstResponder: Bool { !delayFirstResponder }
override var inputAccessoryView: UIView? {
guard
@ -119,8 +127,8 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
return result
}()
lazy var tableView: UITableView = {
let result: UITableView = UITableView()
lazy var tableView: InsetLockableTableView = {
let result: InsetLockableTableView = InsetLockableTableView()
result.separatorStyle = .none
result.backgroundColor = .clear
result.showsVerticalScrollIndicator = false
@ -213,7 +221,7 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
return result
}()
private let messageRequestAcceptButton: UIButton = {
private lazy var messageRequestAcceptButton: UIButton = {
let result: UIButton = UIButton()
result.translatesAutoresizingMaskIntoConstraints = false
result.clipsToBounds = true
@ -244,7 +252,7 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
return result
}()
private let messageRequestDeleteButton: UIButton = {
private lazy var messageRequestDeleteButton: UIButton = {
let result: UIButton = UIButton()
result.translatesAutoresizingMaskIntoConstraints = false
result.clipsToBounds = true
@ -459,6 +467,14 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
highlightFocusedMessageIfNeeded()
didFinishInitialLayout = true
viewModel.markAllAsRead()
if delayFirstResponder {
delayFirstResponder = false
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(50)) { [weak self] in
self?.becomeFirstResponder()
}
}
}
override func viewWillDisappear(_ animated: Bool) {
@ -474,6 +490,7 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
super.viewDidDisappear(animated)
mediaCache.removeAllObjects()
hasReloadedDataAfterDisappearance = false
}
@objc func applicationDidBecomeActive(_ notification: Notification) {
@ -501,10 +518,11 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
}
private func handleUpdates(_ updatedViewData: ConversationViewModel.ViewData, initialLoad: Bool = false) {
// Ensure the first load runs without animations (if we don't do this the cells will animate
// in from a frame of CGRect.zero)
guard hasLoadedInitialData else {
// Ensure the first load or a load when returning from a child screen runs without animations (if
// we don't do this the cells will animate in from a frame of CGRect.zero or have a buggy transition)
guard hasLoadedInitialData && hasReloadedDataAfterDisappearance else {
hasLoadedInitialData = true
hasReloadedDataAfterDisappearance = true
UIView.performWithoutAnimation { handleUpdates(updatedViewData, initialLoad: true) }
return
}
@ -556,6 +574,12 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
let changeset = StagedChangeset(source: viewModel.viewData.items, target: updatedViewData.items)
tableView.reload(
using: StagedChangeset(source: viewModel.viewData.items, target: updatedViewData.items),
deleteSectionsAnimation: .bottom,
insertSectionsAnimation: .bottom,
reloadSectionsAnimation: .none,
deleteRowsAnimation: .bottom,
insertRowsAnimation: .bottom,
reloadRowsAnimation: .none,
interrupt: {
return $0.changeCount > 100
} // Prevent too many changes from causing performance issues
@ -635,6 +659,9 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
}
}
// MARK: Notifications
private func highlightFocusedMessageIfNeeded() {
if let indexPath = focusedMessageIndexPath, let cell = tableView.cellForRow(at: indexPath) as? VisibleMessageCell {
cell.highlight()

View File

@ -249,15 +249,20 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M
// Suggest that the user enable link previews if they haven't already and we haven't
// told them about link previews yet
let text = inputTextView.text!
let userDefaults = UserDefaults.standard
if !OWSLinkPreview.allPreviewUrls(forMessageBodyText: text).isEmpty && !SSKPreferences.areLinkPreviewsEnabled
&& !userDefaults[.hasSeenLinkPreviewSuggestion] {
let areLinkPreviewsEnabled: Bool = GRDBStorage.shared[.areLinkPreviewsEnabled]
if
!OWSLinkPreview.allPreviewUrls(forMessageBodyText: text).isEmpty &&
!areLinkPreviewsEnabled &&
!UserDefaults.standard[.hasSeenLinkPreviewSuggestion]
{
delegate?.showLinkPreviewSuggestionModal()
userDefaults[.hasSeenLinkPreviewSuggestion] = true
UserDefaults.standard[.hasSeenLinkPreviewSuggestion] = true
return
}
// Check that link previews are enabled
guard SSKPreferences.areLinkPreviewsEnabled else { return }
guard areLinkPreviewsEnabled else { return }
// Proceed
autoGenerateLinkPreview()
}

View File

@ -205,7 +205,7 @@ final class QuoteView: UIView {
authorLabel.lineBreakMode = .byTruncatingTail
authorLabel.text = Profile.displayName(
id: authorId,
context: (threadVariant == .openGroup ? .openGroup : .regular)
threadVariant: threadVariant
)
authorLabel.textColor = textColor
authorLabel.font = .boldSystemFont(ofSize: Values.smallFontSize)

View File

@ -10,7 +10,6 @@
#import <Curve25519Kit/Curve25519.h>
#import <SignalCoreKit/NSDate+OWS.h>
#import <SessionMessagingKit/Environment.h>
#import <SignalUtilitiesKit/OWSProfileManager.h>
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
#import <SignalUtilitiesKit/UIUtil.h>
#import <SessionMessagingKit/OWSPrimaryStorage.h>
@ -41,7 +40,6 @@ CGFloat kIconViewLength = 24;
@property (nonatomic) BOOL isDisappearingMessagesEnabled;
@property (nonatomic) NSInteger disappearingMessagesDurationIndex;
@property (nullable, nonatomic) MediaGallery *mediaGallery;
@property (nonatomic, readonly) UIImageView *avatarView;
@property (nonatomic, readonly) UILabel *disappearingMessagesDurationLabel;
@property (nonatomic) UILabel *displayNameLabel;

View File

@ -0,0 +1,35 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
/// This custom UITableView allows us to lock the contentOffset to a specific value - it's current used to prevent
/// the ConversationVC first responder resignation from making the MediaGalleryDetailViewController transition
/// from looking buggy (ie. the table scrolls down with the resignation during the transition)
public class InsetLockableTableView: UITableView {
public var lockContentOffset: Bool = false {
didSet {
guard !lockContentOffset else { return }
self.contentOffset = newOffset
}
}
public var oldOffset: CGPoint = .zero
public var newOffset: CGPoint = .zero
public override func layoutSubviews() {
newOffset = self.contentOffset
guard !lockContentOffset else {
self.contentOffset = CGPoint(
x: newOffset.x,
y: oldOffset.y
)
super.layoutSubviews()
return
}
super.layoutSubviews()
oldOffset = self.contentOffset
}
}

View File

@ -1,8 +1,15 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
final class LinkPreviewModal : Modal {
import UIKit
import GRDB
import SessionUIKit
import SessionMessagingKit
final class LinkPreviewModal: Modal {
private let onLinkPreviewsEnabled: () -> Void
// MARK: Lifecycle
// MARK: - Lifecycle
init(onLinkPreviewsEnabled: @escaping () -> Void) {
self.onLinkPreviewsEnabled = onLinkPreviewsEnabled
super.init(nibName: nil, bundle: nil)
@ -18,22 +25,23 @@ final class LinkPreviewModal : Modal {
override func populateContentView() {
// Title
let titleLabel = UILabel()
let titleLabel: UILabel = UILabel()
titleLabel.textColor = Colors.text
titleLabel.font = .boldSystemFont(ofSize: Values.largeFontSize)
titleLabel.text = NSLocalizedString("modal_link_previews_title", comment: "")
titleLabel.text = "modal_link_previews_title".localized()
titleLabel.textAlignment = .center
// Message
let messageLabel = UILabel()
let messageLabel: UILabel = UILabel()
messageLabel.textColor = Colors.text
messageLabel.font = .systemFont(ofSize: Values.smallFontSize)
let message = NSLocalizedString("modal_link_previews_explanation", comment: "")
messageLabel.text = message
messageLabel.text = "modal_link_previews_explanation".localized()
messageLabel.numberOfLines = 0
messageLabel.lineBreakMode = .byWordWrapping
messageLabel.textAlignment = .center
// Enable button
let enableButton = UIButton()
let enableButton: UIButton = UIButton()
enableButton.set(.height, to: Values.mediumButtonHeight)
enableButton.layer.cornerRadius = Modal.buttonCornerRadius
enableButton.backgroundColor = Colors.buttonBackground
@ -41,13 +49,15 @@ final class LinkPreviewModal : Modal {
enableButton.setTitleColor(Colors.text, for: UIControl.State.normal)
enableButton.setTitle(NSLocalizedString("modal_link_previews_button_title", comment: ""), for: UIControl.State.normal)
enableButton.addTarget(self, action: #selector(enable), for: UIControl.Event.touchUpInside)
// Button stack view
let buttonStackView = UIStackView(arrangedSubviews: [ cancelButton, enableButton ])
let buttonStackView: UIStackView = UIStackView(arrangedSubviews: [ cancelButton, enableButton ])
buttonStackView.axis = .horizontal
buttonStackView.spacing = Values.mediumSpacing
buttonStackView.distribution = .fillEqually
// Main stack view
let mainStackView = UIStackView(arrangedSubviews: [ titleLabel, messageLabel, buttonStackView ])
let mainStackView: UIStackView = UIStackView(arrangedSubviews: [ titleLabel, messageLabel, buttonStackView ])
mainStackView.axis = .vertical
mainStackView.spacing = Values.largeSpacing
contentView.addSubview(mainStackView)
@ -57,9 +67,13 @@ final class LinkPreviewModal : Modal {
contentView.pin(.bottom, to: .bottom, of: mainStackView, withInset: Values.largeSpacing)
}
// MARK: Interaction
// MARK: - Interaction
@objc private func enable() {
SSKPreferences.areLinkPreviewsEnabled = true
GRDBStorage.shared.writeAsync { db in
db[.areLinkPreviewsEnabled] = true
}
presentingViewController?.dismiss(animated: true, completion: nil)
onLinkPreviewsEnabled()
}

View File

@ -1,54 +0,0 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
#import <SignalUtilitiesKit/OWSViewController.h>
NS_ASSUME_NONNULL_BEGIN
@protocol ConversationViewItem;
@class GalleryItemBox;
@class MediaDetailViewController;
@class TSAttachment;
typedef NS_OPTIONS(NSInteger, MediaGalleryOption) {
MediaGalleryOptionSliderEnabled = 1 << 0,
MediaGalleryOptionShowAllMediaButton = 1 << 1
};
@protocol MediaDetailViewControllerDelegate <NSObject>
- (void)mediaDetailViewController:(MediaDetailViewController *)mediaDetailViewController
requestDeleteAttachment:(TSAttachment *)attachment;
- (void)mediaDetailViewController:(MediaDetailViewController *)mediaDetailViewController
isPlayingVideo:(BOOL)isPlayingVideo;
- (void)mediaDetailViewControllerDidTapMedia:(MediaDetailViewController *)mediaDetailViewController;
@end
@interface MediaDetailViewController : OWSViewController
@property (nonatomic, weak) id<MediaDetailViewControllerDelegate> delegate;
@property (nonatomic, readonly) GalleryItemBox *galleryItemBox;
// If viewItem is non-null, long press will show a menu controller.
- (instancetype)initWithGalleryItemBox:(GalleryItemBox *)galleryItemBox
viewItem:(nullable id<ConversationViewItem>)viewItem;
#pragma mark - Actions
- (void)didPressPlayBarButton:(id)sender;
- (void)didPressPauseBarButton:(id)sender;
- (void)playVideo;
// Stops playback and rewinds
- (void)stopAnyVideo;
- (void)setShouldHideToolbars:(BOOL)shouldHideToolbars;
- (void)zoomOutAnimated:(BOOL)isAnimated;
@end
NS_ASSUME_NONNULL_END

View File

@ -1,501 +0,0 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "MediaDetailViewController.h"
#import "AttachmentSharing.h"
#import "ConversationViewItem.h"
#import "Session-Swift.h"
#import "TSAttachmentStream.h"
#import "TSInteraction.h"
#import "UIColor+OWS.h"
#import "UIUtil.h"
#import "UIView+OWS.h"
#import <AVKit/AVKit.h>
#import <MediaPlayer/MPMoviePlayerViewController.h>
#import <MediaPlayer/MediaPlayer.h>
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
#import <SessionUtilitiesKit/NSData+Image.h>
#import <SessionUIKit/SessionUIKit.h>
#import <YYImage/YYImage.h>
NS_ASSUME_NONNULL_BEGIN
#pragma mark -
@interface MediaDetailViewController () <UIScrollViewDelegate,
UIGestureRecognizerDelegate,
PlayerProgressBarDelegate,
OWSVideoPlayerDelegate>
@property (nonatomic) UIScrollView *scrollView;
@property (nonatomic) UIView *mediaView;
@property (nonatomic) UIView *presentationView;
@property (nonatomic) UIView *replacingView;
@property (nonatomic) UIButton *shareButton;
@property (nonatomic) TSAttachmentStream *attachmentStream;
@property (nonatomic, nullable) id<ConversationViewItem> viewItem;
@property (nonatomic, nullable) UIImage *image;
@property (nonatomic, nullable) OWSVideoPlayer *videoPlayer;
@property (nonatomic, nullable) UIButton *playVideoButton;
@property (nonatomic, nullable) PlayerProgressBar *videoProgressBar;
@property (nonatomic, nullable) UIBarButtonItem *videoPlayBarButton;
@property (nonatomic, nullable) UIBarButtonItem *videoPauseBarButton;
@property (nonatomic, nullable) NSArray<NSLayoutConstraint *> *presentationViewConstraints;
@property (nonatomic, nullable) NSLayoutConstraint *mediaViewBottomConstraint;
@property (nonatomic, nullable) NSLayoutConstraint *mediaViewLeadingConstraint;
@property (nonatomic, nullable) NSLayoutConstraint *mediaViewTopConstraint;
@property (nonatomic, nullable) NSLayoutConstraint *mediaViewTrailingConstraint;
@end
#pragma mark -
@implementation MediaDetailViewController
- (void)dealloc
{
[self stopAnyVideo];
}
- (instancetype)initWithGalleryItemBox:(GalleryItemBox *)galleryItemBox
viewItem:(nullable id<ConversationViewItem>)viewItem
{
self = [super initWithNibName:nil bundle:nil];
if (!self) {
return self;
}
_galleryItemBox = galleryItemBox;
_viewItem = viewItem;
// We cache the image data in case the attachment stream is deleted.
__weak MediaDetailViewController *weakSelf = self;
_image = [galleryItemBox.attachmentStream
thumbnailImageLargeWithSuccess:^(UIImage *image) {
weakSelf.image = image;
[weakSelf updateContents];
[weakSelf updateMinZoomScale];
}
failure:^{
OWSLogWarn(@"Could not load media.");
}];
return self;
}
- (TSAttachmentStream *)attachmentStream
{
return self.galleryItemBox.attachmentStream;
}
- (BOOL)isAnimated
{
return self.attachmentStream.isAnimated;
}
- (BOOL)isVideo
{
return self.attachmentStream.isVideo;
}
- (void)viewDidLoad
{
[super viewDidLoad];
self.view.backgroundColor = LKColors.navigationBarBackground;
[self updateContents];
// Loki: Set navigation bar background color
UINavigationBar *navigationBar = self.navigationController.navigationBar;
[navigationBar setBackgroundImage:[UIImage new] forBarMetrics:UIBarMetricsDefault];
navigationBar.shadowImage = [UIImage new];
[navigationBar setTranslucent:NO];
navigationBar.barTintColor = LKColors.navigationBarBackground;
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self resetMediaFrame];
}
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
[self updateMinZoomScale];
[self centerMediaViewConstraints];
}
- (void)updateMinZoomScale
{
if (!self.image) {
self.scrollView.minimumZoomScale = 1.f;
self.scrollView.maximumZoomScale = 1.f;
self.scrollView.zoomScale = 1.f;
return;
}
CGSize viewSize = self.scrollView.bounds.size;
UIImage *image = self.image;
OWSAssertDebug(image);
if (image.size.width == 0 || image.size.height == 0) {
OWSFailDebug(@"Invalid image dimensions. %@", NSStringFromCGSize(image.size));
return;
}
CGFloat scaleWidth = viewSize.width / image.size.width;
CGFloat scaleHeight = viewSize.height / image.size.height;
CGFloat minScale = MIN(scaleWidth, scaleHeight);
if (minScale != self.scrollView.minimumZoomScale) {
self.scrollView.minimumZoomScale = minScale;
self.scrollView.maximumZoomScale = minScale * 8;
self.scrollView.zoomScale = minScale;
}
}
- (void)zoomOutAnimated:(BOOL)isAnimated
{
if (self.scrollView.zoomScale != self.scrollView.minimumZoomScale) {
[self.scrollView setZoomScale:self.scrollView.minimumZoomScale animated:isAnimated];
}
}
#pragma mark - Initializers
- (void)updateContents
{
[self.mediaView removeFromSuperview];
[self.scrollView removeFromSuperview];
[self.playVideoButton removeFromSuperview];
[self.videoProgressBar removeFromSuperview];
UIScrollView *scrollView = [UIScrollView new];
[self.view addSubview:scrollView];
self.scrollView = scrollView;
scrollView.delegate = self;
scrollView.showsVerticalScrollIndicator = NO;
scrollView.showsHorizontalScrollIndicator = NO;
scrollView.decelerationRate = UIScrollViewDecelerationRateFast;
if (@available(iOS 11.0, *)) {
[scrollView contentInsetAdjustmentBehavior];
} else {
self.automaticallyAdjustsScrollViewInsets = NO;
}
[scrollView ows_autoPinToSuperviewEdges];
if (self.isAnimated) {
if (self.attachmentStream.isValidImage) {
YYImage *animatedGif = [YYImage imageWithContentsOfFile:self.attachmentStream.originalFilePath];
YYAnimatedImageView *animatedView = [YYAnimatedImageView new];
animatedView.image = animatedGif;
self.mediaView = animatedView;
} else {
self.mediaView = [UIView new];
self.mediaView.backgroundColor = LKColors.unimportant;
}
} else if (!self.image) {
// Still loading thumbnail.
self.mediaView = [UIView new];
self.mediaView.backgroundColor = LKColors.unimportant;
} else if (self.isVideo) {
if (self.attachmentStream.isValidVideo) {
self.mediaView = [self buildVideoPlayerView];
} else {
self.mediaView = [UIView new];
self.mediaView.backgroundColor = LKColors.unimportant;
}
} else {
// Present the static image using standard UIImageView
UIImageView *imageView = [[UIImageView alloc] initWithImage:self.image];
self.mediaView = imageView;
}
OWSAssertDebug(self.mediaView);
// We add these gestures to mediaView rather than
// the root view so that interacting with the video player
// progres bar doesn't trigger any of these gestures.
[self addGestureRecognizersToView:self.mediaView];
[scrollView addSubview:self.mediaView];
self.mediaViewLeadingConstraint = [self.mediaView autoPinEdgeToSuperviewEdge:ALEdgeLeading];
self.mediaViewTopConstraint = [self.mediaView autoPinEdgeToSuperviewEdge:ALEdgeTop];
self.mediaViewTrailingConstraint = [self.mediaView autoPinEdgeToSuperviewEdge:ALEdgeTrailing];
self.mediaViewBottomConstraint = [self.mediaView autoPinEdgeToSuperviewEdge:ALEdgeBottom];
self.mediaView.contentMode = UIViewContentModeScaleAspectFit;
self.mediaView.userInteractionEnabled = YES;
self.mediaView.clipsToBounds = YES;
self.mediaView.layer.allowsEdgeAntialiasing = YES;
self.mediaView.translatesAutoresizingMaskIntoConstraints = NO;
// Use trilinear filters for better scaling quality at
// some performance cost.
self.mediaView.layer.minificationFilter = kCAFilterTrilinear;
self.mediaView.layer.magnificationFilter = kCAFilterTrilinear;
if (self.isVideo) {
PlayerProgressBar *videoProgressBar = [PlayerProgressBar new];
videoProgressBar.delegate = self;
videoProgressBar.player = self.videoPlayer.avPlayer;
// We hide the progress bar until either:
// 1. Video completes playing
// 2. User taps the screen
videoProgressBar.hidden = YES;
self.videoProgressBar = videoProgressBar;
[self.view addSubview:videoProgressBar];
[videoProgressBar autoPinWidthToSuperview];
[videoProgressBar autoPinEdgeToSuperviewSafeArea:ALEdgeTop];
CGFloat kVideoProgressBarHeight = 44;
[videoProgressBar autoSetDimension:ALDimensionHeight toSize:kVideoProgressBarHeight];
UIButton *playVideoButton = [UIButton new];
self.playVideoButton = playVideoButton;
[playVideoButton addTarget:self action:@selector(playVideo) forControlEvents:UIControlEventTouchUpInside];
UIImage *playImage = [UIImage imageNamed:@"CirclePlay"];
[playVideoButton setBackgroundImage:playImage forState:UIControlStateNormal];
playVideoButton.contentMode = UIViewContentModeScaleAspectFill;
[self.view addSubview:playVideoButton];
CGFloat playVideoButtonWidth = 72.f;
[playVideoButton autoSetDimensionsToSize:CGSizeMake(playVideoButtonWidth, playVideoButtonWidth)];
[playVideoButton autoCenterInSuperview];
}
}
- (UIView *)buildVideoPlayerView
{
NSURL *_Nullable attachmentUrl = self.attachmentStream.originalMediaURL;
NSFileManager *fileManager = [NSFileManager defaultManager];
if (![fileManager fileExistsAtPath:[attachmentUrl path]]) {
OWSFailDebug(@"Missing video file");
}
OWSVideoPlayer *player = [[OWSVideoPlayer alloc] initWithUrl:attachmentUrl];
[player seekToTime:kCMTimeZero];
player.delegate = self;
self.videoPlayer = player;
VideoPlayerView *playerView = [VideoPlayerView new];
playerView.player = player.avPlayer;
[NSLayoutConstraint autoSetPriority:UILayoutPriorityDefaultLow
forConstraints:^{
[playerView autoSetDimensionsToSize:self.image.size];
}];
return playerView;
}
- (void)setShouldHideToolbars:(BOOL)shouldHideToolbars
{
self.videoProgressBar.hidden = shouldHideToolbars;
}
- (void)addGestureRecognizersToView:(UIView *)view
{
UITapGestureRecognizer *doubleTap =
[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(didDoubleTapImage:)];
doubleTap.numberOfTapsRequired = 2;
[view addGestureRecognizer:doubleTap];
UITapGestureRecognizer *singleTap =
[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(didSingleTapImage:)];
[singleTap requireGestureRecognizerToFail:doubleTap];
[view addGestureRecognizer:singleTap];
}
#pragma mark - Gesture Recognizers
- (void)didSingleTapImage:(UITapGestureRecognizer *)gesture
{
[self.delegate mediaDetailViewControllerDidTapMedia:self];
}
- (void)didDoubleTapImage:(UITapGestureRecognizer *)gesture
{
OWSLogVerbose(@"did double tap image.");
if (self.scrollView.zoomScale == self.scrollView.minimumZoomScale) {
CGFloat kDoubleTapZoomScale = 2;
CGFloat zoomWidth = self.scrollView.width / kDoubleTapZoomScale;
CGFloat zoomHeight = self.scrollView.height / kDoubleTapZoomScale;
// center zoom rect around tapLocation
CGPoint tapLocation = [gesture locationInView:self.scrollView];
CGFloat zoomX = MAX(0, tapLocation.x - zoomWidth / 2);
CGFloat zoomY = MAX(0, tapLocation.y - zoomHeight / 2);
CGRect zoomRect = CGRectMake(zoomX, zoomY, zoomWidth, zoomHeight);
CGRect translatedRect = [self.mediaView convertRect:zoomRect fromView:self.scrollView];
[self.scrollView zoomToRect:translatedRect animated:YES];
} else {
// If already zoomed in at all, zoom out all the way.
[self zoomOutAnimated:YES];
}
}
- (void)didPressPlayBarButton:(id)sender
{
OWSAssertDebug(self.isVideo);
OWSAssertDebug(self.videoPlayer);
[self playVideo];
}
- (void)didPressPauseBarButton:(id)sender
{
OWSAssertDebug(self.isVideo);
OWSAssertDebug(self.videoPlayer);
[self pauseVideo];
}
#pragma mark - UIScrollViewDelegate
- (nullable UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
{
return self.mediaView;
}
- (void)centerMediaViewConstraints
{
OWSAssertDebug(self.scrollView);
CGSize scrollViewSize = self.scrollView.bounds.size;
CGSize imageViewSize = self.mediaView.frame.size;
CGFloat yOffset = MAX(0, (scrollViewSize.height - imageViewSize.height) / 2);
self.mediaViewTopConstraint.constant = yOffset;
self.mediaViewBottomConstraint.constant = yOffset;
CGFloat xOffset = MAX(0, (scrollViewSize.width - imageViewSize.width) / 2);
self.mediaViewLeadingConstraint.constant = xOffset;
self.mediaViewTrailingConstraint.constant = xOffset;
}
- (void)scrollViewDidZoom:(UIScrollView *)scrollView
{
[self centerMediaViewConstraints];
[self.view layoutIfNeeded];
}
- (void)resetMediaFrame
{
// HACK: Setting the frame to itself *seems* like it should be a no-op, but
// it ensures the content is drawn at the right frame. In particular I was
// reproducibly seeing some images squished (they were EXIF rotated, maybe
// related). similar to this report:
// https://stackoverflow.com/questions/27961884/swift-uiimageview-stretched-aspect
[self.view layoutIfNeeded];
self.mediaView.frame = self.mediaView.frame;
}
#pragma mark - Video Playback
- (void)playVideo
{
OWSAssertDebug(self.videoPlayer);
self.playVideoButton.hidden = YES;
[self.videoPlayer play];
[self.delegate mediaDetailViewController:self isPlayingVideo:YES];
}
- (void)pauseVideo
{
OWSAssertDebug(self.isVideo);
OWSAssertDebug(self.videoPlayer);
[self.videoPlayer pause];
[self.delegate mediaDetailViewController:self isPlayingVideo:NO];
}
- (void)stopAnyVideo
{
if (self.isVideo) {
[self stopVideo];
}
}
- (void)stopVideo
{
OWSAssertDebug(self.isVideo);
OWSAssertDebug(self.videoPlayer);
[self.videoPlayer stop];
self.playVideoButton.hidden = NO;
[self.delegate mediaDetailViewController:self isPlayingVideo:NO];
}
#pragma mark - OWSVideoPlayer
- (void)videoPlayerDidPlayToCompletion:(OWSVideoPlayer *)videoPlayer
{
OWSAssertDebug(self.isVideo);
OWSAssertDebug(self.videoPlayer);
OWSLogVerbose(@"");
[self stopVideo];
}
#pragma mark - PlayerProgressBarDelegate
- (void)playerProgressBarDidStartScrubbing:(PlayerProgressBar *)playerProgressBar
{
OWSAssertDebug(self.videoPlayer);
[self.videoPlayer pause];
}
- (void)playerProgressBar:(PlayerProgressBar *)playerProgressBar scrubbedToTime:(CMTime)time
{
OWSAssertDebug(self.videoPlayer);
[self.videoPlayer seekToTime:time];
}
- (void)playerProgressBar:(PlayerProgressBar *)playerProgressBar
didFinishScrubbingAtTime:(CMTime)time
shouldResumePlayback:(BOOL)shouldResumePlayback
{
OWSAssertDebug(self.videoPlayer);
[self.videoPlayer seekToTime:time];
if (shouldResumePlayback) {
[self.videoPlayer play];
}
}
#pragma mark - Saving images to Camera Roll
- (void)image:(UIImage *)image didFinishSavingWithError:(NSError *)error contextInfo:(void *)contextInfo
{
if (error) {
OWSLogWarn(@"There was a problem saving <%@> to camera roll.", error.localizedDescription);
}
}
@end
NS_ASSUME_NONNULL_END

View File

@ -1,3 +1,427 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import UIKit
import YYImage
import SessionUIKit
import SignalUtilitiesKit
import SessionMessagingKit
public enum MediaGalleryOption {
case sliderEnabled
case showAllMediaButton
}
class MediaDetailViewController: OWSViewController, UIScrollViewDelegate, OWSVideoPlayerDelegate, PlayerProgressBarDelegate {
public let galleryItem: MediaGalleryViewModel.Item
public weak var delegate: MediaDetailViewControllerDelegate?
private var image: UIImage?
// MARK: - UI
private var mediaViewBottomConstraint: NSLayoutConstraint?
private var mediaViewLeadingConstraint: NSLayoutConstraint?
private var mediaViewTopConstraint: NSLayoutConstraint?
private var mediaViewTrailingConstraint: NSLayoutConstraint?
private lazy var scrollView: UIScrollView = {
let result: UIScrollView = UIScrollView()
result.showsVerticalScrollIndicator = false
result.showsHorizontalScrollIndicator = false
result.contentInsetAdjustmentBehavior = .never
result.decelerationRate = .fast
result.delegate = self
return result
}()
public var mediaView: UIView = UIView()
private var playVideoButton: UIButton = UIButton()
private var videoProgressBar: PlayerProgressBar = PlayerProgressBar()
private var videoPlayer: OWSVideoPlayer?
// MARK: - Initialization
init(
galleryItem: MediaGalleryViewModel.Item,
delegate: MediaDetailViewControllerDelegate? = nil
) {
self.galleryItem = galleryItem
self.delegate = delegate
super.init(nibName: nil, bundle: nil)
// We cache the image data in case the attachment stream is deleted.
galleryItem.attachment.thumbnail(
size: .large,
success: { [weak self] image, _ in
self?.image = image
// Only reload the content if the view has already loaded (if it
// hasn't then it'll load with the image immediately)
if self?.isViewLoaded == true {
self?.updateContents()
self?.updateMinZoomScale()
}
},
failure: {
SNLog("Could not load media.")
}
)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.stopAnyVideo()
}
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = Colors.navigationBarBackground
self.view.addSubview(scrollView)
scrollView.pin(to: self.view)
self.updateContents()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.resetMediaFrame()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if mediaView is YYAnimatedImageView {
// Add a slight delay before starting the gif animation to prevent it from looking
// buggy due to the custom transition
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(250)) { [weak self] in
(self?.mediaView as? YYAnimatedImageView)?.startAnimating()
}
}
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.updateMinZoomScale()
self.centerMediaViewConstraints()
}
// MARK: - Functions
private func updateMinZoomScale() {
guard let image: UIImage = image else {
self.scrollView.minimumZoomScale = 1
self.scrollView.maximumZoomScale = 1
self.scrollView.zoomScale = 1
return
}
let viewSize: CGSize = self.scrollView.bounds.size
guard image.size.width > 0 && image.size.height > 0 else {
SNLog("Invalid image dimensions (\(image.size.width), \(image.size.height))")
return;
}
let scaleWidth: CGFloat = (viewSize.width / image.size.width)
let scaleHeight: CGFloat = (viewSize.height / image.size.height)
let minScale: CGFloat = min(scaleWidth, scaleHeight)
if minScale != self.scrollView.minimumZoomScale {
self.scrollView.minimumZoomScale = minScale
self.scrollView.maximumZoomScale = (minScale * 8)
self.scrollView.zoomScale = minScale
}
}
public func zoomOut(animated: Bool) {
if self.scrollView.zoomScale != self.scrollView.minimumZoomScale {
self.scrollView.setZoomScale(self.scrollView.minimumZoomScale, animated: animated)
}
}
// MARK: - Content
private func updateContents() {
self.mediaView.removeFromSuperview()
self.playVideoButton.removeFromSuperview()
self.videoProgressBar.removeFromSuperview()
// TODO: COnfirm this
scrollView.zoomScale = 1
if self.galleryItem.attachment.isAnimated {
if self.galleryItem.attachment.isValid, let originalFilePath: String = self.galleryItem.attachment.originalFilePath {
let animatedView: YYAnimatedImageView = YYAnimatedImageView()
animatedView.autoPlayAnimatedImage = false
animatedView.image = YYImage(contentsOfFile: originalFilePath)
self.mediaView = animatedView
}
else {
self.mediaView = UIView()
self.mediaView.backgroundColor = Colors.unimportant
}
}
else if self.image == nil {
// Still loading thumbnail.
self.mediaView = UIView()
self.mediaView.backgroundColor = Colors.unimportant
}
else if self.galleryItem.attachment.isVideo {
if self.galleryItem.attachment.isValid {
self.mediaView = self.buildVideoPlayerView()
}
else {
self.mediaView = UIView()
self.mediaView.backgroundColor = Colors.unimportant
}
}
else {
// Present the static image using standard UIImageView
self.mediaView = UIImageView(image: self.image)
}
// We add these gestures to mediaView rather than
// the root view so that interacting with the video player
// progres bar doesn't trigger any of these gestures.
self.addGestureRecognizers(to: self.mediaView)
self.scrollView.addSubview(self.mediaView)
self.mediaViewLeadingConstraint = self.mediaView.pin(.leading, to: .leading, of: self.scrollView)
self.mediaViewTopConstraint = self.mediaView.pin(.top, to: .top, of: self.scrollView)
self.mediaViewTrailingConstraint = self.mediaView.pin(.trailing, to: .trailing, of: self.scrollView)
self.mediaViewBottomConstraint = self.mediaView.pin(.bottom, to: .bottom, of: self.scrollView)
self.mediaView.contentMode = .scaleAspectFit
self.mediaView.isUserInteractionEnabled = true
self.mediaView.clipsToBounds = true
self.mediaView.layer.allowsEdgeAntialiasing = true
self.mediaView.translatesAutoresizingMaskIntoConstraints = false
// Use trilinear filters for better scaling quality at
// some performance cost.
self.mediaView.layer.minificationFilter = .trilinear
self.mediaView.layer.magnificationFilter = .trilinear
if self.galleryItem.attachment.isVideo {
self.videoProgressBar = PlayerProgressBar()
self.videoProgressBar.delegate = self
self.videoProgressBar.player = self.videoPlayer?.avPlayer
// We hide the progress bar until either:
// 1. Video completes playing
// 2. User taps the screen
self.videoProgressBar.isHidden = false
self.view.addSubview(self.videoProgressBar)
self.videoProgressBar.autoPinWidthToSuperview()
self.videoProgressBar.autoPinEdge(toSuperviewSafeArea: .top)
self.videoProgressBar.autoSetDimension(.height, toSize: 44)
self.playVideoButton = UIButton()
self.playVideoButton.contentMode = .scaleAspectFill
self.playVideoButton.setBackgroundImage(UIImage(named: "CirclePlay"), for: .normal)
self.playVideoButton.addTarget(self, action: #selector(playVideo), for: .touchUpInside)
self.view.addSubview(self.playVideoButton)
self.playVideoButton.set(.width, to: 72)
self.playVideoButton.set(.height, to: 72)
self.playVideoButton.center(in: self.view)
}
}
private func buildVideoPlayerView() -> UIView {
guard
let originalFilePath: String = self.galleryItem.attachment.originalFilePath,
FileManager.default.fileExists(atPath: originalFilePath)
else {
owsFailDebug("Missing video file")
return UIView()
}
self.videoPlayer = OWSVideoPlayer(url: URL(fileURLWithPath: originalFilePath))
self.videoPlayer?.seek(to: .zero)
self.videoPlayer?.delegate = self
let imageSize: CGSize = (self.image?.size ?? .zero)
let playerView: VideoPlayerView = VideoPlayerView()
playerView.player = self.videoPlayer?.avPlayer
NSLayoutConstraint.autoSetPriority(.defaultLow) {
playerView.autoSetDimensions(to: imageSize)
}
return playerView
}
public func setShouldHideToolbars(_ shouldHideToolbars: Bool) {
self.videoProgressBar.isHidden = shouldHideToolbars
}
private func addGestureRecognizers(to view: UIView) {
let doubleTap: UITapGestureRecognizer = UITapGestureRecognizer(
target: self,
action: #selector(didDoubleTapImage(_:))
)
doubleTap.numberOfTapsRequired = 2
view.addGestureRecognizer(doubleTap)
let singleTap: UITapGestureRecognizer = UITapGestureRecognizer(
target: self,
action: #selector(didSingleTapImage(_:))
)
singleTap.require(toFail: doubleTap)
view.addGestureRecognizer(singleTap)
}
// MARK: - Gesture Recognizers
@objc private func didSingleTapImage(_ gesture: UITapGestureRecognizer) {
self.delegate?.mediaDetailViewControllerDidTapMedia(self)
}
@objc private func didDoubleTapImage(_ gesture: UITapGestureRecognizer) {
guard self.scrollView.zoomScale == self.scrollView.minimumZoomScale else {
// If already zoomed in at all, zoom out all the way.
self.zoomOut(animated: true)
return
}
let doubleTapZoomScale: CGFloat = 2
let zoomWidth: CGFloat = (self.scrollView.bounds.width / doubleTapZoomScale)
let zoomHeight: CGFloat = (self.scrollView.bounds.height / doubleTapZoomScale)
// Center zoom rect around tapLocation
let tapLocation: CGPoint = gesture.location(in: self.scrollView)
let zoomX: CGFloat = max(0, tapLocation.x - zoomWidth / 2)
let zoomY: CGFloat = max(0, tapLocation.y - zoomHeight / 2)
let zoomRect: CGRect = CGRect(x: zoomX, y: zoomY, width: zoomWidth, height: zoomHeight)
let translatedRect: CGRect = self.mediaView.convert(zoomRect, to: self.scrollView)
self.scrollView.zoom(to: translatedRect, animated: true)
}
@objc public func didPressPlayBarButton() {
self.playVideo()
}
@objc public func didPressPauseBarButton() {
self.pauseVideo()
}
// MARK: - UIScrollViewDelegate
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return self.mediaView
}
private func centerMediaViewConstraints() {
let scrollViewSize: CGSize = self.scrollView.bounds.size
let imageViewSize: CGSize = self.mediaView.frame.size
// We want to modify the yOffset so the content remains centered on the screen (we can do this
// by subtracting half the parentViewController's y position)
//
// Note: Due to weird partial-pixel value rendering behaviours we need to round the inset either
// up or down depending on which direction the partial-pixel would end up rounded to make it
// align correctly
let halfHeightDiff: CGFloat = ((self.scrollView.bounds.size.height - self.mediaView.frame.size.height) / 2)
let shouldRoundUp: Bool = (round(halfHeightDiff) - halfHeightDiff > 0)
let yOffset: CGFloat = (
round((scrollViewSize.height - imageViewSize.height) / 2) -
(shouldRoundUp ?
ceil((self.parent?.view.frame.origin.y ?? 0) / 2) :
floor((self.parent?.view.frame.origin.y ?? 0) / 2)
)
)
self.mediaViewTopConstraint?.constant = yOffset
self.mediaViewBottomConstraint?.constant = yOffset
let xOffset: CGFloat = max(0, (scrollViewSize.width - imageViewSize.width) / 2)
self.mediaViewLeadingConstraint?.constant = xOffset
self.mediaViewTrailingConstraint?.constant = xOffset
}
func scrollViewDidZoom(_ scrollView: UIScrollView) {
self.centerMediaViewConstraints()
self.view.layoutIfNeeded()
}
private func resetMediaFrame() {
// HACK: Setting the frame to itself *seems* like it should be a no-op, but
// it ensures the content is drawn at the right frame. In particular I was
// reproducibly seeing some images squished (they were EXIF rotated, maybe
// related). similar to this report:
// https://stackoverflow.com/questions/27961884/swift-uiimageview-stretched-aspect
self.view.layoutIfNeeded()
self.mediaView.frame = self.mediaView.frame
}
// MARK: - Video Playback
@objc public func playVideo() {
self.playVideoButton.isHidden = true
self.videoPlayer?.play()
self.delegate?.mediaDetailViewController(self, isPlayingVideo: true)
}
private func pauseVideo() {
self.videoPlayer?.pause()
self.delegate?.mediaDetailViewController(self, isPlayingVideo: false)
}
public func stopAnyVideo() {
guard self.galleryItem.attachment.isVideo else { return }
self.stopVideo()
}
private func stopVideo() {
self.videoPlayer?.stop()
self.playVideoButton.isHidden = false
self.delegate?.mediaDetailViewController(self, isPlayingVideo: false)
}
// MARK: - OWSVideoPlayerDelegate
func videoPlayerDidPlayToCompletion(_ videoPlayer: OWSVideoPlayer) {
self.stopVideo()
}
// MARK: - PlayerProgressBarDelegate
func playerProgressBarDidStartScrubbing(_ playerProgressBar: PlayerProgressBar) {
self.videoPlayer?.pause()
}
func playerProgressBar(_ playerProgressBar: PlayerProgressBar, scrubbedToTime time: CMTime) {
self.videoPlayer?.seek(to: time)
}
func playerProgressBar(_ playerProgressBar: PlayerProgressBar, didFinishScrubbingAtTime time: CMTime, shouldResumePlayback: Bool) {
self.videoPlayer?.seek(to: time)
if shouldResumePlayback {
self.videoPlayer?.play()
}
}
}
// MARK: - MediaDetailViewControllerDelegate
protocol MediaDetailViewControllerDelegate: AnyObject {
func mediaDetailViewController(_ mediaDetailViewController: MediaDetailViewController, isPlayingVideo: Bool)
func mediaDetailViewControllerDidTapMedia(_ mediaDetailViewController: MediaDetailViewController)
}

View File

@ -0,0 +1,84 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import SignalUtilitiesKit
import SessionUIKit
class MediaGalleryNavigationController: OWSNavigationController {
// HACK: Though we don't have an input accessory view, the VC we are presented above (ConversationVC) does.
// If the app is backgrounded and then foregrounded, when OWSWindowManager calls mainWindow.makeKeyAndVisible
// the ConversationVC's inputAccessoryView will appear *above* us unless we'd previously become first responder.
override public var canBecomeFirstResponder: Bool {
return true
}
// MARK: - UI
private lazy var backgroundView: UIView = {
let result: UIView = UIView()
result.backgroundColor = Colors.navigationBarBackground
return result
}()
// MARK: - View Lifecycle
override var preferredStatusBarStyle: UIStatusBarStyle {
return (isLightMode ? .default : .lightContent)
}
override func viewDidLoad() {
super.viewDidLoad()
guard let navigationBar = self.navigationBar as? OWSNavigationBar else {
owsFailDebug("navigationBar had unexpected class: \(self.navigationBar)")
return
}
view.backgroundColor = Colors.navigationBarBackground
navigationBar.setBackgroundImage(UIImage(), for: UIBarMetrics.default)
navigationBar.shadowImage = UIImage()
navigationBar.isTranslucent = false
navigationBar.barTintColor = Colors.navigationBarBackground
// Insert a view to ensure the nav bar colour goes to the top of the screen
relayoutBackgroundView()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// If the user's device is already rotated, try to respect that by rotating to landscape now
UIViewController.attemptRotationToDeviceOrientation()
}
// MARK: - Orientation
public override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .allButUpsideDown
}
// MARK: - Functions
private func relayoutBackgroundView() {
guard !backgroundView.isHidden else {
backgroundView.removeFromSuperview()
return
}
view.insertSubview(backgroundView, belowSubview: navigationBar)
backgroundView.pin(.top, to: .top, of: view)
backgroundView.pin(.left, to: .left, of: navigationBar)
backgroundView.pin(.right, to: .right, of: navigationBar)
backgroundView.pin(.bottom, to: .bottom, of: navigationBar)
}
override func setNavigationBarHidden(_ hidden: Bool, animated: Bool) {
super.setNavigationBarHidden(hidden, animated: animated)
backgroundView.isHidden = hidden
relayoutBackgroundView()
}
}

View File

@ -1,903 +0,0 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
import Foundation
public enum GalleryDirection {
case before, after, around
}
class MediaGalleryAlbum {
private var originalItems: [MediaGalleryItem]
var items: [MediaGalleryItem] {
get {
guard let mediaGalleryDataSource = self.mediaGalleryDataSource else {
owsFailDebug("mediaGalleryDataSource was unexpectedly nil")
return originalItems
}
return originalItems.filter { !mediaGalleryDataSource.deletedGalleryItems.contains($0) }
}
}
weak var mediaGalleryDataSource: MediaGalleryDataSource?
init(items: [MediaGalleryItem]) {
self.originalItems = items
}
func add(item: MediaGalleryItem) {
guard !originalItems.contains(item) else {
return
}
originalItems.append(item)
originalItems.sort { (lhs, rhs) -> Bool in
return lhs.albumIndex < rhs.albumIndex
}
}
}
public class MediaGalleryItem: Equatable, Hashable {
let message: TSMessage
let attachmentStream: TSAttachmentStream
let galleryDate: GalleryDate
let captionForDisplay: String?
let albumIndex: Int
var album: MediaGalleryAlbum?
let orderingKey: MediaGalleryItemOrderingKey
init(message: TSMessage, attachmentStream: TSAttachmentStream) {
self.message = message
self.attachmentStream = attachmentStream
self.captionForDisplay = attachmentStream.caption?.filterForDisplay
self.galleryDate = GalleryDate(message: message)
self.albumIndex = message.attachmentIds.index(of: attachmentStream.uniqueId!)
self.orderingKey = MediaGalleryItemOrderingKey(messageSortKey: message.sortId, attachmentSortKey: albumIndex)
}
var isVideo: Bool {
return attachmentStream.isVideo
}
var isAnimated: Bool {
return attachmentStream.isAnimated
}
var isImage: Bool {
return attachmentStream.isImage
}
var imageSize: CGSize {
return attachmentStream.imageSize()
}
public typealias AsyncThumbnailBlock = (UIImage) -> Void
func thumbnailImage(async:@escaping AsyncThumbnailBlock) -> UIImage? {
return attachmentStream.thumbnailImageSmall(success: async, failure: {})
}
// MARK: Equatable
public static func == (lhs: MediaGalleryItem, rhs: MediaGalleryItem) -> Bool {
return lhs.attachmentStream.uniqueId == rhs.attachmentStream.uniqueId
}
// MARK: Hashable
public var hashValue: Int {
return attachmentStream.uniqueId?.hashValue ?? attachmentStream.hashValue
}
// MARK: Sorting
struct MediaGalleryItemOrderingKey: Comparable {
let messageSortKey: UInt64
let attachmentSortKey: Int
// MARK: Comparable
static func < (lhs: MediaGalleryItem.MediaGalleryItemOrderingKey, rhs: MediaGalleryItem.MediaGalleryItemOrderingKey) -> Bool {
if lhs.messageSortKey < rhs.messageSortKey {
return true
}
if lhs.messageSortKey == rhs.messageSortKey {
if lhs.attachmentSortKey < rhs.attachmentSortKey {
return true
}
}
return false
}
}
}
public struct GalleryDate: Hashable, Comparable, Equatable {
let year: Int
let month: Int
init(message: TSMessage) {
let date = message.dateForUI()
self.year = Calendar.current.component(.year, from: date)
self.month = Calendar.current.component(.month, from: date)
}
init(year: Int, month: Int) {
assert(month >= 1 && month <= 12)
self.year = year
self.month = month
}
private var isThisMonth: Bool {
let now = Date()
let year = Calendar.current.component(.year, from: now)
let month = Calendar.current.component(.month, from: now)
let thisMonth = GalleryDate(year: year, month: month)
return self == thisMonth
}
public var date: Date {
var components = DateComponents()
components.month = self.month
components.year = self.year
return Calendar.current.date(from: components)!
}
private var isThisYear: Bool {
let now = Date()
let thisYear = Calendar.current.component(.year, from: now)
return self.year == thisYear
}
static let thisYearFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "MMMM"
return formatter
}()
static let olderFormatter: DateFormatter = {
let formatter = DateFormatter()
// FIXME localize for RTL, or is there a built in way to do this?
formatter.dateFormat = "MMMM yyyy"
return formatter
}()
var localizedString: String {
if isThisMonth {
return NSLocalizedString("MEDIA_GALLERY_THIS_MONTH_HEADER", comment: "Section header in media gallery collection view")
} else if isThisYear {
return type(of: self).thisYearFormatter.string(from: self.date)
} else {
return type(of: self).olderFormatter.string(from: self.date)
}
}
// MARK: Hashable
public var hashValue: Int {
return month.hashValue ^ year.hashValue
}
// MARK: Comparable
public static func < (lhs: GalleryDate, rhs: GalleryDate) -> Bool {
if lhs.year != rhs.year {
return lhs.year < rhs.year
} else if lhs.month != rhs.month {
return lhs.month < rhs.month
} else {
return false
}
}
// MARK: Equatable
public static func == (lhs: GalleryDate, rhs: GalleryDate) -> Bool {
return lhs.month == rhs.month && lhs.year == rhs.year
}
}
protocol MediaGalleryDataSource: class {
var hasFetchedOldest: Bool { get }
var hasFetchedMostRecent: Bool { get }
var galleryItems: [MediaGalleryItem] { get }
var galleryItemCount: Int { get }
var sections: [GalleryDate: [MediaGalleryItem]] { get }
var sectionDates: [GalleryDate] { get }
var deletedAttachments: Set<TSAttachment> { get }
var deletedGalleryItems: Set<MediaGalleryItem> { get }
func ensureGalleryItemsLoaded(_ direction: GalleryDirection, item: MediaGalleryItem, amount: UInt, completion: ((IndexSet, [IndexPath]) -> Void)?)
func galleryItem(before currentItem: MediaGalleryItem) -> MediaGalleryItem?
func galleryItem(after currentItem: MediaGalleryItem) -> MediaGalleryItem?
func showAllMedia(focusedItem: MediaGalleryItem)
func dismissMediaDetailViewController(_ mediaDetailViewController: MediaPageViewController, animated isAnimated: Bool, completion: (() -> Void)?)
func delete(items: [MediaGalleryItem], initiatedBy: AnyObject)
}
protocol MediaGalleryDataSourceDelegate: class {
func mediaGalleryDataSource(_ mediaGalleryDataSource: MediaGalleryDataSource, willDelete items: [MediaGalleryItem], initiatedBy: AnyObject)
func mediaGalleryDataSource(_ mediaGalleryDataSource: MediaGalleryDataSource, deletedSections: IndexSet, deletedItems: [IndexPath])
}
class MediaGalleryNavigationController: OWSNavigationController {
var retainUntilDismissed: MediaGallery?
// HACK: Though we don't have an input accessory view, the VC we are presented above (ConversationVC) does.
// If the app is backgrounded and then foregrounded, when OWSWindowManager calls mainWindow.makeKeyAndVisible
// the ConversationVC's inputAccessoryView will appear *above* us unless we'd previously become first responder.
override public var canBecomeFirstResponder: Bool {
Logger.debug("")
return true
}
// MARK: View Lifecycle
override var preferredStatusBarStyle: UIStatusBarStyle {
return isLightMode ? .default : .lightContent
}
override func viewDidLoad() {
super.viewDidLoad()
guard let navigationBar = self.navigationBar as? OWSNavigationBar else {
owsFailDebug("navigationBar had unexpected class: \(self.navigationBar)")
return
}
view.backgroundColor = Colors.navigationBarBackground
navigationBar.setBackgroundImage(UIImage(), for: UIBarMetrics.default)
navigationBar.shadowImage = UIImage()
navigationBar.isTranslucent = false
navigationBar.barTintColor = Colors.navigationBarBackground
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// If the user's device is already rotated, try to respect that by rotating to landscape now
UIViewController.attemptRotationToDeviceOrientation()
}
// MARK: Orientation
public override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .allButUpsideDown
}
}
@objc
class MediaGallery: NSObject, MediaGalleryDataSource, MediaTileViewControllerDelegate {
@objc
weak public var navigationController: MediaGalleryNavigationController!
var deletedAttachments: Set<TSAttachment> = Set()
var deletedGalleryItems: Set<MediaGalleryItem> = Set()
private var pageViewController: MediaPageViewController?
private var uiDatabaseConnection: YapDatabaseConnection {
return OWSPrimaryStorage.shared().uiDatabaseConnection
}
private let editingDatabaseConnection: YapDatabaseConnection
private let mediaGalleryFinder: OWSMediaGalleryFinder
private var initialDetailItem: MediaGalleryItem?
private let thread: TSThread
private let options: MediaGalleryOption
// we start with a small range size for quick loading.
private let fetchRangeSize: UInt = 10
deinit {
Logger.debug("")
}
@objc
init(thread: TSThread, options: MediaGalleryOption = []) {
self.thread = thread
self.editingDatabaseConnection = OWSPrimaryStorage.shared().newDatabaseConnection()
self.options = options
self.mediaGalleryFinder = OWSMediaGalleryFinder(thread: thread)
super.init()
NotificationCenter.default.addObserver(self,
selector: #selector(uiDatabaseDidUpdate),
name: .OWSUIDatabaseConnectionDidUpdate,
object: OWSPrimaryStorage.shared().dbNotificationObject)
}
// MARK: Present/Dismiss
private var currentItem: MediaGalleryItem {
return self.pageViewController!.currentItem
}
@objc
public func presentDetailView(fromViewController: UIViewController, mediaAttachment: TSAttachment) {
var galleryItem: MediaGalleryItem?
uiDatabaseConnection.read { transaction in
galleryItem = self.buildGalleryItem(attachment: mediaAttachment, transaction: transaction)
}
guard let initialDetailItem = galleryItem else {
return
}
presentDetailView(fromViewController: fromViewController, initialDetailItem: initialDetailItem)
}
public func presentDetailView(fromViewController: UIViewController, initialDetailItem: MediaGalleryItem) {
// For a speedy load, we only fetch a few items on either side of
// the initial message
ensureGalleryItemsLoaded(.around, item: initialDetailItem, amount: 10)
// We lazily load media into the gallery, but with large albums, we want to be sure
// we load all the media required to render the album's media rail.
ensureAlbumEntirelyLoaded(galleryItem: initialDetailItem)
self.initialDetailItem = initialDetailItem
let pageViewController = MediaPageViewController(initialItem: initialDetailItem, mediaGalleryDataSource: self, uiDatabaseConnection: self.uiDatabaseConnection, options: self.options)
self.addDataSourceDelegate(pageViewController)
self.pageViewController = pageViewController
let navController = MediaGalleryNavigationController()
self.navigationController = navController
navController.retainUntilDismissed = self
navigationController.setViewControllers([pageViewController], animated: false)
navigationController.modalPresentationStyle = .fullScreen
navigationController.modalTransitionStyle = .crossDissolve
fromViewController.present(navigationController, animated: true, completion: nil)
}
// If we're using a navigationController other than self to present the views
// e.g. the conversation settings view controller
var fromNavController: OWSNavigationController?
@objc
func pushTileView(fromNavController: OWSNavigationController) {
var mostRecentItem: MediaGalleryItem?
self.uiDatabaseConnection.read { transaction in
if let attachment = self.mediaGalleryFinder.mostRecentMediaAttachment(transaction: transaction) {
mostRecentItem = self.buildGalleryItem(attachment: attachment, transaction: transaction)
}
}
if let mostRecentItem = mostRecentItem {
mediaTileViewController.focusedItem = mostRecentItem
ensureGalleryItemsLoaded(.around, item: mostRecentItem, amount: 100)
}
self.fromNavController = fromNavController
fromNavController.pushViewController(mediaTileViewController, animated: true)
}
func showAllMedia(focusedItem: MediaGalleryItem) {
// TODO fancy animation - zoom media item into it's tile in the all media grid
ensureGalleryItemsLoaded(.around, item: focusedItem, amount: 100)
if let fromNavController = self.fromNavController {
// If from conversation settings view, we've already pushed
fromNavController.popViewController(animated: true)
} else {
// If from conversation view
mediaTileViewController.focusedItem = focusedItem
navigationController.pushViewController(mediaTileViewController, animated: true)
}
}
// MARK: MediaTileViewControllerDelegate
func mediaTileViewController(_ viewController: MediaTileViewController, didTapView tappedView: UIView, mediaGalleryItem: MediaGalleryItem) {
if self.fromNavController != nil {
// If we got to the gallery via conversation settings, present the detail view
// on top of the tile view
//
// == ViewController Schematic ==
//
// [DetailView] <--,
// [TileView] -----'
// [ConversationSettingsView]
// [ConversationView]
//
self.presentDetailView(fromViewController: mediaTileViewController, initialDetailItem: mediaGalleryItem)
} else {
// If we got to the gallery via the conversation view, pop the tile view
// to return to the detail view
//
// == ViewController Schematic ==
//
// [TileView] -----,
// [DetailView] <--'
// [ConversationView]
//
guard let pageViewController = self.pageViewController else {
owsFailDebug("pageViewController was unexpectedly nil")
self.navigationController.dismiss(animated: true)
return
}
pageViewController.setCurrentItem(mediaGalleryItem, direction: .forward, animated: false)
pageViewController.willBePresentedAgain()
// TODO fancy zoom animation
self.navigationController.popViewController(animated: true)
}
}
public func dismissMediaDetailViewController(_ mediaPageViewController: MediaPageViewController, animated isAnimated: Bool, completion completionParam: (() -> Void)?) {
guard let presentingViewController = self.navigationController.presentingViewController else {
owsFailDebug("presentingController was unexpectedly nil")
return
}
let completion = {
completionParam?()
UIApplication.shared.isStatusBarHidden = false
presentingViewController.setNeedsStatusBarAppearanceUpdate()
}
navigationController.view.isUserInteractionEnabled = false
presentingViewController.dismiss(animated: true, completion: completion)
}
// MARK: - Database Notifications
@objc
func uiDatabaseDidUpdate(notification: Notification) {
guard let notifications = notification.userInfo?[OWSUIDatabaseConnectionNotificationsKey] as? [Notification] else {
owsFailDebug("notifications was unexpectedly nil")
return
}
guard mediaGalleryFinder.hasMediaChanges(in: notifications, dbConnection: uiDatabaseConnection) else {
Logger.verbose("no changes for thread: \(thread)")
return
}
let rowChanges = extractRowChanges(notifications: notifications)
assert(rowChanges.count > 0)
process(rowChanges: rowChanges)
}
func extractRowChanges(notifications: [Notification]) -> [YapDatabaseViewRowChange] {
return notifications.flatMap { notification -> [YapDatabaseViewRowChange] in
guard let userInfo = notification.userInfo else {
owsFailDebug("userInfo was unexpectedly nil")
return []
}
guard let extensionChanges = userInfo["extensions"] as? [AnyHashable: Any] else {
owsFailDebug("extensionChanges was unexpectedly nil")
return []
}
guard let galleryData = extensionChanges[OWSMediaGalleryFinder.databaseExtensionName()] as? [AnyHashable: Any] else {
owsFailDebug("galleryData was unexpectedly nil")
return []
}
guard let galleryChanges = galleryData["changes"] as? [Any] else {
owsFailDebug("gallerlyChanges was unexpectedly nil")
return []
}
return galleryChanges.compactMap { $0 as? YapDatabaseViewRowChange }
}
}
func process(rowChanges: [YapDatabaseViewRowChange]) {
let deleteChanges = rowChanges.filter { $0.type == .delete }
let deletedItems: [MediaGalleryItem] = deleteChanges.compactMap { (deleteChange: YapDatabaseViewRowChange) -> MediaGalleryItem? in
guard let deletedItem = self.galleryItems.first(where: { galleryItem in
galleryItem.attachmentStream.uniqueId == deleteChange.collectionKey.key
}) else {
Logger.debug("deletedItem was never loaded - no need to remove.")
return nil
}
return deletedItem
}
self.delete(items: deletedItems, initiatedBy: self)
}
// MARK: - MediaGalleryDataSource
lazy var mediaTileViewController: MediaTileViewController = {
let vc = MediaTileViewController(mediaGalleryDataSource: self, uiDatabaseConnection: self.uiDatabaseConnection)
vc.delegate = self
self.addDataSourceDelegate(vc)
return vc
}()
var galleryItems: [MediaGalleryItem] = []
var sections: [GalleryDate: [MediaGalleryItem]] = [:]
var sectionDates: [GalleryDate] = []
var hasFetchedOldest = false
var hasFetchedMostRecent = false
func buildGalleryItem(attachment: TSAttachment, transaction: YapDatabaseReadTransaction) -> MediaGalleryItem? {
guard let attachmentStream = attachment as? TSAttachmentStream else {
return nil
}
guard let message = attachmentStream.fetchAlbumMessage(with: transaction) else {
return nil
}
let galleryItem = MediaGalleryItem(message: message, attachmentStream: attachmentStream)
galleryItem.album = getAlbum(item: galleryItem)
return galleryItem
}
func ensureAlbumEntirelyLoaded(galleryItem: MediaGalleryItem) {
ensureGalleryItemsLoaded(.before, item: galleryItem, amount: UInt(galleryItem.albumIndex))
let followingCount = galleryItem.message.attachmentIds.count - 1 - galleryItem.albumIndex
guard followingCount >= 0 else {
return
}
ensureGalleryItemsLoaded(.after, item: galleryItem, amount: UInt(followingCount))
}
var galleryAlbums: [String: MediaGalleryAlbum] = [:]
func getAlbum(item: MediaGalleryItem) -> MediaGalleryAlbum? {
guard let albumMessageId = item.attachmentStream.albumMessageId else {
return nil
}
guard let existingAlbum = galleryAlbums[albumMessageId] else {
let newAlbum = MediaGalleryAlbum(items: [item])
galleryAlbums[albumMessageId] = newAlbum
newAlbum.mediaGalleryDataSource = self
return newAlbum
}
existingAlbum.add(item: item)
return existingAlbum
}
// Range instead of indexSet since it's contiguous?
var fetchedIndexSet = IndexSet() {
didSet {
Logger.debug("\(oldValue) -> \(fetchedIndexSet)")
}
}
enum MediaGalleryError: Error {
case itemNoLongerExists
}
func ensureGalleryItemsLoaded(_ direction: GalleryDirection, item: MediaGalleryItem, amount: UInt, completion: ((IndexSet, [IndexPath]) -> Void)? = nil ) {
var galleryItems: [MediaGalleryItem] = self.galleryItems
var sections: [GalleryDate: [MediaGalleryItem]] = self.sections
var sectionDates: [GalleryDate] = self.sectionDates
var newGalleryItems: [MediaGalleryItem] = []
var newDates: [GalleryDate] = []
do {
try Bench(title: "fetching gallery items") {
try self.uiDatabaseConnection.read { transaction in
guard let index = self.mediaGalleryFinder.mediaIndex(attachment: item.attachmentStream, transaction: transaction) else {
throw MediaGalleryError.itemNoLongerExists
}
let initialIndex: Int = index.intValue
let mediaCount: Int = Int(self.mediaGalleryFinder.mediaCount(transaction: transaction))
let requestRange: Range<Int> = { () -> Range<Int> in
let range: Range<Int> = { () -> Range<Int> in
switch direction {
case .around:
// To keep it simple, this isn't exactly *amount* sized if `message` window overlaps the end or
// beginning of the view. Still, we have sufficient buffer to fetch more as the user swipes.
let start: Int = initialIndex - Int(amount) / 2
let end: Int = initialIndex + Int(amount) / 2 + 1
return start..<end
case .before:
let start: Int = initialIndex - Int(amount)
let end: Int = initialIndex
return start..<end
case .after:
let start: Int = initialIndex
let end: Int = initialIndex + Int(amount) + 1
return start..<end
}
}()
return range.clamped(to: 0..<mediaCount)
}()
let requestSet = IndexSet(integersIn: requestRange)
guard !self.fetchedIndexSet.contains(integersIn: requestSet) else {
Logger.debug("all requested messages have already been loaded.")
return
}
let unfetchedSet = requestSet.subtracting(self.fetchedIndexSet)
// For perf we only want to fetch a substantially full batch...
let isSubstantialRequest = unfetchedSet.count > (requestSet.count / 2)
// ...but we always fulfill even small requests if we're getting just the tail end of a gallery.
let isFetchingEdgeOfGallery = (self.fetchedIndexSet.count - unfetchedSet.count) < requestSet.count
guard isSubstantialRequest || isFetchingEdgeOfGallery else {
Logger.debug("ignoring small fetch request: \(unfetchedSet.count)")
return
}
Logger.debug("fetching set: \(unfetchedSet)")
let nsRange: NSRange = NSRange(location: unfetchedSet.min()!, length: unfetchedSet.count)
self.mediaGalleryFinder.enumerateMediaAttachments(range: nsRange, transaction: transaction) { (attachment: TSAttachment) in
guard !self.deletedAttachments.contains(attachment) else {
Logger.debug("skipping \(attachment) which has been deleted.")
return
}
guard let item: MediaGalleryItem = self.buildGalleryItem(attachment: attachment, transaction: transaction) else {
owsFailDebug("unexpectedly failed to buildGalleryItem")
return
}
let date = item.galleryDate
galleryItems.append(item)
if sections[date] != nil {
sections[date]!.append(item)
// so we can update collectionView
newGalleryItems.append(item)
} else {
sectionDates.append(date)
sections[date] = [item]
// so we can update collectionView
newDates.append(date)
newGalleryItems.append(item)
}
}
self.fetchedIndexSet = self.fetchedIndexSet.union(unfetchedSet)
self.hasFetchedOldest = self.fetchedIndexSet.min() == 0
self.hasFetchedMostRecent = self.fetchedIndexSet.max() == mediaCount - 1
}
}
} catch MediaGalleryError.itemNoLongerExists {
Logger.debug("Ignoring reload, since item no longer exists.")
return
} catch {
owsFailDebug("unexpected error: \(error)")
return
}
// TODO only sort if changed
var sortedSections: [GalleryDate: [MediaGalleryItem]] = [:]
Bench(title: "sorting gallery items") {
galleryItems.sort { lhs, rhs -> Bool in
return lhs.orderingKey < rhs.orderingKey
}
sectionDates.sort()
for (date, galleryItems) in sections {
sortedSections[date] = galleryItems.sorted { lhs, rhs -> Bool in
return lhs.orderingKey < rhs.orderingKey
}
}
}
self.galleryItems = galleryItems
self.sections = sortedSections
self.sectionDates = sectionDates
if let completionBlock = completion {
Bench(title: "calculating changes for collectionView") {
// FIXME can we avoid this index offset?
let dateIndices = newDates.map { sectionDates.firstIndex(of: $0)! + 1 }
let addedSections: IndexSet = IndexSet(dateIndices)
let addedItems: [IndexPath] = newGalleryItems.map { galleryItem in
let sectionIdx = sectionDates.firstIndex(of: galleryItem.galleryDate)!
let section = sections[galleryItem.galleryDate]!
let itemIdx = section.firstIndex(of: galleryItem)!
// FIXME can we avoid this index offset?
return IndexPath(item: itemIdx, section: sectionIdx + 1)
}
completionBlock(addedSections, addedItems)
}
}
}
var dataSourceDelegates: [Weak<MediaGalleryDataSourceDelegate>] = []
func addDataSourceDelegate(_ dataSourceDelegate: MediaGalleryDataSourceDelegate) {
dataSourceDelegates.append(Weak(value: dataSourceDelegate))
}
func delete(items: [MediaGalleryItem], initiatedBy: AnyObject) {
AssertIsOnMainThread()
Logger.info("with items: \(items.map { ($0.attachmentStream, $0.message.timestamp) })")
deletedGalleryItems.formUnion(items)
dataSourceDelegates.forEach { $0.value?.mediaGalleryDataSource(self, willDelete: items, initiatedBy: initiatedBy) }
for item in items {
self.deletedAttachments.insert(item.attachmentStream)
}
self.editingDatabaseConnection.asyncReadWrite { transaction in
for item in items {
let message = item.message
let attachment = item.attachmentStream
message.removeAttachment(attachment, transaction: transaction)
if message.attachmentIds.count == 0 {
Logger.debug("removing message after removing last media attachment")
message.remove(with: transaction)
}
}
}
var deletedSections: IndexSet = IndexSet()
var deletedIndexPaths: [IndexPath] = []
let originalSections = self.sections
let originalSectionDates = self.sectionDates
for item in items {
guard let itemIndex = galleryItems.firstIndex(of: item) else {
owsFailDebug("removing unknown item.")
return
}
self.galleryItems.remove(at: itemIndex)
guard let sectionIndex = sectionDates.firstIndex(where: { $0 == item.galleryDate }) else {
owsFailDebug("item with unknown date.")
return
}
guard var sectionItems = self.sections[item.galleryDate] else {
owsFailDebug("item with unknown section")
return
}
guard let sectionRowIndex = sectionItems.firstIndex(of: item) else {
owsFailDebug("item with unknown sectionRowIndex")
return
}
// We need to calculate the index of the deleted item with respect to it's original position.
guard let originalSectionIndex = originalSectionDates.firstIndex(where: { $0 == item.galleryDate }) else {
owsFailDebug("item with unknown date.")
return
}
guard let originalSectionItems = originalSections[item.galleryDate] else {
owsFailDebug("item with unknown section")
return
}
guard let originalSectionRowIndex = originalSectionItems.firstIndex(of: item) else {
owsFailDebug("item with unknown sectionRowIndex")
return
}
if sectionItems == [item] {
// Last item in section. Delete section.
self.sections[item.galleryDate] = nil
self.sectionDates.remove(at: sectionIndex)
deletedSections.insert(originalSectionIndex + 1)
deletedIndexPaths.append(IndexPath(row: originalSectionRowIndex, section: originalSectionIndex + 1))
} else {
sectionItems.remove(at: sectionRowIndex)
self.sections[item.galleryDate] = sectionItems
deletedIndexPaths.append(IndexPath(row: originalSectionRowIndex, section: originalSectionIndex + 1))
}
}
dataSourceDelegates.forEach { $0.value?.mediaGalleryDataSource(self, deletedSections: deletedSections, deletedItems: deletedIndexPaths) }
}
let kGallerySwipeLoadBatchSize: UInt = 5
internal func galleryItem(after currentItem: MediaGalleryItem) -> MediaGalleryItem? {
Logger.debug("")
self.ensureGalleryItemsLoaded(.after, item: currentItem, amount: kGallerySwipeLoadBatchSize)
guard let currentIndex = galleryItems.firstIndex(of: currentItem) else {
owsFailDebug("currentIndex was unexpectedly nil")
return nil
}
let index: Int = galleryItems.index(after: currentIndex)
guard let nextItem = galleryItems[safe: index] else {
// already at last item
return nil
}
guard !deletedGalleryItems.contains(nextItem) else {
Logger.debug("nextItem was deleted - Recursing.")
return galleryItem(after: nextItem)
}
return nextItem
}
internal func galleryItem(before currentItem: MediaGalleryItem) -> MediaGalleryItem? {
Logger.debug("")
self.ensureGalleryItemsLoaded(.before, item: currentItem, amount: kGallerySwipeLoadBatchSize)
guard let currentIndex = galleryItems.firstIndex(of: currentItem) else {
owsFailDebug("currentIndex was unexpectedly nil")
return nil
}
let index: Int = galleryItems.index(before: currentIndex)
guard let previousItem = galleryItems[safe: index] else {
// already at first item
return nil
}
guard !deletedGalleryItems.contains(previousItem) else {
Logger.debug("previousItem was deleted - Recursing.")
return galleryItem(before: previousItem)
}
return previousItem
}
var galleryItemCount: Int {
var count: UInt = 0
self.uiDatabaseConnection.read { (transaction: YapDatabaseReadTransaction) in
count = self.mediaGalleryFinder.mediaCount(transaction: transaction)
}
return Int(count) - deletedAttachments.count
}
}

View File

@ -5,35 +5,860 @@ import GRDB
import DifferenceKit
import SignalUtilitiesKit
public class MediaGalleryViewModel {
public class MediaGalleryViewModel: TransactionObserver {
public let threadId: String
public let threadVariant: SessionThread.Variant
private let item: ConversationViewModel.Item?
private var focusedAttachmentId: String?
public private(set) var focusedIndexPath: IndexPath?
/// This value is the current state of an album view
private var cachedInteractionIdBefore: Atomic<[Int64: Int64]> = Atomic([:])
private var cachedInteractionIdAfter: Atomic<[Int64: Int64]> = Atomic([:])
public var interactionIdBefore: [Int64: Int64] { cachedInteractionIdBefore.wrappedValue }
public var interactionIdAfter: [Int64: Int64] { cachedInteractionIdAfter.wrappedValue }
public private(set) var albumData: [Int64: [Item]] = [:]
/// This value is the current state of a gallery view
public private(set) var galleryData: [SectionModel] = []
// MARK: - Paging
public struct PageInfo {
public enum Target: Equatable {
case before
case around(id: String)
case after
}
let pageSize: Int
let pageOffset: Int
let currentCount: Int
let totalCount: Int
// MARK: - Initizliation
init(
pageSize: Int,
pageOffset: Int = 0,
currentCount: Int = 0,
totalCount: Int = 0
) {
self.pageSize = pageSize
self.pageOffset = pageOffset
self.currentCount = currentCount
self.totalCount = totalCount
}
}
private var isFetchingMoreItems: Atomic<Bool> = Atomic(false)
private var pageInfo: Atomic<PageInfo>
// Gallery observing
private let updatedRows: Atomic<Set<TrackedChange>> = Atomic([])
public var onGalleryChange: (([SectionModel], PageInfo) -> ())?
// MARK: - Initialization
init(
threadId: String,
threadVariant: SessionThread.Variant,
item: ConversationViewModel.Item? = nil
pageSize: Int = 1,
focusedAttachmentId: String? = nil
) {
self.threadId = threadId
self.threadVariant = threadVariant
self.item = item
}
self.pageInfo = Atomic(PageInfo(pageSize: pageSize))
self.focusedAttachmentId = focusedAttachmentId
}
public static func createTileViewController(threadId: String, isClosedGroup: Bool, isOpenGroup: Bool) -> MediaTileViewController {
return MediaTileViewController(
viewModel: MediaGalleryViewModel(
threadId: threadId,
threadVariant: {
if isClosedGroup { return .closedGroup }
if isOpenGroup { return .openGroup }
// MARK: - Data
public struct GalleryDate: Differentiable, Equatable, Comparable, Hashable {
private static let thisYearFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "MMMM"
return .contact
}()
return formatter
}()
private static let olderFormatter: DateFormatter = {
// FIXME: localize for RTL, or is there a built in way to do this?
let formatter = DateFormatter()
formatter.dateFormat = "MMMM yyyy"
return formatter
}()
let year: Int
let month: Int
private var date: Date? {
var components = DateComponents()
components.month = self.month
components.year = self.year
return Calendar.current.date(from: components)
}
var localizedString: String {
let isSameMonth: Bool = (self.month == Calendar.current.component(.month, from: Date()))
let isCurrentYear: Bool = (self.year == Calendar.current.component(.year, from: Date()))
let galleryDate: Date = (self.date ?? Date())
switch (isSameMonth, isCurrentYear) {
case (true, true): return "MEDIA_GALLERY_THIS_MONTH_HEADER".localized()
case (false, true): return GalleryDate.thisYearFormatter.string(from: galleryDate)
default: return GalleryDate.olderFormatter.string(from: galleryDate)
}
}
// MARK: - --Initialization
init(messageDate: Date) {
self.year = Calendar.current.component(.year, from: messageDate)
self.month = Calendar.current.component(.month, from: messageDate)
}
// MARK: - --Comparable
public static func < (lhs: GalleryDate, rhs: GalleryDate) -> Bool {
switch ((lhs.year != rhs.year), (lhs.month != rhs.month)) {
case (true, _): return lhs.year < rhs.year
case (_, true): return lhs.month < rhs.month
default: return false
}
}
}
public typealias SectionModel = ArraySection<Section, Item>
public enum Section: Differentiable, Equatable, Comparable, Hashable {
case emptyGallery
case loadNewer
case galleryMonth(date: GalleryDate)
case loadOlder
}
public struct Item: FetchableRecord, Decodable, Differentiable, Equatable, Hashable, Comparable {
fileprivate static let interactionIdKey: String = CodingKeys.interactionId.stringValue
fileprivate static let interactionVariantKey: String = CodingKeys.interactionVariant.stringValue
fileprivate static let interactionAuthorIdKey: String = CodingKeys.interactionAuthorId.stringValue
fileprivate static let interactionTimestampMsKey: String = CodingKeys.interactionTimestampMs.stringValue
fileprivate static let attachmentRowIdKey: String = CodingKeys.attachmentRowId.stringValue
fileprivate static let attachmentAlbumIndexKey: String = CodingKeys.attachmentAlbumIndex.stringValue
public var differenceIdentifier: String {
return attachment.id
}
let interactionId: Int64
let interactionVariant: Interaction.Variant
let interactionAuthorId: String
let interactionTimestampMs: Int64
let attachmentRowId: Int64
let attachmentAlbumIndex: Int
let attachment: Attachment
var galleryDate: GalleryDate {
GalleryDate(
messageDate: Date(timeIntervalSince1970: (Double(interactionTimestampMs) / 1000))
)
}
var isVideo: Bool { attachment.isVideo }
var isAnimated: Bool { attachment.isAnimated }
var isImage: Bool { attachment.isImage }
var imageSize: CGSize {
guard let width: UInt = attachment.width, let height: UInt = attachment.height else {
return .zero
}
return CGSize(width: Int(width), height: Int(height))
}
var captionForDisplay: String? { attachment.caption?.filterForDisplay }
// MARK: - Comparable
public static func < (lhs: Item, rhs: Item) -> Bool {
if lhs.interactionTimestampMs == rhs.interactionTimestampMs {
return (lhs.attachmentAlbumIndex < rhs.attachmentAlbumIndex)
}
return (lhs.interactionTimestampMs < rhs.interactionTimestampMs)
}
// MARK: - Query
private static let baseQueryFilterSQL: SQL = {
let attachment: TypedTableAlias<Attachment> = TypedTableAlias()
return SQL("\(attachment[.isVisualMedia]) = true AND \(attachment[.isValid]) = true")
}()
private static let galleryQueryOrderSQL: SQL = {
let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
let interactionAttachment: TypedTableAlias<InteractionAttachment> = TypedTableAlias()
/// **Note:** This **MUST** match the desired sort behaviour for the screen otherwise paging will be
/// very broken
return SQL("\(interaction[.timestampMs].desc), \(interactionAttachment[.albumIndex])")
}()
/// Retrieve the index that the attachment with the given `attachmentId` will have in the gallery
fileprivate static func galleryIndex(for attachmentId: String) -> SQLRequest<Int> {
let attachment: TypedTableAlias<Attachment> = TypedTableAlias()
let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
let interactionAttachment: TypedTableAlias<InteractionAttachment> = TypedTableAlias()
return """
SELECT
(gallery.galleryIndex - 1) AS galleryIndex -- Converting from 1-Indexed to 0-indexed
FROM (
SELECT
\(attachment[.id]) AS id,
ROW_NUMBER() OVER (ORDER BY \(galleryQueryOrderSQL)) AS galleryIndex
FROM \(Attachment.self)
JOIN \(InteractionAttachment.self) ON \(interactionAttachment[.attachmentId]) = \(attachment[.id])
JOIN \(Interaction.self) ON \(interaction[.id]) = \(interactionAttachment[.interactionId])
WHERE \(baseQueryFilterSQL)
) AS gallery
WHERE \(SQL("gallery.id = \(attachmentId)"))
"""
}
/// Retrieve the indexes the given attachment row will have in the gallery
fileprivate static func galleryIndexes(for rowIds: Set<Int64>, threadId: String) -> SQLRequest<Int> {
let attachment: TypedTableAlias<Attachment> = TypedTableAlias()
let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
let interactionAttachment: TypedTableAlias<InteractionAttachment> = TypedTableAlias()
return """
SELECT
(gallery.galleryIndex - 1) AS galleryIndex -- Converting from 1-Indexed to 0-indexed
FROM (
SELECT
\(attachment.alias[Column.rowID]) AS rowid,
ROW_NUMBER() OVER (ORDER BY \(galleryQueryOrderSQL)) AS galleryIndex
FROM \(Attachment.self)
JOIN \(InteractionAttachment.self) ON \(interactionAttachment[.attachmentId]) = \(attachment[.id])
JOIN \(Interaction.self) ON (
\(interaction[.id]) = \(interactionAttachment[.interactionId]) AND
\(SQL("\(interaction[.threadId]) = \(threadId)"))
)
WHERE \(baseQueryFilterSQL)
) AS gallery
WHERE \(SQL("gallery.rowid IN \(rowIds)"))
"""
}
private static let baseQuery: QueryInterfaceRequest<Item> = {
let attachment: TypedTableAlias<Attachment> = TypedTableAlias()
let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
let interactionAttachment: TypedTableAlias<InteractionAttachment> = TypedTableAlias()
return Attachment
.select(
interaction[.id].forKey(Item.interactionIdKey),
interaction[.variant].forKey(Item.interactionVariantKey),
interaction[.authorId].forKey(Item.interactionAuthorIdKey),
interaction[.timestampMs].forKey(Item.interactionTimestampMsKey),
attachment.alias[Column.rowID].forKey(Item.attachmentRowIdKey),
interactionAttachment[.albumIndex].forKey(Item.attachmentAlbumIndexKey),
attachment.allColumns()
)
.aliased(attachment)
.filter(literal: baseQueryFilterSQL)
.joining(
required: Attachment.interactionAttachments
.aliased(interactionAttachment)
.joining(
required: InteractionAttachment.interaction
.aliased(interaction)
)
)
.asRequest(of: Item.self)
}()
fileprivate static let albumQuery: QueryInterfaceRequest<Item> = {
let interactionAttachment: TypedTableAlias<InteractionAttachment> = TypedTableAlias()
return Item.baseQuery.order(interactionAttachment[.albumIndex])
}()
fileprivate static let galleryQuery: QueryInterfaceRequest<Item> = {
return Item.baseQuery
.order(literal: galleryQueryOrderSQL)
}()
fileprivate static let galleryQueryReversed: QueryInterfaceRequest<Item> = {
let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
let interactionAttachment: TypedTableAlias<InteractionAttachment> = TypedTableAlias()
/// **Note:** This **MUST** always result in the same data as `galleryQuery` but in the opposite order
return Item.baseQuery
.order(interaction[.timestampMs], interactionAttachment[.albumIndex].desc)
}()
func thumbnailImage(async: @escaping (UIImage) -> ()) {
attachment.thumbnail(size: .small, success: { image, _ in async(image) }, failure: {})
}
}
// MARK: - Album
/// This is all the data the screen needs to populate itself, please see the following link for tips to help optimise
/// performance https://github.com/groue/GRDB.swift#valueobservation-performance
///
/// **Note:** The 'trackingConstantRegion' is optimised in such a way that the request needs to be static
/// otherwise there may be situations where it doesn't get updates, this means we can't have conditional queries
public typealias AlbumObservation = ValueObservation<ValueReducers.RemoveDuplicates<ValueReducers.Fetch<[Item]>>>
public lazy var observableAlbumData: AlbumObservation = buildAlbumObservation(for: nil)
private func buildAlbumObservation(for interactionId: Int64?) -> AlbumObservation {
return ValueObservation
.trackingConstantRegion { db -> [Item] in
guard let interactionId: Int64 = interactionId else { return [] }
let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
return try Item.albumQuery
.filter(interaction[.id] == interactionId)
.fetchAll(db)
}
.removeDuplicates()
}
// MARK: - Gallery
/// This function is used to load a gallery page using the provided `limitInfo`, if a `focusedAttachmentId` is provided then
/// the `limitInfo.offset` value will be ignored and it will retrieve `limitInfo.limit` values positioning the focussed item
/// as closed to the middle as possible prioritising retrieving `limitInfo.limit` items total
///
/// **Note:** The `focusedAttachmentId` should only be provided during the first call, subsequent calls should solely provide
/// the `limitInfo` so content can be added before and after the initial page
private func loadGalleryPage(
_ target: PageInfo.Target,
currentPageInfo: PageInfo
) -> (items: [Item], updatedPageInfo: PageInfo) {
return GRDBStorage.shared
.read { db in
let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
let totalCount: Int = try Item.galleryQuery
.filter(interaction[.threadId] == threadId)
.fetchCount(db)
let queryOffset: Int = {
switch target {
case .before:
return max(0, (currentPageInfo.pageOffset - currentPageInfo.pageSize))
case .around(let targetId):
// If we want to focus on a specific item then we need to find it's index in
// the queried data
guard let targetIndex: Int = try? Int.fetchOne(db, Item.galleryIndex(for: targetId)) else {
// If we couldn't find the targetId then just load the page after the current one
return (currentPageInfo.pageOffset + currentPageInfo.pageSize)
}
// If the focused item is within the first half of the page then we still want
// to retrieve a full page so calculate the offset needed to do so
let halfPageSize: Int = Int(floor(Double(currentPageInfo.pageSize) / 2))
// If the focused item is within the first or last half page then just
// start from the start/end of the content
guard targetIndex > halfPageSize else { return 0 }
guard targetIndex < (totalCount - halfPageSize) else {
return (totalCount - currentPageInfo.pageSize)
}
return (targetIndex - halfPageSize)
case .after:
return (currentPageInfo.pageOffset + currentPageInfo.currentCount)
}
}()
let items: [Item] = try Item.galleryQuery
.filter(interaction[.threadId] == threadId)
.limit(currentPageInfo.pageSize, offset: queryOffset)
.fetchAll(db)
let updatedLimitInfo: PageInfo = PageInfo(
pageSize: currentPageInfo.pageSize,
pageOffset: (target != .after ?
queryOffset :
currentPageInfo.pageOffset
),
currentCount: (currentPageInfo.currentCount + items.count),
totalCount: totalCount
)
return (items, updatedLimitInfo)
}
.defaulting(to: ([], currentPageInfo))
}
private func addingSystemSections(to data: [SectionModel], for pageInfo: PageInfo) -> [SectionModel] {
// Remove and re-add the custom sections as needed
return [
(data.isEmpty ? [SectionModel(section: .emptyGallery)] : []),
(!data.isEmpty && pageInfo.pageOffset > 0 ? [SectionModel(section: .loadNewer)] : []),
data.filter { section -> Bool in
switch section.model {
case .galleryMonth: return true
case .emptyGallery, .loadOlder, .loadNewer: return false
}
},
(!data.isEmpty && (pageInfo.pageOffset + pageInfo.currentCount) < pageInfo.totalCount ?
[SectionModel(section: .loadOlder)] :
[]
)
]
.flatMap { $0 }
}
private func updatedGalleryData(
with existingData: [SectionModel],
dataToUpsert: [Item],
pageInfoToUpdate: PageInfo
) -> (sections: [SectionModel], pageInfo: PageInfo) {
guard !dataToUpsert.isEmpty else { return (existingData, pageInfoToUpdate) }
let updatedGalleryData: (sections: [SectionModel], pageInfo: PageInfo) = updatedGalleryData(
with: self.galleryData,
dataToUpsert: (dataToUpsert, pageInfoToUpdate)
)
let existingDataCount: Int = existingData
.map { $0.elements.count }
.reduce(0, +)
let updatedGalleryDataCount: Int = updatedGalleryData.sections
.map { $0.elements.count }
.reduce(0, +)
let gallerySizeDiff: Int = (updatedGalleryDataCount - existingDataCount)
let updatedPageInfo: PageInfo = PageInfo(
pageSize: pageInfoToUpdate.pageSize,
pageOffset: pageInfoToUpdate.pageOffset,
currentCount: (pageInfoToUpdate.currentCount + gallerySizeDiff),
totalCount: (pageInfoToUpdate.totalCount + gallerySizeDiff)
)
// Add the "system" sections, sort the sections and return the result
return (
self.addingSystemSections(to: updatedGalleryData.sections, for: updatedPageInfo)
.sorted { lhs, rhs -> Bool in (lhs.model > rhs.model) },
updatedPageInfo
)
}
private func updatedGalleryData(
with existingData: [SectionModel],
dataToUpsert: (items: [Item], updatedPageInfo: PageInfo)
) -> (sections: [SectionModel], pageInfo: PageInfo) {
var updatedGalleryData: [SectionModel] = existingData
dataToUpsert
.items
.grouped(by: \.galleryDate)
.forEach { key, items in
guard let existingIndex = galleryData.firstIndex(where: { $0.model == .galleryMonth(date: key) }) else {
// Insert a new section
updatedGalleryData.append(
ArraySection(
model: .galleryMonth(date: key),
elements: items
.sorted()
.reversed()
)
)
return
}
// Filter out collisions, replacing them with the updated values and insert
// and new values
let itemRowIds: Set<Int64> = items.map { $0.attachmentRowId }.asSet()
updatedGalleryData[existingIndex] = ArraySection(
model: .galleryMonth(date: key),
elements: updatedGalleryData[existingIndex].elements
.filter { !itemRowIds.contains($0.attachmentRowId) }
.appending(contentsOf: items)
.sorted()
.reversed()
)
}
// Add the "system" sections, sort the sections and return the result
return (
self.addingSystemSections(to: updatedGalleryData, for: dataToUpsert.updatedPageInfo)
.sorted { lhs, rhs -> Bool in (lhs.model > rhs.model) },
dataToUpsert.updatedPageInfo
)
}
// MARK: - TransactionObserver
private struct TrackedChange: Equatable, Hashable {
let kind: DatabaseEvent.Kind
let rowId: Int64
}
public func observes(eventsOfKind eventKind: DatabaseEventKind) -> Bool {
switch eventKind {
case .delete(let tableName): return (tableName == Attachment.databaseTableName)
case .update(let tableName, let columnNames):
/// **Warning:** This filtering allows us to ignore all changes to attachments except
/// for the 'isValid' column, unfortunately calling the `with()` function on an attachment
/// does result in this column being seen as updated (even if the value doesn't change) so
/// we need to be careful where we set it to avoid unnecessarily triggering updates
return (
tableName == Attachment.databaseTableName &&
columnNames.contains(Attachment.Columns.isValid.name)
)
// We can ignore 'insert' events as we only care about valid attachments
case .insert: return false
}
}
public func databaseDidChange(with event: DatabaseEvent) {
// This will get called for whenever an Attachment's 'isValid' column is
// updated (ie. an attachment finished uploading/downloading), unfortunately
// we won't know if the attachment is actually relevant yet as it could be for
// another thread or it might not be a media attachment
let trackedChange: TrackedChange = TrackedChange(
kind: event.kind,
rowId: event.rowID
)
updatedRows.mutate { $0.insert(trackedChange) }
}
// Note: We will process all updates which come through this method even if
// 'onGalleryChange' is null because if the UI stops observing and then starts again
// later we don't want them to have missed out on changes which happened while they
// weren't subscribed (and doing a full re-query seems painful...)
public func databaseDidCommit(_ db: Database) {
var committedUpdatedRows: Set<TrackedChange> = []
self.updatedRows.mutate { updatedRows in
committedUpdatedRows = updatedRows
updatedRows.removeAll()
}
// Note: This method will be called regardless of whether there were actually changes
// in the areas we are observing so we want to early-out if there aren't any relevant
// updated rows
guard !committedUpdatedRows.isEmpty else { return }
var updatedPageInfo: PageInfo = self.pageInfo.wrappedValue
let attachmentRowIdsToQuery: Set<Int64> = committedUpdatedRows
.filter { $0.kind != .delete }
.map { $0.rowId }
.asSet()
let attachmentRowIdsToDelete: Set<Int64> = committedUpdatedRows
.filter { $0.kind == .delete }
.map { $0.rowId }
.asSet()
let oldGalleryDataCount: Int = self.galleryData
.map { $0.elements.count }
.reduce(0, +)
var galleryDataWithDeletions: [SectionModel] = self.galleryData
// First remove any items which have been deleted
if !attachmentRowIdsToDelete.isEmpty {
galleryDataWithDeletions = galleryDataWithDeletions
.map { section -> SectionModel in
ArraySection(
model: section.model,
elements: section.elements
.filter { item -> Bool in !attachmentRowIdsToDelete.contains(item.attachmentRowId) }
)
}
.filter { section -> Bool in !section.elements.isEmpty }
let updatedGalleryDataCount: Int = galleryDataWithDeletions
.map { $0.elements.count }
.reduce(0, +)
// Make sure there were actually changes
if updatedGalleryDataCount != oldGalleryDataCount {
let gallerySizeDiff: Int = (updatedGalleryDataCount - oldGalleryDataCount)
updatedPageInfo = PageInfo(
pageSize: updatedPageInfo.pageSize,
pageOffset: updatedPageInfo.pageOffset,
currentCount: (updatedPageInfo.currentCount + gallerySizeDiff),
totalCount: (updatedPageInfo.totalCount + gallerySizeDiff)
)
}
}
/// Store the 'deletions-only' update logic in a block as there are a number of places we will fallback to this logic
let sendDeletionsOnlyUpdateIfNeeded: () -> () = {
guard !attachmentRowIdsToDelete.isEmpty else { return }
DispatchQueue.main.async { [weak self] in
self?.onGalleryChange?(galleryDataWithDeletions, updatedPageInfo)
}
}
// If there are no inserted/updated rows then trigger the update callback and stop here
guard !attachmentRowIdsToQuery.isEmpty else {
sendDeletionsOnlyUpdateIfNeeded()
return
}
// Fetch the indexes of the rowIds so we can determine whether they should be added to the screen
let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
let itemIndexes: [Int] = (try? Item.galleryIndexes(for: attachmentRowIdsToQuery, threadId: self.threadId)
.fetchAll(db))
.defaulting(to: [])
// Determine if the indexes for the row ids should be displayed on the screen and remove any
// which shouldn't - values less than 'currentCount' or if there is at least one value less than
// 'currentCount' and the indexes are sequential (ie. more than the current loaded content was
// added at once)
let itemsAreSequential: Bool = (itemIndexes.map { $0 - 1 }.dropFirst() == itemIndexes.dropLast())
let validAttachmentRowIds: Set<Int64> = (itemsAreSequential && itemIndexes.contains(where: { $0 < updatedPageInfo.currentCount }) ?
attachmentRowIdsToQuery :
zip(itemIndexes, attachmentRowIdsToQuery)
.filter { index, _ -> Bool in index < updatedPageInfo.currentCount }
.map { _, rowId -> Int64 in rowId }
.asSet()
)
// If there are no valid attachment row ids then stop here
guard !validAttachmentRowIds.isEmpty else {
sendDeletionsOnlyUpdateIfNeeded()
return
}
// Fetch the inserted/updated rows
let updatedItems: [Item] = (try? Item.galleryQuery
.filter(validAttachmentRowIds.contains(Column.rowID))
.filter(interaction[.threadId] == self.threadId)
.fetchAll(db))
.defaulting(to: [])
// If the inserted/updated rows we irrelevant (eg. associated to another thread, a quote or a link
// preview) then trigger the update callback (if there were deletions) and stop here
guard !updatedItems.isEmpty else {
sendDeletionsOnlyUpdateIfNeeded()
return
}
// Process the upserted data
let updatedGalleryData: (sections: [SectionModel], pageInfo: PageInfo) = updatedGalleryData(
with: galleryDataWithDeletions,
dataToUpsert: updatedItems,
pageInfoToUpdate: updatedPageInfo
)
DispatchQueue.main.async { [weak self] in
self?.onGalleryChange?(updatedGalleryData.sections, updatedGalleryData.pageInfo)
}
}
public func databaseDidRollback(_ db: Database) {}
// MARK: - Functions
@discardableResult public func loadAndCacheAlbumData(for interactionId: Int64) -> [Item] {
typealias AlbumInfo = (albumData: [Item], interactionIdBefore: Int64?, interactionIdAfter: Int64?)
// Note: It's possible we already have cached album data for this interaction
// but to avoid displaying stale data we re-fetch from the database anyway
let maybeAlbumInfo: AlbumInfo? = GRDBStorage.shared
.read { db -> AlbumInfo in
let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
let newAlbumData: [Item] = try Item.albumQuery
.filter(interaction[.id] == interactionId)
.fetchAll(db)
guard let albumTimestampMs: Int64 = newAlbumData.first?.interactionTimestampMs else {
return (newAlbumData, nil, nil)
}
let itemBefore: Item? = try Item.galleryQueryReversed
.filter(interaction[.timestampMs] > albumTimestampMs)
.fetchOne(db)
let itemAfter: Item? = try Item.galleryQuery
.filter(interaction[.timestampMs] < albumTimestampMs)
.fetchOne(db)
return (newAlbumData, itemBefore?.interactionId, itemAfter?.interactionId)
}
guard let newAlbumInfo: AlbumInfo = maybeAlbumInfo else { return [] }
// Cache the album info for the new interactionId
self.updateAlbumData(newAlbumInfo.albumData, for: interactionId)
self.cachedInteractionIdBefore.mutate { $0[interactionId] = newAlbumInfo.interactionIdBefore }
self.cachedInteractionIdAfter.mutate { $0[interactionId] = newAlbumInfo.interactionIdAfter }
return newAlbumInfo.albumData
}
public func replaceAlbumObservation(toObservationFor interactionId: Int64) {
self.observableAlbumData = self.buildAlbumObservation(for: interactionId)
}
public func updateAlbumData(_ updatedData: [Item], for interactionId: Int64) {
self.albumData[interactionId] = updatedData
}
public func updateGalleryData(_ updatedData: [SectionModel], pageInfo: PageInfo) {
self.galleryData = updatedData
self.pageInfo.mutate { $0 = pageInfo }
// If we have a focused attachment id then we need to make sure the 'focusedIndexPath'
// is updated to be accurate
if let focusedAttachmentId: String = focusedAttachmentId {
self.focusedIndexPath = nil
for (section, sectionData) in updatedData.enumerated() {
for (index, item) in sectionData.elements.enumerated() {
if item.attachment.id == focusedAttachmentId {
self.focusedIndexPath = IndexPath(item: index, section: section)
break
}
}
if self.focusedIndexPath != nil { break }
}
}
}
public func loadNewerGalleryItems() {
// Only allow on 'load older' fetch at a time
guard !isFetchingMoreItems.wrappedValue else { return }
// Prevent more fetching until we have completed adding the page
isFetchingMoreItems.mutate { $0 = true }
// Load the page before the current data (newer items) then merge and sort
// with the current data
let updatedGalleryData: (sections: [SectionModel], pageInfo: PageInfo) = updatedGalleryData(
with: galleryData,
dataToUpsert: loadGalleryPage(
.before,
currentPageInfo: pageInfo.wrappedValue
)
)
DispatchQueue.main.async { [weak self] in
self?.onGalleryChange?(updatedGalleryData.sections, updatedGalleryData.pageInfo)
self?.isFetchingMoreItems.mutate { $0 = false }
}
}
public func loadOlderGalleryItems() {
// Only allow on 'load older' fetch at a time
guard !isFetchingMoreItems.wrappedValue else { return }
// Prevent more fetching until we have completed adding the page
isFetchingMoreItems.mutate { $0 = true }
// Load the page after the current data (older items) then merge and sort
// with the current data
let updatedGalleryData: (sections: [SectionModel], pageInfo: PageInfo) = updatedGalleryData(
with: galleryData,
dataToUpsert: loadGalleryPage(
.after,
currentPageInfo: pageInfo.wrappedValue
)
)
DispatchQueue.main.async { [weak self] in
self?.onGalleryChange?(updatedGalleryData.sections, updatedGalleryData.pageInfo)
self?.isFetchingMoreItems.mutate { $0 = false }
}
}
public func updateFocusedItem(attachmentId: String, indexPath: IndexPath) {
// Note: We need to set both of these as the 'focusedIndexPath' is usually
// derived and if the data changes it will be regenerated using the
// 'focusedAttachmentId' value
self.focusedAttachmentId = attachmentId
self.focusedIndexPath = indexPath
}
// MARK: - Creation Functions
public static func createDetailViewController(
for threadId: String,
threadVariant: SessionThread.Variant,
interactionId: Int64,
selectedAttachmentId: String,
options: [MediaGalleryOption]
) -> UIViewController? {
// Load the data for the album immediately (needed before pushing to the screen so
// transitions work nicely)
let viewModel: MediaGalleryViewModel = MediaGalleryViewModel(
threadId: threadId,
threadVariant: threadVariant
)
viewModel.loadAndCacheAlbumData(for: interactionId)
viewModel.replaceAlbumObservation(toObservationFor: interactionId)
guard
!viewModel.albumData.isEmpty,
let initialItem: Item = viewModel.albumData[interactionId]?.first(where: { item -> Bool in
item.attachment.id == selectedAttachmentId
})
else { return nil }
let pageViewController: MediaPageViewController = MediaPageViewController(
viewModel: viewModel,
initialItem: initialItem,
options: options
)
let navController: MediaGalleryNavigationController = MediaGalleryNavigationController()
navController.viewControllers = [pageViewController]
navController.modalPresentationStyle = .fullScreen
navController.transitioningDelegate = pageViewController
return navController
}
public static func createTileViewController(
threadId: String,
threadVariant: SessionThread.Variant,
focusedAttachmentId: String?
) -> MediaTileViewController {
let viewModel: MediaGalleryViewModel = MediaGalleryViewModel(
threadId: threadId,
threadVariant: threadVariant,
pageSize: MediaTileViewController.itemPageSize,
focusedAttachmentId: focusedAttachmentId
)
// Load the data for the album immediately (needed before pushing to the screen so
// transitions work nicely)
let pageTarget: PageInfo.Target = {
// If we don't have a `focusedAttachmentId` then default to `.before` (it'll query
// from a `0` offset
guard let targetId: String = focusedAttachmentId else { return .before }
return .around(id: targetId)
}()
let initialGalleryData: (sections: [SectionModel], pageInfo: PageInfo) = viewModel.updatedGalleryData(
with: [],
dataToUpsert: viewModel.loadGalleryPage(
pageTarget,
currentPageInfo: PageInfo(pageSize: MediaTileViewController.itemPageSize)
)
)
viewModel.updateGalleryData(
initialGalleryData.sections,
pageInfo: initialGalleryData.pageInfo
)
return MediaTileViewController(
viewModel: viewModel
)
}
}
@ -49,8 +874,13 @@ public class SNMediaGallery: NSObject {
fromNavController.pushViewController(
MediaGalleryViewModel.createTileViewController(
threadId: threadId,
isClosedGroup: isClosedGroup,
isOpenGroup: isOpenGroup
threadVariant: {
if isClosedGroup { return .closedGroup }
if isOpenGroup { return .openGroup }
return .contact
}(),
focusedAttachmentId: nil
),
animated: true
)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -10,7 +10,21 @@ class SendMediaNavigationController: OWSNavigationController {
// This is a sensitive constant, if you change it make sure to check
// on iPhone5, 6, 6+, X, layouts.
static let bottomButtonsCenterOffset: CGFloat = -50
private let threadId: String
// MARK: - Initialization
init(threadId: String) {
self.threadId = threadId
super.init()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - Overrides
override var prefersStatusBarHidden: Bool { return true }
@ -48,17 +62,17 @@ class SendMediaNavigationController: OWSNavigationController {
public weak var sendMediaNavDelegate: SendMediaNavDelegate?
@objc
public class func showingCameraFirst() -> SendMediaNavigationController {
let navController = SendMediaNavigationController()
navController.setViewControllers([navController.captureViewController], animated: false)
public class func showingCameraFirst(threadId: String) -> SendMediaNavigationController {
let navController = SendMediaNavigationController(threadId: threadId)
navController.viewControllers = [navController.captureViewController]
return navController
}
@objc
public class func showingMediaLibraryFirst() -> SendMediaNavigationController {
let navController = SendMediaNavigationController()
navController.setViewControllers([navController.mediaLibraryViewController], animated: false)
public class func showingMediaLibraryFirst(threadId: String) -> SendMediaNavigationController {
let navController = SendMediaNavigationController(threadId: threadId)
navController.viewControllers = [navController.mediaLibraryViewController]
return navController
}
@ -218,7 +232,11 @@ class SendMediaNavigationController: OWSNavigationController {
return
}
let approvalViewController = AttachmentApprovalViewController(mode: .sharedNavigation, attachments: self.attachments)
let approvalViewController = AttachmentApprovalViewController(
mode: .sharedNavigation,
threadId: self.threadId,
attachments: self.attachments
)
approvalViewController.approvalDelegate = self
approvalViewController.messageText = sendMediaNavDelegate.sendMediaNavInitialMessageText(self)
@ -429,8 +447,8 @@ extension SendMediaNavigationController: AttachmentApprovalViewControllerDelegat
attachmentDraftCollection.remove(attachment: attachment)
}
func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didApproveAttachments attachments: [SignalAttachment], messageText: String?) {
sendMediaNavDelegate?.sendMediaNav(self, didApproveAttachments: attachments, messageText: messageText)
func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didApproveAttachments attachments: [SignalAttachment], forThreadId threadId: String, messageText: String?) {
sendMediaNavDelegate?.sendMediaNav(self, didApproveAttachments: attachments, forThreadId: threadId, messageText: messageText)
}
func attachmentApprovalDidCancel(_ attachmentApproval: AttachmentApprovalViewController) {
@ -673,7 +691,7 @@ private class DoneButton: UIView {
protocol SendMediaNavDelegate: AnyObject {
func sendMediaNavDidCancel(_ sendMediaNavigationController: SendMediaNavigationController)
func sendMediaNav(_ sendMediaNavigationController: SendMediaNavigationController, didApproveAttachments attachments: [SignalAttachment], messageText: String?)
func sendMediaNav(_ sendMediaNavigationController: SendMediaNavigationController, didApproveAttachments attachments: [SignalAttachment], forThreadId threadId: String, messageText: String?)
func sendMediaNavInitialMessageText(_ sendMediaNavigationController: SendMediaNavigationController) -> String?
func sendMediaNav(_ sendMediaNavigationController: SendMediaNavigationController, didChangeMessageText newMessageText: String?)

View File

@ -63,6 +63,7 @@ extension MediaDismissAnimationController: UIViewControllerAnimatedTransitioning
switch toVC {
case let contextProvider as MediaPresentationContextProvider:
toVC.view.layoutIfNeeded()
toContextProvider = contextProvider
case let navController as UINavigationController:
@ -71,6 +72,7 @@ extension MediaDismissAnimationController: UIViewControllerAnimatedTransitioning
return
}
toVC.view.layoutIfNeeded()
toContextProvider = contextProvider
default:
@ -104,8 +106,8 @@ extension MediaDismissAnimationController: UIViewControllerAnimatedTransitioning
let toMediaContext: MediaPresentationContext? = toContextProvider.mediaPresentationContext(mediaItem: mediaItem, in: containerView)
let duration: CGFloat = transitionDuration(using: transitionContext)
fromMediaContext.mediaView.alpha = 0.0
toMediaContext?.mediaView.alpha = 0.0
fromMediaContext.mediaView.alpha = 0
toMediaContext?.mediaView.alpha = 0
let transitionView = UIImageView(image: presentationImage)
transitionView.frame = fromMediaContext.presentationFrame
@ -197,12 +199,20 @@ extension MediaDismissAnimationController: UIViewControllerAnimatedTransitioning
self?.toTransitionalOverlayView?.removeFromSuperview()
if transitionContext.transitionWasCancelled {
// the "to" view will be nil if we're doing a modal dismiss, in which case
// The "to" view will be nil if we're doing a modal dismiss, in which case
// we wouldn't want to remove the toView.
transitionContext.view(forKey: .to)?.removeFromSuperview()
// Note: We shouldn't need to do this but for some reason it's not
// automatically getting re-enabled so we manually enable it
transitionContext.view(forKey: .from)?.isUserInteractionEnabled = true
}
else {
transitionContext.view(forKey: .from)?.removeFromSuperview()
// Note: We shouldn't need to do this but for some reason it's not
// automatically getting re-enabled so we manually enable it
transitionContext.view(forKey: .to)?.isUserInteractionEnabled = true
}
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)

View File

@ -42,6 +42,8 @@ class MediaInteractiveDismiss: UIPercentDrivenInteractiveTransition {
private var fastEnoughToCompleteTransition = false
private var farEnoughToCompleteTransition = false
private var lastProgress: CGFloat = 0
private var lastIncreasedProgress: CGFloat = 0
private var shouldCompleteTransition: Bool {
if farEnoughToCompleteTransition { return true }
@ -73,9 +75,21 @@ class MediaInteractiveDismiss: UIPercentDrivenInteractiveTransition {
let offset = gestureRecognizer.translation(in: coordinateSpace)
let progress = abs(offset.y) / totalDistance
// `farEnoughToCompleteTransition` is cancelable if the user reverses direction
farEnoughToCompleteTransition = progress >= 0.5
farEnoughToCompleteTransition = (progress >= 0.5)
// If the user has reverted enough progress then we want to reset the velocity
// flag (don't want the user to start quickly, slowly drag it back end end up
// dismissing the screen)
if (lastIncreasedProgress - progress) > 0.2 || progress < 0.05 {
fastEnoughToCompleteTransition = false
}
update(progress)
lastIncreasedProgress = (progress > lastProgress ? progress : lastIncreasedProgress)
lastProgress = progress
interactiveDismissDelegate?.interactiveDismissUpdate(self, didChangeTouchOffset: offset)
@ -86,6 +100,8 @@ class MediaInteractiveDismiss: UIPercentDrivenInteractiveTransition {
interactionInProgress = false
farEnoughToCompleteTransition = false
fastEnoughToCompleteTransition = false
lastIncreasedProgress = 0
lastProgress = 0
case .ended:
if shouldCompleteTransition {
@ -100,6 +116,8 @@ class MediaInteractiveDismiss: UIPercentDrivenInteractiveTransition {
interactionInProgress = false
farEnoughToCompleteTransition = false
fastEnoughToCompleteTransition = false
lastIncreasedProgress = 0
lastProgress = 0
default:
break

View File

@ -9,6 +9,9 @@ enum Media {
var image: UIImage? {
switch self {
case let .gallery(item):
// For videos attempt to load a large thumbnail, for other items just try to load
// the source file directly
guard !item.isVideo else { return item.attachment.existingThumbnail(size: .large) }
guard let originalFilePath: String = item.attachment.originalFilePath else { return nil }
return UIImage(contentsOfFile: originalFilePath)

View File

@ -4,13 +4,11 @@ import UIKit
class MediaZoomAnimationController: NSObject {
private let mediaItem: Media
private let shouldBounce: Bool
init(image: UIImage) {
mediaItem = .image(image)
}
init(galleryItem: MediaGalleryViewModel.Item) {
mediaItem = .gallery(galleryItem)
init(galleryItem: MediaGalleryViewModel.Item, shouldBounce: Bool = true) {
self.mediaItem = .gallery(galleryItem)
self.shouldBounce = shouldBounce
}
}
@ -75,50 +73,70 @@ extension MediaZoomAnimationController: UIViewControllerAnimatedTransitioning {
// as the 'toContextProvider.mediaPresentationContext' is dependant on it having the correct
// positioning (and the navBar sizing isn't correct until after layout)
let toView: UIView = (transitionContext.view(forKey: .to) ?? toVC.view)
let duration: CGFloat = transitionDuration(using: transitionContext)
let oldToViewSuperview: UIView? = toView.superview
toView.layoutIfNeeded()
// If we can't retrieve the contextual info we need to perform the proper zoom animation then
// just fade the destination in (otherwise the user would get stuck on a blank screen)
guard
let fromMediaContext: MediaPresentationContext = fromContextProvider.mediaPresentationContext(mediaItem: mediaItem, in: containerView),
let toMediaContext: MediaPresentationContext = toContextProvider.mediaPresentationContext(mediaItem: mediaItem, in: containerView),
let presentationImage: UIImage = mediaItem.image
else {
toView.frame = containerView.bounds
toView.alpha = 0
containerView.addSubview(toView)
UIView.animate(
withDuration: (duration / 2),
delay: 0,
options: .curveEaseInOut,
animations: {
toView.alpha = 1
},
completion: { _ in
// Need to ensure we add the 'toView' back to it's old superview if it had one
oldToViewSuperview?.addSubview(toView)
guard let fromMediaContext: MediaPresentationContext = fromContextProvider.mediaPresentationContext(mediaItem: mediaItem, in: containerView) else {
transitionContext.completeTransition(false)
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
)
return
}
guard let toMediaContext: MediaPresentationContext = toContextProvider.mediaPresentationContext(mediaItem: mediaItem, in: containerView) else {
transitionContext.completeTransition(false)
return
}
guard let presentationImage: UIImage = mediaItem.image else {
transitionContext.completeTransition(true)
return
}
let duration: CGFloat = transitionDuration(using: transitionContext)
fromMediaContext.mediaView.alpha = 0
toMediaContext.mediaView.alpha = 0
let fromSnapshotView: UIView = (fromVC.view.snapshotView(afterScreenUpdates: false) ?? UIView())
containerView.addSubview(fromSnapshotView)
toView.frame = containerView.bounds
toView.alpha = 0
containerView.addSubview(toView)
let transitionView = UIImageView(image: presentationImage)
let transitionView: UIImageView = UIImageView(image: presentationImage)
transitionView.frame = fromMediaContext.presentationFrame
transitionView.contentMode = MediaView.contentMode
transitionView.layer.masksToBounds = true
transitionView.layer.cornerRadius = fromMediaContext.cornerRadius
transitionView.layer.maskedCorners = fromMediaContext.cornerMask
containerView.addSubview(transitionView)
// Note: We need to do this after adding the 'transitionView' and insert it at the back
// otherwise the screen can flicker since we have 'afterScreenUpdates: true' (if we use
// 'afterScreenUpdates: false' then the 'fromMediaContext.mediaView' won't be hidden
// during the transition)
let fromSnapshotView: UIView = (fromVC.view.snapshotView(afterScreenUpdates: true) ?? UIView())
containerView.insertSubview(fromSnapshotView, at: 0)
let overshootPercentage: CGFloat = 0.15
let overshootFrame: CGRect = CGRect(
x: (toMediaContext.presentationFrame.minX + ((toMediaContext.presentationFrame.minX - fromMediaContext.presentationFrame.minX) * overshootPercentage)),
y: (toMediaContext.presentationFrame.minY + ((toMediaContext.presentationFrame.minY - fromMediaContext.presentationFrame.minY) * overshootPercentage)),
width: (toMediaContext.presentationFrame.width + ((toMediaContext.presentationFrame.width - fromMediaContext.presentationFrame.width) * overshootPercentage)),
height: (toMediaContext.presentationFrame.height + ((toMediaContext.presentationFrame.height - fromMediaContext.presentationFrame.height) * overshootPercentage))
let overshootFrame: CGRect = (self.shouldBounce ?
CGRect(
x: (toMediaContext.presentationFrame.minX + ((toMediaContext.presentationFrame.minX - fromMediaContext.presentationFrame.minX) * overshootPercentage)),
y: (toMediaContext.presentationFrame.minY + ((toMediaContext.presentationFrame.minY - fromMediaContext.presentationFrame.minY) * overshootPercentage)),
width: (toMediaContext.presentationFrame.width + ((toMediaContext.presentationFrame.width - fromMediaContext.presentationFrame.width) * overshootPercentage)),
height: (toMediaContext.presentationFrame.height + ((toMediaContext.presentationFrame.height - fromMediaContext.presentationFrame.height) * overshootPercentage))
) :
toMediaContext.presentationFrame
)
// Add any UI elements which should appear above the media view

View File

@ -6,7 +6,6 @@
#import "Session-Swift.h"
#import <SignalCoreKit/Threading.h>
#import <SessionMessagingKit/Environment.h>
#import <SignalUtilitiesKit/OWSProfileManager.h>
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
NS_ASSUME_NONNULL_BEGIN

View File

@ -9,12 +9,9 @@
// Separate iOS Frameworks from other imports.
#import "AvatarViewHelper.h"
#import "AVAudioSession+OWS.h"
#import "ContactCellView.h"
#import "ContactTableViewCell.h"
#import "ConversationViewItem.h"
#import "ConversationViewModel.h"
#import "DateUtil.h"
#import "MediaDetailViewController.h"
#import "NotificationSettingsViewController.h"
#import "OWSAnyTouchGestureRecognizer.h"
#import "OWSAudioPlayer.h"
@ -39,12 +36,10 @@
#import <SignalCoreKit/OWSLogs.h>
#import <SignalCoreKit/Threading.h>
#import <SignalUtilitiesKit/AttachmentSharing.h>
#import <SignalUtilitiesKit/ContactTableViewCell.h>
#import <SessionMessagingKit/Environment.h>
#import <SessionMessagingKit/OWSAudioPlayer.h>
#import <SignalUtilitiesKit/OWSFormat.h>
#import <SessionMessagingKit/OWSPreferences.h>
#import <SignalUtilitiesKit/OWSProfileManager.h>
#import <SessionMessagingKit/OWSQuotedReplyModel.h>
#import <SignalUtilitiesKit/OWSViewController.h>
#import <SignalUtilitiesKit/UIColor+OWS.h>
@ -62,10 +57,7 @@
#import <SignalUtilitiesKit/OWSDispatch.h>
#import <SignalUtilitiesKit/OWSError.h>
#import <SessionUtilitiesKit/OWSFileSystem.h>
#import <SessionMessagingKit/OWSMediaGalleryFinder.h>
#import <SessionMessagingKit/OWSRecipientIdentity.h>
#import <SignalUtilitiesKit/SignalAccount.h>
#import <SessionMessagingKit/SignalRecipient.h>
#import <SessionMessagingKit/TSAccountManager.h>
#import <SessionMessagingKit/TSAttachment.h>
#import <SessionMessagingKit/TSAttachmentPointer.h>

View File

@ -195,7 +195,7 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
let notificationTitle: String?
var notificationBody: String?
let senderName = Profile.displayName(db, id: interaction.authorId, thread: thread)
let senderName = Profile.displayName(db, id: interaction.authorId, threadVariant: thread.variant)
let previewType: Preferences.NotificationPreviewType = db[.preferencesNotificationPreviewType]
.defaulting(to: .nameAndPreview)
@ -347,13 +347,8 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
private func checkIfShouldPlaySound() -> Bool {
AssertIsOnMainThread()
guard UIApplication.shared.applicationState == .active else {
return true
}
guard preferences.soundInForeground() else {
return false
}
guard UIApplication.shared.applicationState == .active else { return true }
guard GRDBStorage.shared[.playNotificationSoundInForeground] else { return false }
let nowMs: UInt64 = UInt64(floor(Date().timeIntervalSince1970 * 1000))
let recentThreshold = nowMs - UInt64(kAudioNotificationsThrottleInterval * Double(kSecondInMs))

View File

@ -4,7 +4,6 @@
#import "NotificationSettingsOptionsViewController.h"
#import "Session-Swift.h"
#import <SessionMessagingKit/Environment.h>
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
@implementation NotificationSettingsOptionsViewController

View File

@ -7,8 +7,6 @@
#import "NotificationSettingsViewController.h"
#import "NotificationSettingsOptionsViewController.h"
#import "OWSSoundSettingsViewController.h"
#import <SessionMessagingKit/Environment.h>
#import <SessionMessagingKit/OWSPreferences.h>
#import <SessionMessagingKit/SessionMessagingKit-Swift.h>
#import <SignalUtilitiesKit/UIUtil.h>
#import "Session-Swift.h"
@ -40,8 +38,6 @@
__weak NotificationSettingsViewController *weakSelf = self;
OWSPreferences *prefs = Environment.shared.preferences;
OWSTableSection *strategySection = [OWSTableSection new];
strategySection.headerTitle = NSLocalizedString(@"preferences_notifications_strategy_category_title", @"");
[strategySection addItem:[OWSTableItem switchItemWithText:NSLocalizedString(@"vc_notification_settings_notification_mode_title", @"")
@ -79,7 +75,7 @@
[soundsSection addItem:[OWSTableItem switchItemWithText:inAppSoundsLabelText
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"in_app_sounds")
isOnBlock:^{
return [prefs soundInForeground];
return [SMKPreferences playNotificationSoundInForeground];
}
isEnabledBlock:^{
return YES;
@ -111,7 +107,7 @@
- (void)didToggleSoundNotificationsSwitch:(UISwitch *)sender
{
[Environment.shared.preferences setSoundInForeground:sender.on];
[SMKPreferences setPlayNotificationSoundInForeground:sender.on];
}
- (void)didToggleAPNsSwitch:(UISwitch *)sender

View File

@ -72,7 +72,7 @@ static NSString *const kSealedSenderInfoURL = @"https://signal.org/blog/sealed-s
@"Label for the 'read receipts' setting.")
accessibilityIdentifier:[NSString stringWithFormat:@"settings.privacy.%@", @"read_receipts"]
isOnBlock:^{
return [SSKPreferences areReadReceiptsEnabled];
return [SMKPreferences areReadReceiptsEnabled];
}
isEnabledBlock:^{
return YES;
@ -90,7 +90,7 @@ static NSString *const kSealedSenderInfoURL = @"https://signal.org/blog/sealed-s
@"Label for the 'typing indicators' setting.")
accessibilityIdentifier:[NSString stringWithFormat:@"settings.privacy.%@", @"typing_indicators"]
isOnBlock:^{
return [SSKPreferences areTypingIndicatorsEnabled];
return [SMKPreferences areTypingIndicatorsEnabled];
}
isEnabledBlock:^{
return YES;
@ -143,7 +143,7 @@ static NSString *const kSealedSenderInfoURL = @"https://signal.org/blog/sealed-s
addItem:[OWSTableItem switchItemWithText:NSLocalizedString(@"Disable Preview in App Switcher", @"")
accessibilityIdentifier:[NSString stringWithFormat:@"settings.privacy.%@", @"screen_security"]
isOnBlock:^{
return [Environment.shared.preferences screenSecurityIsEnabled];
return [SMKPreferences isScreenSecurityEnabled];
}
isEnabledBlock:^{
return YES;
@ -168,7 +168,7 @@ static NSString *const kSealedSenderInfoURL = @"https://signal.org/blog/sealed-s
@"Setting for enabling & disabling link previews.")
accessibilityIdentifier:[NSString stringWithFormat:@"settings.privacy.%@", @"link_previews"]
isOnBlock:^{
return [SSKPreferences areLinkPreviewsEnabled];
return [SMKPreferences areLinkPreviewsEnabled];
}
isEnabledBlock:^{
return YES;
@ -219,28 +219,28 @@ static NSString *const kSealedSenderInfoURL = @"https://signal.org/blog/sealed-s
{
BOOL enabled = sender.isOn;
OWSLogInfo(@"toggled screen security: %@", enabled ? @"ON" : @"OFF");
[SSKPreferences setScreenSecurity:enabled];
[SMKPreferences setScreenSecurity:enabled];
}
- (void)didToggleReadReceiptsSwitch:(UISwitch *)sender
{
BOOL enabled = sender.isOn;
OWSLogInfo(@"toggled areReadReceiptsEnabled: %@", enabled ? @"ON" : @"OFF");
[SSKPreferences setAreReadReceiptsEnabled:enabled];
[SMKPreferences setAreReadReceiptsEnabled:enabled];
}
- (void)didToggleTypingIndicatorsSwitch:(UISwitch *)sender
{
BOOL enabled = sender.isOn;
OWSLogInfo(@"toggled areTypingIndicatorsEnabled: %@", enabled ? @"ON" : @"OFF");
[SSKPreferences setTypingIndicatorsEnabled:enabled];
[SMKPreferences setTypingIndicatorsEnabled:enabled];
}
- (void)didToggleLinkPreviewsEnabled:(UISwitch *)sender
{
BOOL enabled = sender.isOn;
OWSLogInfo(@"toggled to: %@", (enabled ? @"ON" : @"OFF"));
SSKPreferences.areLinkPreviewsEnabled = enabled;
[SMKPreferences setLinkPreviewsEnabled:enabled];
}
- (void)isScreenLockEnabledDidChange:(UISwitch *)sender

View File

@ -217,6 +217,7 @@ public extension ConversationCell.ViewModel {
let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
let linkPreview: TypedTableAlias<LinkPreview> = TypedTableAlias()
let interactionAttachment: TypedTableAlias<InteractionAttachment> = TypedTableAlias()
let attachment: TypedTableAlias<Attachment> = TypedTableAlias()
let recipientState: TypedTableAlias<RecipientState> = TypedTableAlias()
let unreadCountTableLiteral: SQL = SQL(stringLiteral: "\(ViewModel.threadUnreadCountString)_table")
@ -227,8 +228,9 @@ public extension ConversationCell.ViewModel {
let profileNameColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.name.name)
let authorProfileLiteral: SQL = SQL(stringLiteral: "authorProfile")
let attachmentIdColumnLiteral: SQL = SQL(stringLiteral: Attachment.Columns.id.name)
let firstInteractionAttachmentLiteral: SQL = SQL(stringLiteral: "firstInteractionAttachmentLiteral")
let firstInteractionAttachmentLiteral: SQL = SQL(stringLiteral: "firstInteractionAttachment")
let interactionAttachmentAttachmentIdColumnLiteral: SQL = SQL(stringLiteral: InteractionAttachment.Columns.attachmentId.name)
let interactionAttachmentInteractionIdColumnLiteral: SQL = SQL(stringLiteral: InteractionAttachment.Columns.interactionId.name)
let interactionAttachmentAlbumIndexColumnLiteral: SQL = SQL(stringLiteral: InteractionAttachment.Columns.albumIndex.name)
let interactionStateTableLiteral: SQL = SQL(stringLiteral: "interactionState")
let interactionStateInteractionIdColumnLiteral: SQL = SQL(stringLiteral: RecipientState.Columns.interactionId.name)
@ -355,21 +357,31 @@ public extension ConversationCell.ViewModel {
LEFT JOIN \(InteractionAttachment.self) ON \(interactionAttachment[.interactionId]) = \(interaction[.id])
LEFT JOIN \(InteractionAttachment.self) AS \(firstInteractionAttachmentLiteral) ON (
\(firstInteractionAttachmentLiteral).\(interactionAttachmentAlbumIndexColumnLiteral) = 0 AND
\(firstInteractionAttachmentLiteral).\(interactionAttachmentAttachmentIdColumnLiteral) = \(interaction[.id])
\(firstInteractionAttachmentLiteral).\(interactionAttachmentInteractionIdColumnLiteral) = \(interaction[.id])
)
LEFT JOIN \(Attachment.self) AS \(ViewModel.interactionAttachmentDescriptionInfoKey) ON \(ViewModel.interactionAttachmentDescriptionInfoKey).\(attachmentIdColumnLiteral) = \(firstInteractionAttachmentLiteral).\(interactionAttachmentAttachmentIdColumnLiteral)
LEFT JOIN (
SELECT
\(recipientState[.interactionId]),
\(recipientState[.state])
FROM \(RecipientState.self)
JOIN \(Interaction.self) ON \(interaction[.id]) = \(recipientState[.interactionId])
WHERE \(SQL("\(recipientState[.state]) != \(RecipientState.State.skipped)")) -- Ignore 'skipped'
ORDER BY
-- If there is a single 'sending' then should be 'sending', otherwise if there is a single
-- 'failed' and there is no 'sending' then it should be 'failed'
\(SQL("\(recipientState[.state]) = \(RecipientState.State.sending)")) DESC,
\(SQL("\(recipientState[.state]) = \(RecipientState.State.failed)")) DESC
\(attachment[.id]),
\(attachment[.variant]),
\(attachment[.contentType]),
\(attachment[.sourceFilename])
FROM \(Attachment.self)
) AS \(ViewModel.interactionAttachmentDescriptionInfoKey) ON \(ViewModel.interactionAttachmentDescriptionInfoKey).\(attachmentIdColumnLiteral) = \(firstInteractionAttachmentLiteral).\(interactionAttachmentAttachmentIdColumnLiteral)
LEFT JOIN (
SELECT * FROM (
SELECT
\(recipientState[.interactionId]),
\(recipientState[.state])
FROM \(RecipientState.self)
JOIN \(Interaction.self) ON \(interaction[.id]) = \(recipientState[.interactionId])
WHERE \(SQL("\(recipientState[.state]) != \(RecipientState.State.skipped)")) -- Ignore 'skipped'
ORDER BY
-- If there is a single 'sending' then should be 'sending', otherwise if there is a single
-- 'failed' and there is no 'sending' then it should be 'failed'
\(SQL("\(recipientState[.state]) = \(RecipientState.State.sending)")) DESC,
\(SQL("\(recipientState[.state]) = \(RecipientState.State.failed)")) DESC
) AS \(interactionStateTableLiteral)
GROUP BY \(interactionStateTableLiteral).\(interactionStateInteractionIdColumnLiteral)
) AS \(interactionStateTableLiteral) ON \(interactionStateTableLiteral).\(interactionStateInteractionIdColumnLiteral) = \(interaction[.id])
WHERE (
@ -578,7 +590,7 @@ public extension ConversationCell.ViewModel {
\(ViewModel.closedGroupProfileBackFallbackKey).\(profileIdColumnLiteral) = \(userPublicKey)
)
ORDER BY rank
ORDER BY \(Column.rank)
"""
return request.adapted { db in
@ -644,7 +656,7 @@ public extension ConversationCell.ViewModel {
var sqlQuery: SQL = ""
let selectQuery: SQL = """
SELECT
IFNULL(rank, 100) AS rank,
IFNULL(\(Column.rank), 100) AS \(Column.rank),
\(thread[.id]) AS \(ViewModel.threadIdKey),
\(thread[.variant]) AS \(ViewModel.threadVariantKey),
@ -1037,7 +1049,7 @@ public extension ConversationCell.ViewModel {
GROUP BY \(ViewModel.threadIdKey)
ORDER BY
rank,
\(Column.rank),
\(ViewModel.threadIsNoteToSelfKey),
\(ViewModel.closedGroupNameKey),
\(ViewModel.openGroupNameKey),

View File

@ -351,8 +351,8 @@ NS_ASSUME_NONNULL_BEGIN
if (Environment.shared.isRequestingPermission) {
return ScreenLockUIStateNone;
}
if (Environment.shared.preferences.screenSecurityIsEnabled) {
if ([SMKPreferences isScreenSecurityEnabled]) {
OWSLogVerbose(@"desiredUIState: screen protection 4.");
return ScreenLockUIStateScreenProtection;
} else {

View File

@ -8,7 +8,6 @@ NS_ASSUME_NONNULL_BEGIN
@class AvatarViewHelper;
@class OWSContactsManager;
@class SignalAccount;
@class TSThread;
@protocol AvatarViewHelperDelegate <NSObject>

View File

@ -0,0 +1,10 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import DifferenceKit
public extension ArraySection {
init(section: Model, elements: [Element] = []) {
self.init(model: section, elements: elements)
}
}

View File

@ -30,7 +30,6 @@ public enum MentionUtilities {
var string = string
var lastMatchEnd: Int = 0
var mentions: [(range: NSRange, publicKey: String)] = []
let context: Profile.Context = (threadVariant == .openGroup ? .openGroup : .regular)
while let match: NSTextCheckingResult = regex.firstMatch(
in: string,
@ -41,7 +40,7 @@ public enum MentionUtilities {
let publicKey: String = String(string[range].dropFirst()) // Drop the @
guard let displayName: String = Profile.displayNameNoFallback(id: publicKey, context: context) else {
guard let displayName: String = Profile.displayNameNoFallback(id: publicKey, threadVariant: threadVariant) else {
lastMatchEnd = (match.range.location + match.range.length)
continue
}

View File

@ -44,11 +44,16 @@ public enum Legacy {
internal static let attachmentUploadJobCollection = "AttachmentUploadJobCollection"
internal static let attachmentDownloadJobCollection = "AttachmentDownloadJobCollection"
// Preferences
internal static let preferencesCollection = "SignalPreferences"
internal static let preferencesKeyNotificationPreviewType = "preferencesKeyNotificationPreviewType"
internal static let preferencesKeyScreenSecurityDisabled = "Screen Security Key"
internal static let preferencesKeyLastRecordedPushToken = "LastRecordedPushToken"
internal static let preferencesKeyLastRecordedVoipToken = "LastRecordedVoipToken"
internal static let preferencesKeyAreLinkPreviewsEnabled = "areLinkPreviewsEnabled"
internal static let preferencesKeyNotificationPreviewType = "preferencesKeyNotificationPreviewType"
internal static let preferencesKeyNotificationSoundInForeground = "NotificationSoundInForeground"
internal static let preferencesKeyHasSavedThreadKey = "hasSavedThread"
internal static let readReceiptManagerCollection = "OWSReadReceiptManagerCollection"
internal static let readReceiptManagerAreReadReceiptsEnabled = "areReadReceiptsEnabled"
@ -56,6 +61,10 @@ public enum Legacy {
internal static let typingIndicatorsCollection = "TypingIndicators"
internal static let typingIndicatorsEnabledKey = "kDatabaseKey_TypingIndicatorsEnabled"
internal static let screenLockCollection = "OWSScreenLock_Collection"
internal static let screenLockIsScreenLockEnabledKey = "OWSScreenLock_Key_IsScreenLockEnabled"
internal static let screenLockScreenLockTimeoutSecondsKey = "OWSScreenLock_Key_ScreenLockTimeoutSeconds"
internal static let soundsStorageNotificationCollection = "kOWSSoundsStorageNotificationCollection"
internal static let soundsGlobalNotificationKey = "kOWSSoundsStorageGlobalNotificationKey"

View File

@ -1255,12 +1255,25 @@ enum _003_YDBToGRDBMigration: Migration {
inCollection: Legacy.typingIndicatorsCollection,
defaultValue: false
)
legacyPreferences[Legacy.screenLockIsScreenLockEnabledKey] = transaction.bool(
forKey: Legacy.screenLockIsScreenLockEnabledKey,
inCollection: Legacy.screenLockCollection,
defaultValue: false
)
legacyPreferences[Legacy.screenLockScreenLockTimeoutSecondsKey] = transaction.double(
forKey: Legacy.screenLockScreenLockTimeoutSecondsKey,
inCollection: Legacy.screenLockCollection,
defaultValue: (15 * 60)
)
}
db[.preferencesNotificationPreviewType] = Preferences.NotificationPreviewType(rawValue: legacyPreferences[Legacy.preferencesKeyNotificationPreviewType] as? Int ?? -1)
.defaulting(to: .nameAndPreview)
db[.defaultNotificationSound] = Preferences.Sound(rawValue: legacyPreferences[Legacy.soundsGlobalNotificationKey] as? Int ?? -1)
.defaulting(to: Preferences.Sound.defaultNotificationSound)
db[.playNotificationSoundInForeground] = (legacyPreferences[Legacy.preferencesKeyNotificationSoundInForeground] as? Bool == true)
db[.preferencesNotificationPreviewType] = Preferences.NotificationPreviewType(rawValue: legacyPreferences[Legacy.preferencesKeyNotificationPreviewType] as? Int ?? -1)
.defaulting(to: .nameAndPreview)
if let lastPushToken: String = legacyPreferences[Legacy.preferencesKeyLastRecordedPushToken] as? String {
db[.lastRecordedPushToken] = lastPushToken
@ -1270,15 +1283,19 @@ enum _003_YDBToGRDBMigration: Migration {
db[.lastRecordedVoipToken] = lastVoipToken
}
// Note: The 'preferencesKeyScreenSecurityDisabled' value previously controlled whether the setting
// was disabled, this has been inverted to 'preferencesAppSwitcherPreviewEnabled' so it can default
// Note: The 'preferencesKeyScreenSecurityDisabled' value previously controlled whether the
// setting was disabled, this has been inverted to 'appSwitcherPreviewEnabled' so it can default
// to 'false' (as most Bool values do)
db[.preferencesAppSwitcherPreviewEnabled] = (legacyPreferences[Legacy.preferencesKeyScreenSecurityDisabled] as? Bool == false)
db[.areReadReceiptsEnabled] = (legacyPreferences[Legacy.readReceiptManagerAreReadReceiptsEnabled] as? Bool == true)
db[.typingIndicatorsEnabled] = (legacyPreferences[Legacy.typingIndicatorsEnabledKey] as? Bool == true)
db[.isScreenLockEnabled] = (legacyPreferences[Legacy.screenLockIsScreenLockEnabledKey] as? Bool == true)
db[.screenLockTimeoutSeconds] = (legacyPreferences[Legacy.screenLockScreenLockTimeoutSecondsKey] as? Double)
.defaulting(to: (15 * 60))
db[.appSwitcherPreviewEnabled] = (legacyPreferences[Legacy.preferencesKeyScreenSecurityDisabled] as? Bool == false)
db[.areLinkPreviewsEnabled] = (legacyPreferences[Legacy.preferencesKeyAreLinkPreviewsEnabled] as? Bool == true)
db[.hasHiddenMessageRequests] = CurrentAppContext().appUserDefaults()
.bool(forKey: Legacy.userDefaultsHasHiddenMessageRequests)
db[.hasSavedThreadKey] = (legacyPreferences[Legacy.preferencesKeyHasSavedThreadKey] as? Bool == true)
print("RAWR [\(Date().timeIntervalSince1970)] - Process preferences inserts - End")
@ -1381,24 +1398,23 @@ enum _003_YDBToGRDBMigration: Migration {
return (true, cachedDuration)
}
let (isValid, duration): (Bool, TimeInterval?) = Attachment.determineValidityAndDuration(
let attachmentVailidityInfo = Attachment.determineValidityAndDuration(
contentType: stream.contentType,
localRelativeFilePath: processedLocalRelativeFilePath,
originalFilePath: originalFilePath
)
return (isValid, duration)
return (attachmentVailidityInfo.isValid, attachmentVailidityInfo.duration)
}
if stream.isVideo {
let videoPlayer: AVPlayer = AVPlayer(url: URL(fileURLWithPath: originalFilePath))
let duration: TimeInterval? = videoPlayer.currentItem
.map { item -> TimeInterval in
// Accorting to the CMTime docs "value/timescale = seconds"
(TimeInterval(item.duration.value) / TimeInterval(item.duration.timescale))
}
let attachmentVailidityInfo = Attachment.determineValidityAndDuration(
contentType: stream.contentType,
localRelativeFilePath: processedLocalRelativeFilePath,
originalFilePath: originalFilePath
)
return ((duration ?? 0) > 0, duration)
return (attachmentVailidityInfo.isValid, attachmentVailidityInfo.duration)
}
if stream.isVisualMedia {

View File

@ -10,9 +10,9 @@ import AVFoundation
public struct Attachment: Codable, Identifiable, Equatable, Hashable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
public static var databaseTableName: String { "attachment" }
internal static let interactionAttachments = hasOne(InteractionAttachment.self)
internal static let quoteForeignKey = ForeignKey([Columns.id], to: [Quote.Columns.attachmentId])
internal static let linkPreviewForeignKey = ForeignKey([Columns.id], to: [LinkPreview.Columns.attachmentId])
public static let interactionAttachments = hasOne(InteractionAttachment.self)
public static let interaction = hasOne(
Interaction.self,
through: interactionAttachments,
@ -245,23 +245,27 @@ public struct Attachment: Codable, Identifiable, Equatable, Hashable, FetchableR
// MARK: - CustomStringConvertible
extension Attachment: CustomStringConvertible {
public struct DescriptionInfo: FetchableRecord, Decodable, Equatable, ColumnExpressible {
public struct DescriptionInfo: FetchableRecord, Decodable, Equatable, Hashable, ColumnExpressible {
public typealias Columns = CodingKeys
public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable {
case id
case variant
case contentType
case sourceFilename
}
let id: String
let variant: Attachment.Variant
let contentType: String
let sourceFilename: String?
public init(
id: String,
variant: Attachment.Variant,
contentType: String,
sourceFilename: String?
) {
self.id = id
self.variant = variant
self.contentType = contentType
self.sourceFilename = sourceFilename
@ -279,8 +283,7 @@ extension Attachment: CustomStringConvertible {
public static func description(for descriptionInfo: DescriptionInfo, count: Int) -> String {
// We only support multi-attachment sending of images so we can just default to the image attachment
// if there were multiple attachments
guard count == 1 else { return "\("ATTACHMENT".localized()) \(emoji(for: OWSMimeTypeImageJpeg))" }
guard count == 1 else { return "\(emoji(for: OWSMimeTypeImageJpeg)) \("ATTACHMENT".localized())" }
if MIMETypeUtil.isAudio(descriptionInfo.contentType) {
// a missing filename is the legacy way to determine if an audio attachment is
// a voice note vs. other arbitrary audio attachments.
@ -293,7 +296,7 @@ extension Attachment: CustomStringConvertible {
}
}
return "\("ATTACHMENT".localized()) \(emoji(for: descriptionInfo.contentType))"
return "\(emoji(for: descriptionInfo.contentType)) \("ATTACHMENT".localized())"
}
public static func emoji(for contentType: String) -> String {
@ -316,6 +319,7 @@ extension Attachment: CustomStringConvertible {
public var description: String {
return Attachment.description(
for: DescriptionInfo(
id: id,
variant: variant,
contentType: contentType,
sourceFilename: sourceFilename
@ -679,12 +683,11 @@ extension Attachment {
// Process video attachments
if MIMETypeUtil.isVideo(contentType) {
let videoPlayer: AVPlayer = AVPlayer(url: URL(fileURLWithPath: targetPath))
let durationSeconds: TimeInterval? = videoPlayer.currentItem
.map { item -> TimeInterval in
// Accorting to the CMTime docs "value/timescale = seconds"
(TimeInterval(item.duration.value) / TimeInterval(item.duration.timescale))
}
let asset: AVURLAsset = AVURLAsset(url: URL(fileURLWithPath: targetPath), options: nil)
let durationSeconds: TimeInterval = (
// According to the CMTime docs "value/timescale = seconds"
TimeInterval(asset.duration.value) / TimeInterval(asset.duration.timescale)
)
return (
OWSMediaUtils.isValidVideo(path: targetPath),
@ -831,6 +834,27 @@ extension Attachment {
loadThumbnail(with: size.dimension, success: success, failure: failure)
}
public func existingThumbnail(size: ThumbnailSize) -> UIImage? {
var existingImage: UIImage?
let semaphore: DispatchSemaphore = DispatchSemaphore(value: 0)
loadThumbnail(
with: size.dimension,
success: { image, _ in
existingImage = image
semaphore.signal()
},
failure: { semaphore.signal() }
)
// We don't really want to wait at all so having a tiny timeout here will give the
// 'loadThumbnail' call the change to return a result for an existing thumbnail but
// not a new one
_ = semaphore.wait(timeout: .now() + .milliseconds(10))
return existingImage
}
public func cloneAsThumbnail() -> Attachment? {
let cloneId: String = UUID().uuidString
let thumbnailName: String = "quoted-thumbnail-\(sourceFilename ?? "null")"

View File

@ -5,7 +5,7 @@ import GRDB
import SignalCoreKit
import SessionUtilitiesKit
public struct Profile: Codable, Identifiable, Equatable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible, CustomStringConvertible {
public struct Profile: Codable, Identifiable, Equatable, Hashable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible, CustomStringConvertible {
public static var databaseTableName: String { "profile" }
internal static let interactionForeignKey = ForeignKey([Columns.id], to: [Interaction.Columns.authorId])
internal static let contactForeignKey = ForeignKey([Columns.id], to: [Contact.Columns.id])
@ -244,43 +244,26 @@ public extension Profile {
.defaulting(to: [])
}
static func displayName(_ db: Database? = nil, id: ID, thread: SessionThread, customFallback: String? = nil) -> String {
return displayName(
db,
id: id,
context: (thread.variant == .openGroup ? .openGroup : .regular),
customFallback: customFallback
)
}
static func displayName(_ db: Database? = nil, id: ID, context: Context = .regular, customFallback: String? = nil) -> String {
static func displayName(_ db: Database? = nil, id: ID, threadVariant: SessionThread.Variant = .contact, customFallback: String? = nil) -> String {
guard let db: Database = db else {
return GRDBStorage.shared
.read { db in displayName(db, id: id, context: context, customFallback: customFallback) }
.read { db in displayName(db, id: id, threadVariant: threadVariant, customFallback: customFallback) }
.defaulting(to: (customFallback ?? id))
}
let existingDisplayName: String? = (try? Profile.fetchOne(db, id: id))?
.displayName(for: context)
.displayName(for: threadVariant)
return (existingDisplayName ?? (customFallback ?? id))
}
static func displayNameNoFallback(_ db: Database? = nil, id: ID, thread: SessionThread) -> String? {
return displayName(
db,
id: id,
context: (thread.variant == .openGroup ? .openGroup : .regular)
)
}
static func displayNameNoFallback(_ db: Database? = nil, id: ID, context: Context = .regular) -> String? {
static func displayNameNoFallback(_ db: Database? = nil, id: ID, threadVariant: SessionThread.Variant = .contact) -> String? {
guard let db: Database = db else {
return GRDBStorage.shared.read { db in displayNameNoFallback(db, id: id, context: context) }
return GRDBStorage.shared.read { db in displayNameNoFallback(db, id: id, threadVariant: threadVariant) }
}
return (try? Profile.fetchOne(db, id: id))?
.displayName(for: context)
.displayName(for: threadVariant)
}
// MARK: - Fetch or Create
@ -352,13 +335,6 @@ public extension Profile {
// MARK: - Convenience
public extension Profile {
// MARK: - Context
@objc enum Context: Int {
case regular
case openGroup
}
// MARK: - Truncation
enum Truncation {
@ -387,15 +363,8 @@ public extension Profile {
}
/// The name to display in the UI for a given thread variant
func displayName(for threadVariant: SessionThread.Variant) -> String {
return displayName(
for: (threadVariant == .openGroup ? .openGroup : .regular)
)
}
/// The name to display in the UI
func displayName(for context: Context = .regular) -> String {
return Profile.displayName(for: context, id: id, name: name, nickname: nickname)
func displayName(for threadVariant: SessionThread.Variant = .contact) -> String {
return Profile.displayName(for: threadVariant, id: id, name: name, nickname: nickname)
}
static func displayName(
@ -404,22 +373,6 @@ public extension Profile {
name: String?,
nickname: String?,
customFallback: String? = nil
) -> String {
return Profile.displayName(
for: (threadVariant == .openGroup ? .openGroup : .regular),
id: id,
name: name,
nickname: nickname,
customFallback: customFallback
)
}
static func displayName(
for context: Context,
id: String,
name: String?,
nickname: String?,
customFallback: String? = nil
) -> String {
if let nickname: String = nickname { return nickname }
@ -427,8 +380,8 @@ public extension Profile {
return (customFallback ?? Profile.truncated(id: id, truncating: .middle))
}
switch context {
case .regular: return name
switch threadVariant {
case .contact, .closedGroup: return name
case .openGroup:
// In open groups, where it's more likely that multiple users have the same name,
@ -444,43 +397,6 @@ public extension Profile {
@objc(SMKProfile)
public class SMKProfile: NSObject {
var id: String
@objc var name: String
@objc var nickname: String?
init(id: String, name: String, nickname: String?) {
self.id = id
self.name = name
self.nickname = nickname
}
@objc public static func fetchCurrentUserName() -> String {
let existingProfile: Profile? = GRDBStorage.shared.read { db in
Profile.fetchOrCreateCurrentUser(db)
}
return (existingProfile?.name ?? "")
}
@objc public static func fetchOrCreate(id: String) -> SMKProfile {
let profile: Profile = Profile.fetchOrCreate(id: id)
return SMKProfile(
id: id,
name: profile.name,
nickname: profile.nickname
)
}
@objc public static func saveProfile(_ profile: SMKProfile) {
GRDBStorage.shared.write { db in
try? Profile
.fetchOrCreate(db, id: profile.id)
.with(nickname: .updateTo(profile.nickname))
.save(db)
}
}
@objc public static func displayName(id: String) -> String {
return Profile.displayName(id: id)
}
@ -489,22 +405,6 @@ public class SMKProfile: NSObject {
return Profile.displayName(id: id, customFallback: customFallback)
}
@objc public static func displayName(id: String, context: Profile.Context = .regular) -> String {
let existingProfile: Profile? = GRDBStorage.shared.read { db in
Profile.fetchOrCreateCurrentUser(db)
}
return (existingProfile?.name ?? id)
}
public static func displayName(id: String, thread: SessionThread) -> String {
return Profile.displayName(id: id, thread: thread)
}
@objc public static var localProfileKey: OWSAES256Key? {
Profile.fetchOrCreateCurrentUser().profileEncryptionKey
}
@objc(displayNameAfterSavingNickname:forProfileId:)
public static func displayNameAfterSaving(nickname: String?, for profileId: String) -> String {
return GRDBStorage.shared.write { db in

View File

@ -124,6 +124,12 @@ public struct SessionThread: Codable, Identifiable, Equatable, FetchableRecord,
// MARK: - Custom Database Interaction
public func insert(_ db: Database) throws {
try performInsert(db)
db[.hasSavedThreadKey] = true
}
public func delete(_ db: Database) throws -> Bool {
// Delete any jobs associated to this thread
try Job

View File

@ -7,7 +7,6 @@
#import "OWSDisappearingMessagesFinder.h"
#import "OWSFileSystem.h"
#import "OWSIncomingMessageFinder.h"
#import "OWSMediaGalleryFinder.h"
#import <SessionUtilitiesKit/SessionUtilitiesKit.h>
#import "OWSStorage.h"
#import "OWSStorage+Subclass.h"
@ -177,7 +176,6 @@ void VerifyRegistrationsForPrimaryStorage(OWSStorage *storage)
[FullTextSearchFinder asyncRegisterDatabaseExtensionWithStorage:self];
[OWSIncomingMessageFinder asyncRegisterExtensionWithPrimaryStorage:self];
[OWSDisappearingMessagesFinder asyncRegisterDatabaseExtensions:self];
[OWSMediaGalleryFinder asyncRegisterDatabaseExtensionsWithPrimaryStorage:self];
[TSDatabaseView asyncRegisterLazyRestoreAttachmentsDatabaseView:self];
[self.database

View File

@ -1,86 +0,0 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import SessionUtilitiesKit
@objc
public class SSKPreferences: NSObject {
// Never instantiate this class.
private override init() {}
private static let collection = "SSKPreferences"
// MARK: -
private static let areLinkPreviewsEnabledKey = "areLinkPreviewsEnabled"
@objc
public static var areLinkPreviewsEnabled: Bool {
get {
return getBool(key: areLinkPreviewsEnabledKey, defaultValue: false)
}
set {
setBool(newValue, key: areLinkPreviewsEnabledKey)
}
}
// MARK: -
private static let hasSavedThreadKey = "hasSavedThread"
@objc
public static var hasSavedThread: Bool {
get {
return getBool(key: hasSavedThreadKey)
}
set {
setBool(newValue, key: hasSavedThreadKey)
}
}
@objc
public class func setHasSavedThread(value: Bool, transaction: YapDatabaseReadWriteTransaction) {
transaction.setBool(value,
forKey: hasSavedThreadKey,
inCollection: collection)
}
// MARK: -
private class func getBool(key: String, defaultValue: Bool = false) -> Bool {
return OWSPrimaryStorage.dbReadConnection().bool(forKey: key, inCollection: collection, defaultValue: defaultValue)
}
private class func setBool(_ value: Bool, key: String) {
OWSPrimaryStorage.dbReadWriteConnection().setBool(value, forKey: key, inCollection: collection)
}
}
// MARK: - Objective C Support
public extension SSKPreferences {
@objc(setScreenSecurity:)
static func objc_setScreenSecurity(_ enabled: Bool) {
GRDBStorage.shared.write { db in db[.preferencesAppSwitcherPreviewEnabled] = enabled }
}
@objc(areReadReceiptsEnabled)
static func objc_areReadReceiptsEnabled() -> Bool {
return GRDBStorage.shared[.areReadReceiptsEnabled]
}
@objc(setAreReadReceiptsEnabled:)
static func objc_setAreReadReceiptsEnabled(_ enabled: Bool) {
GRDBStorage.shared.write { db in db[.areReadReceiptsEnabled] = enabled }
}
@objc(setTypingIndicatorsEnabled:)
static func objc_setTypingIndicatorsEnabled(_ enabled: Bool) {
GRDBStorage.shared.write { db in db[.typingIndicatorsEnabled] = enabled }
}
@objc(areTypingIndicatorsEnabled)
static func objc_areTypingIndicatorsEnabled() -> Bool {
return (GRDBStorage.shared.read { db in db[.typingIndicatorsEnabled] } == true)
}
}

View File

@ -21,7 +21,7 @@ public enum AttachmentDownloadJob: JobExecutor {
let threadId: String = job.threadId,
let detailsData: Data = job.details,
let details: Details = try? JSONDecoder().decode(Details.self, from: detailsData),
var attachment: Attachment = GRDBStorage.shared
let attachment: Attachment = GRDBStorage.shared
.read({ db in try Attachment.fetchOne(db, id: details.attachmentId) })
else {
failure(job, JobRunnerError.missingRequiredDetails, false)
@ -36,14 +36,12 @@ public enum AttachmentDownloadJob: JobExecutor {
return
}
// Update to the 'downloading' state
attachment = GRDBStorage.shared
.write { db in
try attachment
.with(state: .downloading)
.saved(db)
}
.defaulting(to: attachment)
// Update to the 'downloading' state (no need to update the 'attachment' instance)
GRDBStorage.shared.write { db in
try Attachment
.filter(id: attachment.id)
.updateAll(db, Attachment.Columns.state.set(to: Attachment.State.downloading))
}
let temporaryFileUrl: URL = URL(
fileURLWithPath: OWSTemporaryDirectoryAccessibleAfterFirstAuth() + UUID().uuidString
@ -94,16 +92,19 @@ public enum AttachmentDownloadJob: JobExecutor {
// Remove the temporary file
OWSFileSystem.deleteFile(temporaryFileUrl.path)
// Update the attachment state
/// Update the attachment state
///
/// **Note:** We **MUST** use the `'with()` function here as it will update the
/// `isValid` and `duration` values based on the downloaded data and the state
GRDBStorage.shared.write { db in
try attachment
_ = try attachment
.with(
state: .downloaded,
creationTimestamp: Date().timeIntervalSince1970,
localRelativeFilePath: attachment.originalFilePath?
.substring(from: (Attachment.attachmentsFolder.count + 1)) // Leading forward slash
)
.save(db)
.saved(db)
}
success(job, false)
@ -113,12 +114,15 @@ public enum AttachmentDownloadJob: JobExecutor {
switch error {
case OnionRequestAPI.Error.httpRequestFailedAtDestination(let statusCode, _, _) where statusCode == 400:
// Otherwise, the attachment will show a state of downloading forever,
// and the message won't be able to be marked as read
/// Otherwise, the attachment will show a state of downloading forever, and the message
/// won't be able to be marked as read
///
/// **Note:** We **MUST** use the `'with()` function here as it will update the
/// `isValid` and `duration` values based on the downloaded data and the state
GRDBStorage.shared.write { db in
try attachment
_ = try attachment
.with(state: .failed)
.save(db)
.saved(db)
}
// This usually indicates a file that has expired on the server, so there's no need to retry
@ -154,163 +158,3 @@ extension AttachmentDownloadJob {
}
}
}
// TODO: MessageInvalidator.invalidate(tsMessage, with: transaction)
// public let attachmentID: String
// public let tsMessageID: String
// public let threadID: String
// public var delegate: JobDelegate?
// public var id: String?
// public var failureCount: UInt = 0
// public var isDeferred = false
//
// public enum Error : LocalizedError {
// case noAttachment
// case invalidURL
//
// public var errorDescription: String? {
// switch self {
// case .noAttachment: return "No such attachment."
// case .invalidURL: return "Invalid file URL."
// }
// }
// }
//
// // MARK: Settings
// public class var collection: String { return "AttachmentDownloadJobCollection" }
// public static let maxFailureCount: UInt = 20
//
// // MARK: Initialization
// public init(attachmentID: String, tsMessageID: String, threadID: String) {
// self.attachmentID = attachmentID
// self.tsMessageID = tsMessageID
// self.threadID = threadID
// }
//
// // MARK: Coding
// public init?(coder: NSCoder) {
// guard let attachmentID = coder.decodeObject(forKey: "attachmentID") as! String?,
// let tsMessageID = coder.decodeObject(forKey: "tsIncomingMessageID") as! String?,
// let threadID = coder.decodeObject(forKey: "threadID") as! String?,
// let id = coder.decodeObject(forKey: "id") as! String? else { return nil }
// self.attachmentID = attachmentID
// self.tsMessageID = tsMessageID
// self.threadID = threadID
// self.id = id
// self.failureCount = coder.decodeObject(forKey: "failureCount") as! UInt? ?? 0
// self.isDeferred = coder.decodeBool(forKey: "isDeferred")
// }
//
// public func encode(with coder: NSCoder) {
// coder.encode(attachmentID, forKey: "attachmentID")
// coder.encode(tsMessageID, forKey: "tsIncomingMessageID")
// coder.encode(threadID, forKey: "threadID")
// coder.encode(id, forKey: "id")
// coder.encode(failureCount, forKey: "failureCount")
// coder.encode(isDeferred, forKey: "isDeferred")
// }
//
// // MARK: Running
// public func execute() {
// if let id = id {
// JobQueue.currentlyExecutingJobs.insert(id)
// }
// guard !isDeferred else { return }
// if TSAttachment.fetch(uniqueId: attachmentID) is TSAttachmentStream {
// // FIXME: It's not clear * how * this happens, but apparently we can get to this point
// // from time to time with an already downloaded attachment.
// return handleSuccess()
// }
// guard let pointer = TSAttachment.fetch(uniqueId: attachmentID) as? TSAttachmentPointer else {
// return handleFailure(error: Error.noAttachment)
// }
// let storage = SNMessagingKitConfiguration.shared.storage
// storage.write(with: { transaction in
// storage.setAttachmentState(to: .downloading, for: pointer, associatedWith: self.tsMessageID, using: transaction)
// }, completion: { })
// let temporaryFilePath = URL(fileURLWithPath: OWSTemporaryDirectoryAccessibleAfterFirstAuth() + UUID().uuidString)
// let handleFailure: (Swift.Error) -> Void = { error in // Intentionally capture self
// OWSFileSystem.deleteFile(temporaryFilePath.absoluteString)
// if let error = error as? Error, case .noAttachment = error {
// storage.write(with: { transaction in
// storage.setAttachmentState(to: .failed, for: pointer, associatedWith: self.tsMessageID, using: transaction)
// }, completion: { })
// self.handlePermanentFailure(error: error)
// } else if let error = error as? OnionRequestAPI.Error, case .httpRequestFailedAtDestination(let statusCode, _, _) = error,
// statusCode == 400 {
// // Otherwise, the attachment will show a state of downloading forever,
// // and the message won't be able to be marked as read.
// storage.write(with: { transaction in
// storage.setAttachmentState(to: .failed, for: pointer, associatedWith: self.tsMessageID, using: transaction)
// }, completion: { })
// // This usually indicates a file that has expired on the server, so there's no need to retry.
// self.handlePermanentFailure(error: error)
// } else {
// self.handleFailure(error: error)
// }
// }
// if let tsMessage = TSMessage.fetch(uniqueId: tsMessageID), let v2OpenGroup = storage.getV2OpenGroup(for: tsMessage.uniqueThreadId) {
// guard let fileAsString = pointer.downloadURL.split(separator: "/").last, let file = UInt64(fileAsString) else {
// return handleFailure(Error.invalidURL)
// }
// OpenGroupAPIV2.download(file, from: v2OpenGroup.room, on: v2OpenGroup.server).done(on: DispatchQueue.global(qos: .userInitiated)) { data in
// self.handleDownloadedAttachment(data: data, temporaryFilePath: temporaryFilePath, pointer: pointer, failureHandler: handleFailure)
// }.catch(on: DispatchQueue.global()) { error in
// handleFailure(error)
// }
// } else {
// guard let fileAsString = pointer.downloadURL.split(separator: "/").last, let file = UInt64(fileAsString) else {
// return handleFailure(Error.invalidURL)
// }
// let useOldServer = pointer.downloadURL.contains(FileServerAPIV2.oldServer)
// FileServerAPIV2.download(file, useOldServer: useOldServer).done(on: DispatchQueue.global(qos: .userInitiated)) { data in
// self.handleDownloadedAttachment(data: data, temporaryFilePath: temporaryFilePath, pointer: pointer, failureHandler: handleFailure)
// }.catch(on: DispatchQueue.global()) { error in
// handleFailure(error)
// }
// }
// }
//
// private func handleDownloadedAttachment(data: Data, temporaryFilePath: URL, pointer: TSAttachmentPointer, failureHandler: (Swift.Error) -> Void) {
// let storage = SNMessagingKitConfiguration.shared.storage
// do {
// try data.write(to: temporaryFilePath, options: .atomic)
// } catch {
// return failureHandler(error)
// }
// let plaintext: Data
// if let key = pointer.encryptionKey, let digest = pointer.digest, key.count > 0 && digest.count > 0 {
// do {
// plaintext = try Cryptography.decryptAttachment(data, withKey: key, digest: digest, unpaddedSize: pointer.byteCount)
// } catch {
// return failureHandler(error)
// }
// } else {
// plaintext = data // Open group attachments are unencrypted
// }
// let stream = TSAttachmentStream(pointer: pointer)
// do {
// try stream.write(plaintext)
// } catch {
// return failureHandler(error)
// }
// OWSFileSystem.deleteFile(temporaryFilePath.absoluteString)
// storage.write(with: { transaction in
// storage.persist(stream, associatedWith: self.tsMessageID, using: transaction)
// }, completion: {
// self.handleSuccess()
// })
// }
//
// private func handleSuccess() {
// delegate?.handleJobSucceeded(self)
// }
//
// private func handlePermanentFailure(error: Swift.Error) {
// delegate?.handleJobFailedPermanently(self, with: error)
// }
//
// private func handleFailure(error: Swift.Error) {
// delegate?.handleJobFailed(self, with: error)
// }
//}

View File

@ -0,0 +1,50 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import PromiseKit
import SignalCoreKit
import SessionUtilitiesKit
import SessionSnodeKit
public enum GarbageCollectionJob: JobExecutor {
public static var maxFailureCount: Int = 10
public static var requiresThreadId: Bool = true
public static let requiresInteractionId: Bool = false // Some messages don't have interactions
public static func run(
_ job: Job,
success: @escaping (Job, Bool) -> (),
failure: @escaping (Job, Error?, Bool) -> (),
deferred: @escaping (Job) -> ()
) {
guard
let detailsData: Data = job.details,
let details: Details = try? JSONDecoder().decode(Details.self, from: detailsData)
else {
failure(job, JobRunnerError.missingRequiredDetails, false)
return
}
failure(job, JobRunnerError.missingRequiredDetails, true)
}
}
// MARK: - GarbageCollectionJob.Details
extension GarbageCollectionJob {
public enum Types: Codable, CaseIterable {
case oldOpenGroupMessages
case expiredControlMessageProcessRecords
case threadTypingIndicators
case orphanedAttachmentFiles
}
public struct Details: Codable {
public let typesToCollect: [Types]
public init(typesToCollect: [Types] = Types.allCases) {
self.typesToCollect = typesToCollect
}
}
}

View File

@ -240,143 +240,3 @@ extension MessageSendJob {
}
}
}
// public let message: Message
// public let destination: Message.Destination
// public var delegate: JobDelegate?
// public var id: String?
// public var failureCount: UInt = 0
//
// // MARK: Settings
// public class var collection: String { return "MessageSendJobCollection" }
// public static let maxFailureCount: UInt = 10
//
// // MARK: Initialization
// @objc public convenience init(message: Message, publicKey: String) { self.init(message: message, destination: .contact(publicKey: publicKey)) }
// @objc public convenience init(message: Message, groupPublicKey: String) { self.init(message: message, destination: .closedGroup(groupPublicKey: groupPublicKey)) }
//
// public init(message: Message, destination: Message.Destination) {
// self.message = message
// self.destination = destination
// }
//
// // MARK: Coding
// public init?(coder: NSCoder) {
// guard let message = coder.decodeObject(forKey: "message") as! Message?,
// var rawDestination = coder.decodeObject(forKey: "destination") as! String?,
// let id = coder.decodeObject(forKey: "id") as! String? else { return nil }
// self.message = message
// if rawDestination.removePrefix("contact(") {
// guard rawDestination.removeSuffix(")") else { return nil }
// let publicKey = rawDestination
// destination = .contact(publicKey: publicKey)
// } else if rawDestination.removePrefix("closedGroup(") {
// guard rawDestination.removeSuffix(")") else { return nil }
// let groupPublicKey = rawDestination
// destination = .closedGroup(groupPublicKey: groupPublicKey)
// } else if rawDestination.removePrefix("openGroup(") {
// guard rawDestination.removeSuffix(")") else { return nil }
// let components = rawDestination.split(separator: ",").map { String($0).trimmingCharacters(in: .whitespacesAndNewlines) }
// guard components.count == 2, let channel = UInt64(components[0]) else { return nil }
// let server = components[1]
// destination = .openGroup(channel: channel, server: server)
// } else if rawDestination.removePrefix("openGroupV2(") {
// guard rawDestination.removeSuffix(")") else { return nil }
// let components = rawDestination.split(separator: ",").map { String($0).trimmingCharacters(in: .whitespacesAndNewlines) }
// guard components.count == 2 else { return nil }
// let room = components[0]
// let server = components[1]
// destination = .openGroupV2(room: room, server: server)
// } else {
// return nil
// }
// self.id = id
// self.failureCount = coder.decodeObject(forKey: "failureCount") as! UInt? ?? 0
// }
//
// public func encode(with coder: NSCoder) {
// coder.encode(message, forKey: "message")
// switch destination {
// case .contact(let publicKey): coder.encode("contact(\(publicKey))", forKey: "destination")
// case .closedGroup(let groupPublicKey): coder.encode("closedGroup(\(groupPublicKey))", forKey: "destination")
// case .openGroup(let channel, let server): coder.encode("openGroup(\(channel), \(server))", forKey: "destination")
// case .openGroupV2(let room, let server): coder.encode("openGroupV2(\(room), \(server))", forKey: "destination")
// }
// coder.encode(id, forKey: "id")
// coder.encode(failureCount, forKey: "failureCount")
// }
//
// // MARK: Running
// public func execute() {
// if let id = id {
// JobQueue.currentlyExecutingJobs.insert(id)
// }
// let storage = SNMessagingKitConfiguration.shared.storage
// if let message = message as? VisibleMessage {
// guard TSOutgoingMessage.find(withTimestamp: message.sentTimestamp!) != nil else { return } // The message has been deleted
// let attachments = message.attachmentIDs.compactMap { TSAttachment.fetch(uniqueId: $0) as? TSAttachmentStream }
// let attachmentsToUpload = attachments.filter { !$0.isUploaded }
// attachmentsToUpload.forEach { attachment in
// if storage.getAttachmentUploadJob(for: attachment.uniqueId!) != nil {
// // Wait for it to finish
// } else {
// let job = AttachmentUploadJob(attachmentID: attachment.uniqueId!, threadID: message.threadID!, message: message, messageSendJobID: id!)
// storage.write(with: { transaction in
// JobQueue.shared.add(job, using: transaction)
// }, completion: { })
// }
// }
// if !attachmentsToUpload.isEmpty { return } // Wait for all attachments to upload before continuing
// }
// storage.write(with: { transaction in // Intentionally capture self
// MessageSender.send(self.message, to: self.destination, using: transaction).done(on: DispatchQueue.global(qos: .userInitiated)) {
// self.handleSuccess()
// }.catch(on: DispatchQueue.global(qos: .userInitiated)) { error in
// SNLog("Couldn't send message due to error: \(error).")
// if let error = error as? MessageSender.Error, !error.isRetryable {
// self.handlePermanentFailure(error: error)
// } else if let error = error as? OnionRequestAPI.Error, case .httpRequestFailedAtDestination(let statusCode, _, _) = error,
// statusCode == 429 { // Rate limited
// self.handlePermanentFailure(error: error)
// } else {
// self.handleFailure(error: error)
// }
// }
// }, completion: { })
// }
//
// private func handleSuccess() {
// delegate?.handleJobSucceeded(self)
// }
//
// private func handlePermanentFailure(error: Error) {
// delegate?.handleJobFailedPermanently(self, with: error)
// }
//
// private func handleFailure(error: Error) {
// SNLog("Failed to send \(type(of: message)).")
// if let message = message as? VisibleMessage {
// guard TSOutgoingMessage.find(withTimestamp: message.sentTimestamp!) != nil else { return } // The message has been deleted
// }
// delegate?.handleJobFailed(self, with: error)
// }
//}
//
//// MARK: Convenience
//private extension String {
//
// @discardableResult
// mutating func removePrefix<T : StringProtocol>(_ prefix: T) -> Bool {
// guard hasPrefix(prefix) else { return false }
// removeFirst(prefix.count)
// return true
// }
//
// @discardableResult
// mutating func removeSuffix<T : StringProtocol>(_ suffix: T) -> Bool {
// guard hasSuffix(suffix) else { return false }
// removeLast(suffix.count)
// return true
// }
//}
//

View File

@ -7,58 +7,43 @@ import SessionUtilitiesKit
extension ConfigurationMessage {
public static func getCurrent(_ db: Database) throws -> ConfigurationMessage {
let profile: Profile = Profile.fetchOrCreateCurrentUser(db)
let displayName: String = profile.name
let profilePictureUrl: String? = profile.profilePictureUrl
let profileKey: Data? = profile.profileEncryptionKey?.keyData
var closedGroups: Set<CMClosedGroup> = []
var openGroups: Set<String> = []
Storage.read { transaction in
TSGroupThread.enumerateCollectionObjects(with: transaction) { object, _ in
guard let thread = object as? TSGroupThread else { return }
switch thread.groupModel.groupType {
case .closedGroup:
guard thread.isCurrentUserMemberInGroup() else { return }
let groupID = thread.groupModel.groupId
let groupPublicKey = LKGroupUtilities.getDecodedGroupID(groupID)
guard
Storage.shared.isClosedGroup(groupPublicKey, using: transaction),
let encryptionKeyPair = Storage.shared.getLatestClosedGroupEncryptionKeyPair(for: groupPublicKey, using: transaction)
else {
return
}
let closedGroup = ClosedGroup(
publicKey: groupPublicKey,
name: (thread.groupModel.groupName ?? ""),
encryptionKeyPair: encryptionKeyPair,
members: Set(thread.groupModel.groupMemberIds),
admins: Set(thread.groupModel.groupAdminIds),
expirationTimer: thread.disappearingMessagesDuration(with: transaction)
)
closedGroups.insert(closedGroup)
case .openGroup:
if let threadId: String = thread.uniqueId, let v2OpenGroup = Storage.shared.getV2OpenGroup(for: threadId) {
openGroups.insert("\(v2OpenGroup.server)/\(v2OpenGroup.room)?public_key=\(v2OpenGroup.publicKey)")
}
default: break
let currentUserProfile: Profile = Profile.fetchOrCreateCurrentUser(db)
let displayName: String = currentUserProfile.name
let profilePictureUrl: String? = currentUserProfile.profilePictureUrl
let profileKey: Data? = currentUserProfile.profileEncryptionKey?.keyData
let closedGroups: Set<CMClosedGroup> = try ClosedGroup.fetchAll(db)
.compactMap { closedGroup -> CMClosedGroup? in
guard let latestKeyPair: ClosedGroupKeyPair = try closedGroup.fetchLatestKeyPair(db) else {
return nil
}
}
}
let currentUserPublicKey: String = getUserHexEncodedPublicKey()
let contacts: Set<CMContact> = try Contact.fetchAll(db)
.compactMap { contact -> CMContact? in
guard contact.id != currentUserPublicKey else { return nil }
return CMClosedGroup(
publicKey: closedGroup.publicKey,
name: closedGroup.name,
encryptionKeyPublicKey: latestKeyPair.publicKey,
encryptionKeySecretKey: latestKeyPair.secretKey,
members: try closedGroup.members
.select(GroupMember.Columns.profileId)
.asRequest(of: String.self)
.fetchSet(db),
admins: try closedGroup.admins
.select(GroupMember.Columns.profileId)
.asRequest(of: String.self)
.fetchSet(db),
expirationTimer: (try? DisappearingMessagesConfiguration
.fetchOne(db, id: closedGroup.threadId)
.map { ($0.isEnabled ? UInt32($0.durationSeconds) : 0) })
.defaulting(to: 0)
)
}
.asSet()
let openGroups: Set<String> = try OpenGroup.fetchAll(db)
.map { "\($0.server)/\($0.room)?public_key=\($0.publicKey)" }
.asSet()
let contacts: Set<CMContact> = try Contact
.filter(Contact.Columns.id != currentUserProfile.id)
.fetchAll(db)
.map { contact -> CMContact in
// Can just default the 'hasX' values to true as they will be set to this
// when converting to proto anyway
let profile: Profile? = try? Profile.fetchOne(db, id: contact.id)

View File

@ -4,7 +4,7 @@ import Foundation
import GRDB
import SessionUtilitiesKit
public final class DataExtractionNotification : ControlMessage {
public final class DataExtractionNotification: ControlMessage {
private enum CodingKeys: String, CodingKey {
case kind
}
@ -15,7 +15,7 @@ public final class DataExtractionNotification : ControlMessage {
public enum Kind: CustomStringConvertible, Codable {
case screenshot
case mediaSaved(timestamp: UInt64)
case mediaSaved(timestamp: UInt64) // Note: The 'timestamp' should the original message timestamp
public var description: String {
switch self {

View File

@ -55,7 +55,6 @@ typedef NS_ENUM(NSInteger, TSGroupMetaMessage) {
@class SNProtoContentBuilder;
@class SNProtoDataMessage;
@class SNProtoDataMessageBuilder;
@class SignalRecipient;
@interface TSOutgoingMessageRecipientState : MTLModel

View File

@ -7,10 +7,7 @@
#import "TSOutgoingMessage.h"
#import "TSDatabaseSecondaryIndexes.h"
#import "OWSPrimaryStorage.h"
#import "ProfileManagerProtocol.h"
#import "ProtoUtils.h"
#import "SSKEnvironment.h"
#import "SignalRecipient.h"
#import "TSAccountManager.h"
#import "TSAttachmentStream.h"
#import "TSContactThread.h"

View File

@ -11,7 +11,6 @@ FOUNDATION_EXPORT const unsigned char SessionMessagingKitVersionString[];
#import <SessionMessagingKit/OWSBackupFragment.h>
#import <SessionMessagingKit/OWSDisappearingMessagesFinder.h>
#import <SessionMessagingKit/OWSIncomingMessageFinder.h>
#import <SessionMessagingKit/OWSMediaGalleryFinder.h>
#import <SessionMessagingKit/OWSPreferences.h>
#import <SessionMessagingKit/OWSPrimaryStorage.h>
#import <SessionMessagingKit/OWSQuotedReplyModel.h>
@ -20,8 +19,6 @@ FOUNDATION_EXPORT const unsigned char SessionMessagingKitVersionString[];
#import <SessionMessagingKit/OWSStorage+Subclass.h>
#import <SessionMessagingKit/OWSUserProfile.h>
#import <SessionMessagingKit/OWSWindowManager.h>
#import <SessionMessagingKit/ProtoUtils.h>
#import <SessionMessagingKit/SignalRecipient.h>
#import <SessionMessagingKit/SSKEnvironment.h>
#import <SessionMessagingKit/TSAccountManager.h>
#import <SessionMessagingKit/TSAttachment.h>

View File

@ -1,9 +1,11 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import AFNetworking
import GRDB
import Foundation
import PromiseKit
import SignalCoreKit
import SessionUtilitiesKit
@objc
public enum LinkPreviewError: Int, Error {
@ -329,7 +331,7 @@ public class OWSLinkPreview: MTLModel {
return nil
}
guard SSKPreferences.areLinkPreviewsEnabled else {
guard GRDBStorage.shared[.areLinkPreviewsEnabled] else {
return nil
}
@ -425,28 +427,19 @@ public class OWSLinkPreview: MTLModel {
// Exit early if link previews are not enabled in order to avoid
// tainting the cache.
guard OWSLinkPreview.featureEnabled else {
return
}
guard SSKPreferences.areLinkPreviewsEnabled else {
return
}
guard OWSLinkPreview.featureEnabled else { return }
guard GRDBStorage.shared[.areLinkPreviewsEnabled] else { return }
serialQueue.sync {
linkPreviewDraftCache = linkPreviewDraft
}
}
@objc
public class func tryToBuildPreviewInfoObjc(previewUrl: String?) -> AnyPromise {
return AnyPromise(tryToBuildPreviewInfo(previewUrl: previewUrl))
}
public class func tryToBuildPreviewInfo(previewUrl: String?) -> Promise<OWSLinkPreviewDraft> {
guard OWSLinkPreview.featureEnabled else {
return Promise(error: LinkPreviewError.featureDisabled)
}
guard SSKPreferences.areLinkPreviewsEnabled else {
guard GRDBStorage.shared[.areLinkPreviewsEnabled] else {
return Promise(error: LinkPreviewError.featureDisabled)
}
guard let previewUrl = previewUrl else {

View File

@ -186,7 +186,7 @@ public final class Poller : NSObject {
}
}
SNLog("Received \(messageCount) message(s).")
SNLog("Received \(messageCount) new message\(messageCount == 1 ? "" : "s") (\(messages.count - messageCount) duplicates)")
}
self?.pollCount += 1
@ -196,7 +196,9 @@ public final class Poller : NSObject {
}
return withDelay(Poller.pollInterval, completionQueue: Threading.pollerQueue) {
guard let strongSelf = self, strongSelf.isPolling.wrappedValue else { return Promise { $0.fulfill(()) } }
guard let strongSelf = self, strongSelf.isPolling.wrappedValue else {
return Promise { $0.fulfill(()) }
}
return strongSelf.poll(snode, seal: longTermSeal)
}

View File

@ -8,7 +8,6 @@ NS_ASSUME_NONNULL_BEGIN
BOOL IsNoteToSelfEnabled(void);
@class OWSDisappearingMessagesConfiguration;
@class TSInteraction;
/**

View File

@ -1,30 +0,0 @@
////
//// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
////
//
//@class OWSAES256Key;
//@class TSThread;
//@class YapDatabaseReadWriteTransaction;
//@class SNContact;
//
//NS_ASSUME_NONNULL_BEGIN
//
//@protocol ProfileManagerProtocol <NSObject>
//
//#pragma mark - Local Profile
//
//- (void)updateServiceWithProfileName:(nullable NSString *)localProfileName avatarURL:(nullable NSString *)avatarURL;
//
//#pragma mark - Other User's Profiles
//
//- (nullable NSData *)profileKeyDataForRecipientId:(NSString *)recipientId;
//- (void)setProfileKeyData:(NSData *)profileKeyData forRecipientId:(NSString *)recipientId;
//- (void)setProfileKeyData:(NSData *)profileKeyData forRecipientId:(NSString *)recipientId avatarURL:(nullable NSString *)avatarURL;
//
//#pragma mark - Other
//
//- (void)downloadAvatarForUserProfile:(SNContact *)userProfile;
//
//@end
//
//NS_ASSUME_NONNULL_END

View File

@ -1,50 +0,0 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import <SessionUtilitiesKit/TSYapDatabaseObject.h>
NS_ASSUME_NONNULL_BEGIN
// SignalRecipient serves two purposes:
//
// a) It serves as a cache of "known" Signal accounts. When the service indicates
// that an account exists, we make sure that an instance of SignalRecipient exists
// for that recipient id (using mark as registered) and has at least one device.
// When the service indicates that an account does not exist, we remove any devices
// from that SignalRecipient - but do not remove it from the database.
// Note that SignalRecipients without any devices are not considered registered.
//// b) We hang the "known device list" for known signal accounts on this entity.
@interface SignalRecipient : TSYapDatabaseObject
@property (nonatomic, readonly) NSOrderedSet *devices;
- (instancetype)init NS_UNAVAILABLE;
+ (nullable instancetype)registeredRecipientForRecipientId:(NSString *)recipientId
mustHaveDevices:(BOOL)mustHaveDevices
transaction:(YapDatabaseReadTransaction *)transaction;
+ (instancetype)getOrBuildUnsavedRecipientForRecipientId:(NSString *)recipientId
transaction:(YapDatabaseReadTransaction *)transaction;
- (void)updateRegisteredRecipientWithDevicesToAdd:(nullable NSArray *)devicesToAdd
devicesToRemove:(nullable NSArray *)devicesToRemove
transaction:(YapDatabaseReadWriteTransaction *)transaction;
- (NSString *)recipientId;
- (NSComparisonResult)compare:(SignalRecipient *)other;
+ (BOOL)isRegisteredRecipient:(NSString *)recipientId transaction:(YapDatabaseReadTransaction *)transaction;
+ (SignalRecipient *)markRecipientAsRegisteredAndGet:(NSString *)recipientId
transaction:(YapDatabaseReadWriteTransaction *)transaction;
+ (void)markRecipientAsRegistered:(NSString *)recipientId
deviceId:(UInt32)deviceId
transaction:(YapDatabaseReadWriteTransaction *)transaction;
+ (void)markRecipientAsUnregistered:(NSString *)recipientId transaction:(YapDatabaseReadWriteTransaction *)transaction;
@end
NS_ASSUME_NONNULL_END

View File

@ -1,217 +0,0 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "SignalRecipient.h"
#import "ProfileManagerProtocol.h"
#import "SSKEnvironment.h"
#import "TSAccountManager.h"
#import <YapDatabase/YapDatabaseConnection.h>
NS_ASSUME_NONNULL_BEGIN
@interface SignalRecipient ()
@property (nonatomic) NSOrderedSet *devices;
@end
#pragma mark -
@implementation SignalRecipient
#pragma mark - Dependencies
- (id<ProfileManagerProtocol>)profileManager
{
return SSKEnvironment.shared.profileManager;
}
- (TSAccountManager *)tsAccountManager
{
return SSKEnvironment.shared.tsAccountManager;
}
#pragma mark -
+ (instancetype)getOrBuildUnsavedRecipientForRecipientId:(NSString *)recipientId
transaction:(YapDatabaseReadTransaction *)transaction
{
SignalRecipient *_Nullable recipient =
[self registeredRecipientForRecipientId:recipientId mustHaveDevices:NO transaction:transaction];
if (!recipient) {
recipient = [[self alloc] initWithTextSecureIdentifier:recipientId];
}
return recipient;
}
- (instancetype)initWithTextSecureIdentifier:(NSString *)textSecureIdentifier
{
self = [super initWithUniqueId:textSecureIdentifier];
if (!self) {
return self;
}
_devices = [NSOrderedSet orderedSetWithObject:@(1)];
return self;
}
- (nullable instancetype)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (!self) {
return self;
}
if (_devices == nil) {
_devices = [NSOrderedSet new];
}
// Since we use device count to determine whether a user is registered or not,
// ensure the local user always has at least *this* device.
if (![_devices containsObject:@(1)]) {
if ([self.uniqueId isEqualToString:self.tsAccountManager.localNumber]) {
[self addDevices:[NSSet setWithObject:@(1)]];
}
}
return self;
}
+ (nullable instancetype)registeredRecipientForRecipientId:(NSString *)recipientId
mustHaveDevices:(BOOL)mustHaveDevices
transaction:(YapDatabaseReadTransaction *)transaction
{
SignalRecipient *_Nullable signalRecipient = [self fetchObjectWithUniqueID:recipientId transaction:transaction];
if (mustHaveDevices && signalRecipient.devices.count < 1) {
return nil;
}
return signalRecipient;
}
- (void)addDevices:(NSSet *)devices
{
NSMutableOrderedSet *updatedDevices = [self.devices mutableCopy];
[updatedDevices unionSet:devices];
self.devices = [updatedDevices copy];
}
- (void)removeDevices:(NSSet *)devices
{
NSMutableOrderedSet *updatedDevices = [self.devices mutableCopy];
[updatedDevices minusSet:devices];
self.devices = [updatedDevices copy];
}
- (void)updateRegisteredRecipientWithDevicesToAdd:(nullable NSArray *)devicesToAdd
devicesToRemove:(nullable NSArray *)devicesToRemove
transaction:(YapDatabaseReadWriteTransaction *)transaction {
// Add before we remove, since removeDevicesFromRecipient:...
// can markRecipientAsUnregistered:... if the recipient has
// no devices left.
if (devicesToAdd.count > 0) {
[self addDevicesToRegisteredRecipient:[NSSet setWithArray:devicesToAdd] transaction:transaction];
}
if (devicesToRemove.count > 0) {
[self removeDevicesFromRecipient:[NSSet setWithArray:devicesToRemove] transaction:transaction];
}
}
- (void)addDevicesToRegisteredRecipient:(NSSet *)devices transaction:(YapDatabaseReadWriteTransaction *)transaction
{
[self reloadWithTransaction:transaction];
[self addDevices:devices];
[self saveWithTransaction_internal:transaction];
}
- (void)removeDevicesFromRecipient:(NSSet *)devices transaction:(YapDatabaseReadWriteTransaction *)transaction
{
[self reloadWithTransaction:transaction ignoreMissing:YES];
[self removeDevices:devices];
[self saveWithTransaction_internal:transaction];
}
- (NSString *)recipientId
{
return self.uniqueId;
}
- (NSComparisonResult)compare:(SignalRecipient *)other
{
return [self.recipientId compare:other.recipientId];
}
- (void)removeWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
{
// We need to distinguish between "users we know to be unregistered" and
// "users whose registration status is unknown". The former correspond to
// instances of SignalRecipient with no devices. The latter do not
// correspond to an instance of SignalRecipient in the database (although
// they may correspond to an "unsaved" instance of SignalRecipient built
// by getOrBuildUnsavedRecipientForRecipientId.
[super removeWithTransaction:transaction];
}
- (void)saveWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
{
// We only want to mutate the persisted SignalRecipients in the database
// using other methods of this class, e.g. markRecipientAsRegistered...
// to create, addDevices and removeDevices to mutate. We're trying to
// be strict about using persisted SignalRecipients as a cache to
// reflect "last known registration status". Forcing our codebase to
// use those methods helps ensure that we update the cache deliberately.
[self saveWithTransaction_internal:transaction];
}
- (void)saveWithTransaction_internal:(YapDatabaseReadWriteTransaction *)transaction
{
[super saveWithTransaction:transaction];
}
+ (BOOL)isRegisteredRecipient:(NSString *)recipientId transaction:(YapDatabaseReadTransaction *)transaction
{
return nil != [self registeredRecipientForRecipientId:recipientId mustHaveDevices:YES transaction:transaction];
}
+ (SignalRecipient *)markRecipientAsRegisteredAndGet:(NSString *)recipientId
transaction:(YapDatabaseReadWriteTransaction *)transaction
{
SignalRecipient *_Nullable instance =
[self registeredRecipientForRecipientId:recipientId mustHaveDevices:YES transaction:transaction];
if (!instance) {
instance = [[self alloc] initWithTextSecureIdentifier:recipientId];
[instance saveWithTransaction_internal:transaction];
}
return instance;
}
+ (void)markRecipientAsRegistered:(NSString *)recipientId
deviceId:(UInt32)deviceId
transaction:(YapDatabaseReadWriteTransaction *)transaction
{
SignalRecipient *recipient = [self markRecipientAsRegisteredAndGet:recipientId transaction:transaction];
if (![recipient.devices containsObject:@(deviceId)]) {
[recipient addDevices:[NSSet setWithObject:@(deviceId)]];
[recipient saveWithTransaction_internal:transaction];
}
}
+ (void)markRecipientAsUnregistered:(NSString *)recipientId transaction:(YapDatabaseReadWriteTransaction *)transaction
{
SignalRecipient *instance = [self getOrBuildUnsavedRecipientForRecipientId:recipientId
transaction:transaction];
if (instance.devices.count > 0) {
[instance removeDevices:instance.devices.set];
}
[instance saveWithTransaction_internal:transaction];
}
@end
NS_ASSUME_NONNULL_END

View File

@ -6,7 +6,6 @@
#import "AppContext.h"
#import "AppReadiness.h"
#import "NSNotificationCenter+OWS.h"
#import "ProfileManagerProtocol.h"
#import "SSKEnvironment.h"
#import "YapDatabaseConnection+OWS.h"
#import "YapDatabaseTransaction+OWS.h"

View File

@ -1,30 +0,0 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
/// A message is invalidated when it needs to be re-rendered in the UI. Examples of when this happens include:
///
/// When the sent or read status of a message is updated.
/// When an attachment is uploaded or downloaded.
@objc public final class MessageInvalidator : NSObject {
private static var invalidatedMessages: Set<String> = []
@objc public static let shared = MessageInvalidator()
private override init() { }
@objc public static func invalidate(_ message: TSMessage, with transaction: YapDatabaseReadWriteTransaction) {
guard let id = message.uniqueId else { return }
invalidatedMessages.insert(id)
message.touch(with: transaction)
}
@objc public static func isInvalidated(_ message: TSMessage) -> Bool {
guard let id = message.uniqueId else { return false }
return invalidatedMessages.contains(id)
}
@objc public static func markAsUpdated(_ id: String) {
invalidatedMessages.remove(id)
}
}

View File

@ -46,7 +46,7 @@ typedef NS_ENUM(NSUInteger, OWSAudioBehavior) {
@property (nonatomic) NSTimeInterval duration;
- (instancetype)initWithMediaUrl:(NSURL *)mediaUrl audioBehavior:(OWSAudioBehavior)audioBehavior;
- (instancetype)initWithMediaUrl:(NSURL *)mediaUrl audioBehavior:(OWSAudioBehavior)audioBehavior delegate:(id<OWSAudioPlayerDelegate>)delegate;
- (instancetype)initWithMediaUrl:(NSURL *)mediaUrl audioBehavior:(OWSAudioBehavior)audioBehavior delegate:(nullable id<OWSAudioPlayerDelegate>)delegate;
- (void)play;
- (void)setCurrentTime:(NSTimeInterval)currentTime;
- (void)pause;

View File

@ -61,7 +61,7 @@ NS_ASSUME_NONNULL_BEGIN
- (instancetype)initWithMediaUrl:(NSURL *)mediaUrl
audioBehavior:(OWSAudioBehavior)audioBehavior
delegate:(id<OWSAudioPlayerDelegate>)delegate
delegate:(nullable id<OWSAudioPlayerDelegate>)delegate
{
self = [super init];
if (!self) {

View File

@ -1,54 +0,0 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@class OWSStorage;
@class TSAttachment;
@class TSThread;
@class YapDatabaseAutoViewTransaction;
@class YapDatabaseConnection;
@class YapDatabaseReadTransaction;
@class YapDatabaseViewRowChange;
@interface OWSMediaGalleryFinder : NSObject
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithThread:(TSThread *)thread NS_DESIGNATED_INITIALIZER;
// How many media items a thread has
- (NSUInteger)mediaCountWithTransaction:(YapDatabaseReadTransaction *)transaction NS_SWIFT_NAME(mediaCount(transaction:));
// The ordinal position of an attachment within a thread's media gallery
- (nullable NSNumber *)mediaIndexForAttachment:(TSAttachment *)attachment
transaction:(YapDatabaseReadTransaction *)transaction
NS_SWIFT_NAME(mediaIndex(attachment:transaction:));
- (nullable TSAttachment *)oldestMediaAttachmentWithTransaction:(YapDatabaseReadTransaction *)transaction
NS_SWIFT_NAME(oldestMediaAttachment(transaction:));
- (nullable TSAttachment *)mostRecentMediaAttachmentWithTransaction:(YapDatabaseReadTransaction *)transaction
NS_SWIFT_NAME(mostRecentMediaAttachment(transaction:));
- (void)enumerateMediaAttachmentsWithRange:(NSRange)range
transaction:(YapDatabaseReadTransaction *)transaction
block:(void (^)(TSAttachment *))attachmentBlock
NS_SWIFT_NAME(enumerateMediaAttachments(range:transaction:block:));
- (BOOL)hasMediaChangesInNotifications:(NSArray<NSNotification *> *)notifications
dbConnection:(YapDatabaseConnection *)dbConnection;
#pragma mark - Extension registration
@property (nonatomic, readonly) NSString *mediaGroup;
- (YapDatabaseAutoViewTransaction *)galleryExtensionWithTransaction:(YapDatabaseReadTransaction *)transaction
NS_SWIFT_NAME(galleryExtension(transaction:));
+ (NSString *)databaseExtensionName;
+ (void)asyncRegisterDatabaseExtensionsWithPrimaryStorage:(OWSStorage *)storage;
@end
NS_ASSUME_NONNULL_END

View File

@ -1,209 +0,0 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
#import "OWSMediaGalleryFinder.h"
#import "OWSStorage.h"
#import "TSAttachmentStream.h"
#import "TSMessage.h"
#import "TSThread.h"
#import <YapDatabase/YapDatabaseAutoView.h>
#import <YapDatabase/YapDatabaseTransaction.h>
#import <YapDatabase/YapDatabaseViewTypes.h>
#import <YapDatabase/YapWhitelistBlacklist.h>
#import "TSAttachment.h"
NS_ASSUME_NONNULL_BEGIN
static NSString *const OWSMediaGalleryFinderExtensionName = @"OWSMediaGalleryFinderExtensionName";
@interface OWSMediaGalleryFinder ()
@property (nonatomic, readonly) TSThread *thread;
@end
@implementation OWSMediaGalleryFinder
- (instancetype)initWithThread:(TSThread *)thread
{
self = [super init];
if (!self) {
return self;
}
_thread = thread;
return self;
}
#pragma mark - Public Finder Methods
- (NSUInteger)mediaCountWithTransaction:(YapDatabaseReadTransaction *)transaction
{
return [[self galleryExtensionWithTransaction:transaction] numberOfItemsInGroup:self.mediaGroup];
}
- (nullable NSNumber *)mediaIndexForAttachment:(TSAttachment *)attachment
transaction:(YapDatabaseReadTransaction *)transaction
{
NSString *groupId;
NSUInteger index;
BOOL wasFound = [[self galleryExtensionWithTransaction:transaction] getGroup:&groupId
index:&index
forKey:attachment.uniqueId
inCollection:[TSAttachment collection]];
if (!wasFound) {
return nil;
}
return @(index);
}
- (nullable TSAttachment *)oldestMediaAttachmentWithTransaction:(YapDatabaseReadTransaction *)transaction
{
return [[self galleryExtensionWithTransaction:transaction] firstObjectInGroup:self.mediaGroup];
}
- (nullable TSAttachment *)mostRecentMediaAttachmentWithTransaction:(YapDatabaseReadTransaction *)transaction
{
return [[self galleryExtensionWithTransaction:transaction] lastObjectInGroup:self.mediaGroup];
}
- (void)enumerateMediaAttachmentsWithRange:(NSRange)range
transaction:(YapDatabaseReadTransaction *)transaction
block:(void (^)(TSAttachment *))attachmentBlock
{
[[self galleryExtensionWithTransaction:transaction]
enumerateKeysAndObjectsInGroup:self.mediaGroup
withOptions:0
range:range
usingBlock:^(NSString *_Nonnull collection,
NSString *_Nonnull key,
id _Nonnull object,
NSUInteger index,
BOOL *_Nonnull stop) {
attachmentBlock((TSAttachment *)object);
}];
}
- (BOOL)hasMediaChangesInNotifications:(NSArray<NSNotification *> *)notifications
dbConnection:(YapDatabaseConnection *)dbConnection
{
YapDatabaseAutoViewConnection *extConnection = [dbConnection ext:OWSMediaGalleryFinderExtensionName];
return [extConnection hasChangesForGroup:self.mediaGroup inNotifications:notifications];
}
#pragma mark - Util
- (YapDatabaseAutoViewTransaction *)galleryExtensionWithTransaction:(YapDatabaseReadTransaction *)transaction
{
YapDatabaseAutoViewTransaction *extension = [transaction extension:OWSMediaGalleryFinderExtensionName];
return extension;
}
+ (NSString *)mediaGroupWithThreadId:(NSString *)threadId
{
return [NSString stringWithFormat:@"%@-media", threadId];
}
- (NSString *)mediaGroup
{
return [[self class] mediaGroupWithThreadId:self.thread.uniqueId];
}
#pragma mark - Extension registration
+ (NSString *)databaseExtensionName
{
return OWSMediaGalleryFinderExtensionName;
}
+ (void)asyncRegisterDatabaseExtensionsWithPrimaryStorage:(OWSStorage *)storage
{
[storage asyncRegisterExtension:[self mediaGalleryDatabaseExtension]
withName:OWSMediaGalleryFinderExtensionName];
}
+ (YapDatabaseAutoView *)mediaGalleryDatabaseExtension
{
YapDatabaseViewSorting *sorting =
[YapDatabaseViewSorting withObjectBlock:^NSComparisonResult(YapDatabaseReadTransaction *_Nonnull transaction,
NSString *_Nonnull group,
NSString *_Nonnull collection1,
NSString *_Nonnull key1,
id _Nonnull object1,
NSString *_Nonnull collection2,
NSString *_Nonnull key2,
id _Nonnull object2) {
if (![object1 isKindOfClass:[TSAttachment class]]) {
return NSOrderedSame;
}
TSAttachment *attachment1 = (TSAttachment *)object1;
if (![object2 isKindOfClass:[TSAttachment class]]) {
return NSOrderedSame;
}
TSAttachment *attachment2 = (TSAttachment *)object2;
TSMessage *_Nullable message1 = [attachment1 fetchAlbumMessageWithTransaction:transaction];
TSMessage *_Nullable message2 = [attachment2 fetchAlbumMessageWithTransaction:transaction];
if (message1 == nil || message2 == nil) {
return NSOrderedSame;
}
if ([message1.uniqueId isEqualToString:message2.uniqueId]) {
NSUInteger index1 = [message1.attachmentIds indexOfObject:attachment1.uniqueId];
NSUInteger index2 = [message1.attachmentIds indexOfObject:attachment2.uniqueId];
if (index1 == NSNotFound || index2 == NSNotFound) {
return NSOrderedSame;
}
return [@(index1) compare:@(index2)];
} else {
return [message1 compareForSorting:message2];
}
}];
YapDatabaseViewGrouping *grouping =
[YapDatabaseViewGrouping withObjectBlock:^NSString *_Nullable(YapDatabaseReadTransaction *_Nonnull transaction,
NSString *_Nonnull collection,
NSString *_Nonnull key,
id _Nonnull object) {
// Don't include nil or not yet downloaded attachments.
if (![object isKindOfClass:[TSAttachmentStream class]]) {
return nil;
}
TSAttachmentStream *attachment = (TSAttachmentStream *)object;
if (attachment.albumMessageId == nil) {
return nil;
}
if (!attachment.isValidVisualMedia) {
return nil;
}
TSMessage *message = [attachment fetchAlbumMessageWithTransaction:transaction];
if (message == nil) {
return nil;
}
return [self mediaGroupWithThreadId:message.uniqueThreadId];
}];
YapDatabaseViewOptions *options = [YapDatabaseViewOptions new];
options.allowedCollections =
[[YapWhitelistBlacklist alloc] initWithWhitelist:[NSSet setWithObject:TSAttachment.collection]];
return [[YapDatabaseAutoView alloc] initWithGrouping:grouping sorting:sorting versionTag:@"4" options:options];
}
@end
NS_ASSUME_NONNULL_END

View File

@ -44,20 +44,6 @@ extern NSString *const OWSPreferencesCallLoggingDidChangeNotification;
- (BOOL)hasSentAMessage;
- (void)setHasSentAMessage:(BOOL)enabled;
+ (BOOL)isLoggingEnabled;
+ (void)setIsLoggingEnabled:(BOOL)flag;
- (BOOL)screenSecurityIsEnabled;
- (void)setScreenSecurity:(BOOL)flag;
- (NotificationType)notificationPreviewType;
- (NotificationType)notificationPreviewTypeWithTransaction:(YapDatabaseReadTransaction *)transaction;
- (void)setNotificationPreviewType:(NotificationType)type;
- (NSString *)nameForNotificationPreviewType:(NotificationType)notificationType;
- (BOOL)soundInForeground;
- (void)setSoundInForeground:(BOOL)enabled;
- (BOOL)hasDeclinedNoContactsView;
- (void)setHasDeclinedNoContactsView:(BOOL)value;
@ -95,16 +81,6 @@ extern NSString *const OWSPreferencesCallLoggingDidChangeNotification;
- (BOOL)doCallsHideIPAddress;
- (void)setDoCallsHideIPAddress:(BOOL)flag;
#pragma mark - Push Tokens
- (void)setPushToken:(NSString *)value;
- (nullable NSString *)getPushToken;
- (void)setVoipToken:(NSString *)value;
- (nullable NSString *)getVoipToken;
- (void)unsetRecordedAPNSTokens;
@end
NS_ASSUME_NONNULL_END

View File

@ -8,13 +8,7 @@ NS_ASSUME_NONNULL_BEGIN
NSString *const OWSPreferencesSignalDatabaseCollection = @"SignalPreferences";
NSString *const OWSPreferencesCallLoggingDidChangeNotification = @"OWSPreferencesCallLoggingDidChangeNotification";
NSString *const OWSPreferencesKeyScreenSecurity = @"Screen Security Key";
NSString *const OWSPreferencesKeyEnableDebugLog = @"Debugging Log Enabled Key";
NSString *const OWSPreferencesKeyNotificationPreviewType = @"Notification Preview Type Key";
NSString *const OWSPreferencesKeyHasSentAMessage = @"User has sent a message";
NSString *const OWSPreferencesKeyPlaySoundInForeground = @"NotificationSoundInForeground";
NSString *const OWSPreferencesKeyLastRecordedPushToken = @"LastRecordedPushToken";
NSString *const OWSPreferencesKeyLastRecordedVoipToken = @"LastRecordedVoipToken";
NSString *const OWSPreferencesKeyCallKitEnabled = @"CallKitEnabled";
NSString *const OWSPreferencesKeyCallKitPrivacyEnabled = @"CallKitPrivacyEnabled";
NSString *const OWSPreferencesKeyCallsHideIPAddress = @"CallsHideIPAddress";
@ -92,17 +86,6 @@ NSString *const OWSPreferencesKeySystemCallLogEnabled = @"OWSPreferencesKeySyste
[NSUserDefaults.appUserDefaults synchronize];
}
- (BOOL)screenSecurityIsEnabled
{
NSNumber *preference = [self tryGetValueForKey:OWSPreferencesKeyScreenSecurity];
return preference ? [preference boolValue] : YES;
}
- (void)setScreenSecurity:(BOOL)flag
{
[self setValueForKey:OWSPreferencesKeyScreenSecurity toValue:@(flag)];
}
- (BOOL)hasSentAMessage
{
NSNumber *preference = [self tryGetValueForKey:OWSPreferencesKeyHasSentAMessage];
@ -113,26 +96,6 @@ NSString *const OWSPreferencesKeySystemCallLogEnabled = @"OWSPreferencesKeySyste
}
}
+ (BOOL)isLoggingEnabled
{
NSNumber *preference = [NSUserDefaults.appUserDefaults objectForKey:OWSPreferencesKeyEnableDebugLog];
if (preference) {
return [preference boolValue];
} else {
return YES;
}
}
+ (void)setIsLoggingEnabled:(BOOL)flag
{
// Logging preferences are stored in UserDefaults instead of the database, so that we can (optionally) start
// logging before the database is initialized. This is important because sometimes there are problems *with* the
// database initialization, and without logging it would be hard to track down.
[NSUserDefaults.appUserDefaults setObject:@(flag) forKey:OWSPreferencesKeyEnableDebugLog];
[NSUserDefaults.appUserDefaults synchronize];
}
- (void)setHasSentAMessage:(BOOL)enabled
{
[self setValueForKey:OWSPreferencesKeyHasSentAMessage toValue:@(enabled)];
@ -335,92 +298,6 @@ NSString *const OWSPreferencesKeySystemCallLogEnabled = @"OWSPreferencesKeySyste
[self setValueForKey:OWSPreferencesKeyCallsHideIPAddress toValue:@(flag)];
}
#pragma mark Notification Preferences
- (BOOL)soundInForeground
{
NSNumber *preference = [self tryGetValueForKey:OWSPreferencesKeyPlaySoundInForeground];
if (preference) {
return [preference boolValue];
} else {
return YES;
}
}
- (void)setSoundInForeground:(BOOL)enabled
{
[self setValueForKey:OWSPreferencesKeyPlaySoundInForeground toValue:@(enabled)];
}
- (void)setNotificationPreviewType:(NotificationType)type
{
[self setValueForKey:OWSPreferencesKeyNotificationPreviewType toValue:@(type)];
}
- (NotificationType)notificationPreviewType
{
NSNumber *preference = [self tryGetValueForKey:OWSPreferencesKeyNotificationPreviewType];
if (preference) {
return [preference unsignedIntegerValue];
} else {
return NotificationNamePreview;
}
}
- (NotificationType)notificationPreviewTypeWithTransaction:(YapDatabaseReadTransaction *)transaction
{
NSNumber *preference = [self tryGetValueForKey:OWSPreferencesKeyNotificationPreviewType transaction:transaction];
if (preference) {
return [preference unsignedIntegerValue];
} else {
return NotificationNamePreview;
}
}
- (NSString *)nameForNotificationPreviewType:(NotificationType)notificationType
{
switch (notificationType) {
case NotificationNamePreview:
return NSLocalizedString(@"NOTIFICATIONS_SENDER_AND_MESSAGE", nil);
case NotificationNameNoPreview:
return NSLocalizedString(@"NOTIFICATIONS_SENDER_ONLY", nil);
case NotificationNoNameNoPreview:
return NSLocalizedString(@"NOTIFICATIONS_NONE", nil);
default:
return @"";
}
}
#pragma mark - Push Tokens
- (void)setPushToken:(NSString *)value
{
[self setValueForKey:OWSPreferencesKeyLastRecordedPushToken toValue:value];
}
- (nullable NSString *)getPushToken
{
return [self tryGetValueForKey:OWSPreferencesKeyLastRecordedPushToken];
}
- (void)setVoipToken:(NSString *)value
{
[self setValueForKey:OWSPreferencesKeyLastRecordedVoipToken toValue:value];
}
- (nullable NSString *)getVoipToken
{
return [self tryGetValueForKey:OWSPreferencesKeyLastRecordedVoipToken];
}
- (void)unsetRecordedAPNSTokens
{
[self setValueForKey:OWSPreferencesKeyLastRecordedPushToken toValue:nil];
[self setValueForKey:OWSPreferencesKeyLastRecordedVoipToken toValue:nil];
}
@end
NS_ASSUME_NONNULL_END

View File

@ -18,7 +18,7 @@ public extension Setting.BoolKey {
///
/// **Note:** In the legacy setting this flag controlled whether the preview was "disabled" (and defaulted to
/// true), by inverting this flag we can default it to false as is standard for Bool values
static let preferencesAppSwitcherPreviewEnabled: Setting.BoolKey = "preferencesAppSwitcherPreviewEnabled"
static let appSwitcherPreviewEnabled: Setting.BoolKey = "appSwitcherPreviewEnabled"
/// Controls whether typing indicators are enabled
///
@ -30,8 +30,22 @@ public extension Setting.BoolKey {
/// **Note:** Only works if both participants in a "contact" thread have this setting enabled
static let typingIndicatorsEnabled: Setting.BoolKey = "typingIndicatorsEnabled"
/// Controls whether the device will automatically lock the screen
static let isScreenLockEnabled: Setting.BoolKey = "isScreenLockEnabled"
/// Controls whether Link Previews (image & title URL metadata) will be downloaded when the user enters a URL
///
/// **Note:** Link Previews are only enabled for HTTPS urls
static let areLinkPreviewsEnabled: Setting.BoolKey = "areLinkPreviewsEnabled"
/// Controls whether the message requests item has been hidden on the home screen
static let hasHiddenMessageRequests: Setting.BoolKey = "hasHiddenMessageRequests"
/// Controls whether the notification sound should play while the app is in the foreground
static let playNotificationSoundInForeground: Setting.BoolKey = "playNotificationSoundInForeground"
/// A flag indicating whether the user has ever saved a thread
static let hasSavedThreadKey: Setting.BoolKey = "hasSavedThread"
}
public extension Setting.StringKey {
@ -42,6 +56,11 @@ public extension Setting.StringKey {
static let lastRecordedVoipToken: Setting.StringKey = "lastRecordedVoipToken"
}
public extension Setting.DoubleKey {
/// The duration of the timeout for screen lock in seconds
static let screenLockTimeoutSeconds: Setting.DoubleKey = "screenLockTimeoutSeconds"
}
public enum Preferences {
public enum NotificationPreviewType: Int, CaseIterable, EnumSetting {
/// Notifications should include both the sender name and a preview of the message content
@ -298,6 +317,56 @@ public class SMKPreferences: NSObject {
case .noNameNoPreview: return "NotificationNoNameNoPreview"
}
}
@objc(setPlayNotificationSoundInForeground:)
static func objc_setPlayNotificationSoundInForeground(_ enabled: Bool) {
GRDBStorage.shared.write { db in db[.playNotificationSoundInForeground] = enabled }
}
@objc(playNotificationSoundInForeground)
static func objc_playNotificationSoundInForeground() -> Bool {
return GRDBStorage.shared[.playNotificationSoundInForeground]
}
@objc(setScreenSecurity:)
static func objc_setScreenSecurity(_ enabled: Bool) {
GRDBStorage.shared.write { db in db[.appSwitcherPreviewEnabled] = enabled }
}
@objc(isScreenSecurityEnabled)
static func objc_isScreenSecurityEnabled() -> Bool {
return GRDBStorage.shared[.appSwitcherPreviewEnabled]
}
@objc(setAreReadReceiptsEnabled:)
static func objc_setAreReadReceiptsEnabled(_ enabled: Bool) {
GRDBStorage.shared.write { db in db[.areReadReceiptsEnabled] = enabled }
}
@objc(areReadReceiptsEnabled)
static func objc_areReadReceiptsEnabled() -> Bool {
return GRDBStorage.shared[.areReadReceiptsEnabled]
}
@objc(setTypingIndicatorsEnabled:)
static func objc_setTypingIndicatorsEnabled(_ enabled: Bool) {
GRDBStorage.shared.write { db in db[.typingIndicatorsEnabled] = enabled }
}
@objc(areTypingIndicatorsEnabled)
static func objc_areTypingIndicatorsEnabled() -> Bool {
return GRDBStorage.shared[.typingIndicatorsEnabled]
}
@objc(setLinkPreviewsEnabled:)
static func objc_setLinkPreviewsEnabled(_ enabled: Bool) {
GRDBStorage.shared.write { db in db[.areLinkPreviewsEnabled] = enabled }
}
@objc(areLinkPreviewsEnabled)
static func objc_areLinkPreviewsEnabled() -> Bool {
return GRDBStorage.shared[.areLinkPreviewsEnabled]
}
}
@objc(SMKSound)

View File

@ -1,24 +0,0 @@
////
//// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
////
//
//#import <Foundation/Foundation.h>
//
//NS_ASSUME_NONNULL_BEGIN
//
//@class SNProtoDataMessageBuilder;
//@class TSThread;
//
//@interface ProtoUtils : NSObject
//
//- (instancetype)init NS_UNAVAILABLE;
//
//+ (void)addLocalProfileKeyIfNecessary:(TSThread *)thread
// recipientId:(NSString *_Nullable)recipientId
// dataMessageBuilder:(SNProtoDataMessageBuilder *)dataMessageBuilder;
//
//+ (void)addLocalProfileKeyToDataMessageBuilder:(SNProtoDataMessageBuilder *)dataMessageBuilder;
//
//@end
//
//NS_ASSUME_NONNULL_END

View File

@ -1,50 +0,0 @@
////
//// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
////
//
//#import "ProtoUtils.h"
//#import "ProfileManagerProtocol.h"
//#import "SSKEnvironment.h"
//#import "TSThread.h"
//#import <SignalCoreKit/Cryptography.h>
//#import <SessionMessagingKit/SessionMessagingKit-Swift.h>
//
//NS_ASSUME_NONNULL_BEGIN
//
//@implementation ProtoUtils
//
//#pragma mark - Dependencies
//
////+ (id<ProfileManagerProtocol>)profileManager {
//// return SSKEnvironment.shared.profileManager;
////}
//
////+ (OWSAES256Key *)localProfileKey
////{
//// return [[LKStorage.shared getUser] profileEncryptionKey];
////}
//
//#pragma mark -
//
//+ (BOOL)shouldMessageHaveLocalProfileKey:(TSThread *)thread recipientId:(NSString *_Nullable)recipientId
//{
// return YES;
//}
//
//+ (void)addLocalProfileKeyIfNecessary:(TSThread *)thread
// recipientId:(NSString *_Nullable)recipientId
// dataMessageBuilder:(SNProtoDataMessageBuilder *)dataMessageBuilder
//{
// if ([self shouldMessageHaveLocalProfileKey:thread recipientId:recipientId]) {
// [dataMessageBuilder setProfileKey:[SMKProfile localProfileKey].keyData];
// }
//}
//
//+ (void)addLocalProfileKeyToDataMessageBuilder:(SNProtoDataMessageBuilder *)dataMessageBuilder
//{
// [dataMessageBuilder setProfileKey:[SMKProfile localProfileKey].keyData];
//}
//
//@end
//
//NS_ASSUME_NONNULL_END

View File

@ -14,6 +14,7 @@ NS_ASSUME_NONNULL_BEGIN
@interface YapDatabaseReadTransaction (OWS)
- (BOOL)boolForKey:(NSString *)key inCollection:(NSString *)collection defaultValue:(BOOL)defaultValue;
- (double)doubleForKey:(NSString *)key inCollection:(NSString *)collection defaultValue:(double)defaultValue;
- (int)intForKey:(NSString *)key inCollection:(NSString *)collection;
- (nullable NSDate *)dateForKey:(NSString *)key inCollection:(NSString *)collection;
- (nullable NSDictionary *)dictionaryForKey:(NSString *)key inCollection:(NSString *)collection;

View File

@ -30,6 +30,12 @@ NS_ASSUME_NONNULL_BEGIN
return value ? [value boolValue] : defaultValue;
}
- (double)doubleForKey:(NSString *)key inCollection:(NSString *)collection defaultValue:(double)defaultValue
{
NSNumber *_Nullable value = [self objectForKey:key inCollection:collection ofExpectedType:[NSNumber class]];
return value ? [value doubleValue] : defaultValue;
}
- (nullable NSData *)dataForKey:(NSString *)key inCollection:(NSString *)collection
{
return [self objectForKey:key inCollection:collection ofExpectedType:[NSData class]];

View File

@ -43,7 +43,7 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol {
return
}
let senderName = Profile.displayName(db, id: senderPublicKey, thread: thread)
let senderName = Profile.displayName(db, id: senderPublicKey, threadVariant: thread.variant)
var notificationTitle = senderName

View File

@ -269,6 +269,10 @@ public final class GRDBStorage {
onChange: onChange
)
}
public func addObserver(_ observer: TransactionObserver) {
dbPool.add(transactionObserver: observer)
}
}
// MARK: - Promise Extensions

View File

@ -40,6 +40,10 @@ public struct Job: Codable, Equatable, Identifiable, FetchableRecord, MutablePer
/// This is a recurring job that ensures the app fetches the default open group rooms on launch
case retrieveDefaultOpenGroupRooms
/// This is a recurring job that removes expired and orphaned data, it runs on launch and can also be triggered
/// as 'runOnce' to avoid waiting until the next launch to clear data
case garbageCollection
/// This is a recurring job that runs on launch and flags any messages marked as 'sending' to
/// be in their 'failed' state
///

View File

@ -1,3 +1,4 @@
import UIKit
public extension Array where Element : CustomStringConvertible {
@ -44,6 +45,11 @@ public extension Array {
}
}
public extension Array {
func grouped<Key: Hashable>(by keyForValue: (Element) throws -> Key) -> [Key: [Element]] {
return ((try? Dictionary(grouping: self, by: keyForValue)) ?? [:])
}
}
public extension Array where Element: Hashable {
func asSet() -> Set<Element> {

View File

@ -19,6 +19,10 @@ class AddMoreRailItem: GalleryRailItem {
return view
}
func isEqual(to other: GalleryRailItem?) -> Bool {
return (other is AddMoreRailItem)
}
}
class SignalAttachmentItem: Hashable {

View File

@ -26,7 +26,6 @@ FOUNDATION_EXPORT const unsigned char SignalUtilitiesKitVersionString[];
#import <SignalUtilitiesKit/OWSNavigationController.h>
#import <SignalUtilitiesKit/OWSOperation.h>
#import <SignalUtilitiesKit/OWSPrimaryStorage+keyFromIntLong.h>
#import <SignalUtilitiesKit/OWSProfileManager.h>
#import <SignalUtilitiesKit/OWSQueues.h>
#import <SignalUtilitiesKit/OWSResaveCollectionDBMigration.h>
#import <SignalUtilitiesKit/OWSTableViewController.h>
@ -35,7 +34,6 @@ FOUNDATION_EXPORT const unsigned char SignalUtilitiesKitVersionString[];
#import <SignalUtilitiesKit/OWSUnreadIndicator.h>
#import <SignalUtilitiesKit/OWSViewController.h>
#import <SignalUtilitiesKit/ScreenLockViewController.h>
#import <SignalUtilitiesKit/SignalAccount.h>
#import <SignalUtilitiesKit/SSKAsserts.h>
#import <SignalUtilitiesKit/ThreadUtil.h>
#import <SignalUtilitiesKit/TSConstants.h>

View File

@ -1,20 +1,21 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import LocalAuthentication
import SessionMessagingKit
// FIXME: Refactor this once the 'PrivacySettingsTableViewController' and 'OWSScreenLockUI' have been refactored
@objc public class OWSScreenLock: NSObject {
public enum OWSScreenLockOutcome {
case success
case cancel
case failure(error:String)
case unexpectedFailure(error:String)
case failure(error: String)
case unexpectedFailure(error: String)
}
@objc public let screenLockTimeoutDefault = 15 * kMinuteInterval
@objc public let screenLockTimeoutDefault = (15 * kMinuteInterval)
@objc public let screenLockTimeouts = [
1 * kMinuteInterval,
5 * kMinuteInterval,
@ -26,22 +27,12 @@ import LocalAuthentication
@objc public static let ScreenLockDidChange = Notification.Name("ScreenLockDidChange")
let primaryStorage: OWSPrimaryStorage
let dbConnection: YapDatabaseConnection
private let OWSScreenLock_Collection = "OWSScreenLock_Collection"
private let OWSScreenLock_Key_IsScreenLockEnabled = "OWSScreenLock_Key_IsScreenLockEnabled"
private let OWSScreenLock_Key_ScreenLockTimeoutSeconds = "OWSScreenLock_Key_ScreenLockTimeoutSeconds"
// MARK: - Singleton class
@objc(sharedManager)
public static let shared = OWSScreenLock()
private override init() {
self.primaryStorage = OWSPrimaryStorage.shared()
self.dbConnection = self.primaryStorage.newDatabaseConnection()
super.init()
SwiftSingletons.register(self)
@ -50,44 +41,31 @@ import LocalAuthentication
// MARK: - Properties
@objc public func isScreenLockEnabled() -> Bool {
AssertIsOnMainThread()
if !OWSStorage.isStorageReady() {
owsFailDebug("accessed screen lock state before storage is ready.")
return false
}
return self.dbConnection.bool(forKey: OWSScreenLock_Key_IsScreenLockEnabled, inCollection: OWSScreenLock_Collection, defaultValue: false)
return GRDBStorage.shared[.isScreenLockEnabled]
}
@objc
public func setIsScreenLockEnabled(_ value: Bool) {
AssertIsOnMainThread()
assert(OWSStorage.isStorageReady())
self.dbConnection.setBool(value, forKey: OWSScreenLock_Key_IsScreenLockEnabled, inCollection: OWSScreenLock_Collection)
NotificationCenter.default.postNotificationNameAsync(OWSScreenLock.ScreenLockDidChange, object: nil)
GRDBStorage.shared.writeAsync(
updates: { db in db[.isScreenLockEnabled] = value },
completion: { _, _ in
NotificationCenter.default.postNotificationNameAsync(OWSScreenLock.ScreenLockDidChange, object: nil)
}
)
}
@objc public func screenLockTimeout() -> TimeInterval {
AssertIsOnMainThread()
if !OWSStorage.isStorageReady() {
owsFailDebug("accessed screen lock state before storage is ready.")
return 0
}
return self.dbConnection.double(forKey: OWSScreenLock_Key_ScreenLockTimeoutSeconds, inCollection: OWSScreenLock_Collection, defaultValue: screenLockTimeoutDefault)
return GRDBStorage.shared[.screenLockTimeoutSeconds]
.defaulting(to: screenLockTimeoutDefault)
}
@objc public func setScreenLockTimeout(_ value: TimeInterval) {
AssertIsOnMainThread()
assert(OWSStorage.isStorageReady())
self.dbConnection.setDouble(value, forKey: OWSScreenLock_Key_ScreenLockTimeoutSeconds, inCollection: OWSScreenLock_Collection)
NotificationCenter.default.postNotificationNameAsync(OWSScreenLock.ScreenLockDidChange, object: nil)
GRDBStorage.shared.writeAsync(
updates: { db in db[.screenLockTimeoutSeconds] = value },
completion: { _, _ in
NotificationCenter.default.postNotificationNameAsync(OWSScreenLock.ScreenLockDidChange, object: nil)
}
)
}
// MARK: - Methods

View File

@ -1,25 +1,42 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import PromiseKit
import SessionUIKit
public protocol GalleryRailItemProvider {
var railItems: [GalleryRailItem] { get }
}
// MARK: - GalleryRailItem
public protocol GalleryRailItem {
func buildRailItemView() -> UIView
func isEqual(to other: GalleryRailItem?) -> Bool
}
// MARK: - GalleryRailCellViewDelegate
protocol GalleryRailCellViewDelegate: AnyObject {
func didTapGalleryRailCellView(_ galleryRailCellView: GalleryRailCellView)
}
public class GalleryRailCellView: UIView {
// MARK: - GalleryRailCellView
weak var delegate: GalleryRailCellViewDelegate?
public class GalleryRailCellView: UIView {
public let cellBorderWidth: CGFloat = 3
public var item: GalleryRailItem?
fileprivate weak var delegate: GalleryRailCellViewDelegate?
private(set) var isSelected: Bool = false
// MARK: - UI
let contentContainer: UIView = {
let view = UIView()
view.autoPinToSquareAspectRatio()
view.clipsToBounds = true
return view
}()
// MARK: - Initialization
override init(frame: CGRect) {
super.init(frame: frame)
@ -38,16 +55,14 @@ public class GalleryRailCellView: UIView {
fatalError("init(coder:) has not been implemented")
}
// MARK: Actions
// MARK: - Actions
@objc
func didTap(sender: UITapGestureRecognizer) {
self.delegate?.didTapGalleryRailCellView(self)
}
// MARK:
var item: GalleryRailItem?
// MARK: Content
func configure(item: GalleryRailItem, delegate: GalleryRailCellViewDelegate) {
self.item = item
@ -62,11 +77,7 @@ public class GalleryRailCellView: UIView {
itemView.autoPinEdgesToSuperviewEdges()
}
// MARK: Selected
private(set) var isSelected: Bool = false
public let cellBorderWidth: CGFloat = 3
// MARK: - Selected
func setIsSelected(_ isSelected: Bool) {
self.isSelected = isSelected
@ -81,134 +92,189 @@ public class GalleryRailCellView: UIView {
contentContainer.layer.borderWidth = 0
}
}
// MARK: Subview Helpers
let contentContainer: UIView = {
let view = UIView()
view.autoPinToSquareAspectRatio()
view.clipsToBounds = true
return view
}()
}
public protocol GalleryRailViewDelegate: class {
// MARK: - GalleryRailViewDelegate
public protocol GalleryRailViewDelegate: AnyObject {
func galleryRailView(_ galleryRailView: GalleryRailView, didTapItem imageRailItem: GalleryRailItem)
}
// MARK: - GalleryRailView
public class GalleryRailView: UIView, GalleryRailCellViewDelegate {
public enum ScrollFocusMode {
case keepCentered
case keepWithinBounds
}
public weak var delegate: GalleryRailViewDelegate?
public var scrollFocusMode: ScrollFocusMode = .keepCentered
public var cellViews: [GalleryRailCellView] = []
public weak var delegate: GalleryRailViewDelegate?
private var album: [GalleryRailItem]?
private var oldSize: CGSize = .zero
var cellViewItems: [GalleryRailItem] {
get { return cellViews.compactMap { $0.item } }
}
// MARK: Initializers
// MARK: - Initializers
override init(frame: CGRect) {
super.init(frame: frame)
clipsToBounds = false
addSubview(scrollView)
scrollView.clipsToBounds = false
scrollView.layoutMargins = .zero
scrollView.autoPinEdgesToSuperviewMargins()
scrollView.addSubview(stackView)
stackView.autoPinEdgesToSuperviewEdges()
stackView.autoMatch(.height, to: .height, of: scrollView)
}
public required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - UI
private let scrollView: UIScrollView = {
let result: UIScrollView = UIScrollView()
result.clipsToBounds = false
result.layoutMargins = .zero
result.isScrollEnabled = true
return result
}()
private let stackView: UIStackView = {
let result: UIStackView = UIStackView()
result.clipsToBounds = false
result.axis = .horizontal
result.spacing = 0
return result
}()
// MARK: Public
// MARK: - Public
public func configureCellViews(itemProvider: GalleryRailItemProvider?, focusedItem: GalleryRailItem?, cellViewBuilder: (GalleryRailItem) -> GalleryRailCellView) {
public func configureCellViews(album: [GalleryRailItem], focusedItem: GalleryRailItem?, cellViewBuilder: (GalleryRailItem) -> GalleryRailCellView) {
let animationDuration: TimeInterval = 0.2
let zippedItems = zip(album, self.cellViewItems)
guard let itemProvider = itemProvider else {
UIView.animate(withDuration: animationDuration) {
self.isHidden = true
}
self.cellViews = []
return
}
let areRailItemsIdentical = { (lhs: [GalleryRailItem], rhs: [GalleryRailItem]) -> Bool in
guard lhs.count == rhs.count else {
return false
}
for (index, element) in lhs.enumerated() {
guard element === rhs[index] else {
return false
}
}
return true
}
if itemProvider === self.itemProvider, areRailItemsIdentical(itemProvider.railItems, self.cellViewItems) {
// Check if the album has changed
guard
album.count != self.cellViewItems.count ||
zippedItems.contains(where: { lhs, rhs in !lhs.isEqual(to: rhs) })
else {
UIView.animate(withDuration: animationDuration) {
self.updateFocusedItem(focusedItem)
self.layoutIfNeeded()
}
}
self.itemProvider = itemProvider
guard itemProvider.railItems.count > 1 else {
let cellViews = scrollView.subviews
UIView.animate(withDuration: animationDuration,
animations: {
cellViews.forEach { $0.isHidden = true }
self.isHidden = true
},
completion: { _ in cellViews.forEach { $0.removeFromSuperview() } })
self.cellViews = []
return
}
scrollView.subviews.forEach { $0.removeFromSuperview() }
// If so update to the new album
self.album = album
UIView.animate(withDuration: animationDuration) {
self.isHidden = false
// Check if there are multiple items in the album (if not then just slide it away)
guard album.count > 1 else {
let oldFrame: CGRect = self.stackView.frame
UIView.animate(
withDuration: animationDuration,
animations: { [weak self] in
self?.stackView.frame = oldFrame.offsetBy(
dx: 0,
dy: oldFrame.height
)
},
completion: { [weak self] _ in
self?.stackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
self?.stackView.frame = oldFrame
self?.isHidden = true
self?.cellViews = []
}
)
return
}
// Otherwise slide it away, recreate it and then slide it back
var oldFrame: CGRect = self.stackView.frame
let newCellViews: [GalleryRailCellView] = buildCellViews(
items: album,
cellViewBuilder: cellViewBuilder
)
let cellViews = buildCellViews(items: itemProvider.railItems, cellViewBuilder: cellViewBuilder)
self.cellViews = cellViews
let stackView = UIStackView(arrangedSubviews: cellViews)
stackView.axis = .horizontal
stackView.spacing = 0
stackView.clipsToBounds = false
scrollView.addSubview(stackView)
stackView.autoPinEdgesToSuperviewEdges()
stackView.autoMatch(.height, to: .height, of: scrollView)
updateFocusedItem(focusedItem)
UIView.animate(
withDuration: (animationDuration / 2),
delay: 0,
options: .curveEaseIn,
animations: { [weak self] in
self?.stackView.frame = oldFrame.offsetBy(
dx: 0,
dy: oldFrame.height
)
},
completion: { [weak self] _ in
self?.stackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
newCellViews.forEach { cellView in
self?.stackView.addArrangedSubview(cellView)
}
self?.cellViews = newCellViews
// Update the UI (need to re-offset it as the position gets reset during
// during these changes)
UIView.performWithoutAnimation {
self?.updateFocusedItem(focusedItem)
self?.isHidden = false
oldFrame = (self?.stackView.frame)
.defaulting(to: oldFrame)
self?.stackView.frame = oldFrame.offsetBy(
dx: 0,
dy: oldFrame.height
)
}
UIView.animate(
withDuration: (animationDuration / 2),
delay: 0,
options: .curveEaseOut,
animations: { [weak self] in
self?.stackView.frame = oldFrame
},
completion: nil
)
}
)
}
// MARK: GalleryRailCellViewDelegate
// MARK: - GalleryRailCellViewDelegate
func didTapGalleryRailCellView(_ galleryRailCellView: GalleryRailCellView) {
guard let item = galleryRailCellView.item else {
owsFailDebug("item was unexpectedly nil")
return
}
guard let item = galleryRailCellView.item else { return }
delegate?.galleryRailView(self, didTapItem: item)
}
// MARK: Subview Helpers
private var itemProvider: GalleryRailItemProvider?
private let scrollView: UIScrollView = {
let scrollView = UIScrollView()
scrollView.isScrollEnabled = true
return scrollView
}()
// MARK: - Subview Helpers
public override func layoutSubviews() {
super.layoutSubviews()
guard self.bounds.size != self.oldSize else { return }
self.oldSize = self.bounds.size
// If the bounds of the biew changed then update the focused item to ensure the
// alignment isn't broken
if let focusedItem: GalleryRailItem = self.cellViews.first(where: { $0.isSelected })?.item {
self.updateFocusedItem(focusedItem)
}
}
private func buildCellViews(items: [GalleryRailItem], cellViewBuilder: (GalleryRailItem) -> GalleryRailCellView) -> [GalleryRailCellView] {
return items.map { item in
@ -218,49 +284,42 @@ public class GalleryRailView: UIView, GalleryRailCellViewDelegate {
}
}
enum ScrollFocusMode {
case keepCentered, keepWithinBounds
}
var scrollFocusMode: ScrollFocusMode = .keepCentered
func updateFocusedItem(_ focusedItem: GalleryRailItem?) {
var selectedCellView: GalleryRailCellView?
cellViews.forEach { cellView in
if cellView.item === focusedItem {
assert(selectedCellView == nil)
selectedCellView = cellView
cellView.setIsSelected(true)
} else {
cellView.setIsSelected(false)
}
}
let selectedCellView: GalleryRailCellView? = cellViews.first(where: { cellView -> Bool in
(cellView.item?.isEqual(to: focusedItem) == true)
})
cellViews.forEach { $0.setIsSelected(false) }
selectedCellView?.setIsSelected(true)
self.layoutIfNeeded()
switch scrollFocusMode {
case .keepCentered:
guard let selectedCell = selectedCellView else {
owsFailDebug("selectedCell was unexpectedly nil")
return
}
case .keepCentered:
guard
let selectedCell: UIView = selectedCellView,
let selectedCellSuperview: UIView = selectedCell.superview
else { return }
let cellViewCenter = selectedCell.superview!.convert(selectedCell.center, to: scrollView)
let additionalInset = scrollView.center.x - cellViewCenter.x
let cellViewCenter: CGPoint = selectedCellSuperview.convert(selectedCell.center, to: scrollView)
let additionalInset: CGFloat = ((scrollView.frame.width / 2) - cellViewCenter.x)
var inset: UIEdgeInsets = scrollView.contentInset
inset.left = additionalInset
scrollView.contentInset = inset
var inset = scrollView.contentInset
inset.left = additionalInset
scrollView.contentInset = inset
var offset: CGPoint = scrollView.contentOffset
offset.x = -additionalInset
scrollView.contentOffset = offset
case .keepWithinBounds:
guard
let selectedCell: UIView = selectedCellView,
let selectedCellSuperview: UIView = selectedCell.superview
else { return }
var offset = scrollView.contentOffset
offset.x = -additionalInset
scrollView.contentOffset = offset
case .keepWithinBounds:
guard let selectedCell = selectedCellView else {
owsFailDebug("selectedCell was unexpectedly nil")
return
}
let cellFrame = selectedCell.superview!.convert(selectedCell.frame, to: scrollView)
scrollView.scrollRectToVisible(cellFrame, animated: true)
let cellFrame: CGRect = selectedCellSuperview.convert(selectedCell.frame, to: scrollView)
scrollView.scrollRectToVisible(cellFrame, animated: true)
}
}
}

View File

@ -2,8 +2,6 @@
//// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
////
//
//#import <SessionMessagingKit/ProfileManagerProtocol.h>
//
//NS_ASSUME_NONNULL_BEGIN
//
//extern const NSUInteger kOWSProfileManager_NameDataLength;
@ -17,7 +15,7 @@
//@class YapDatabaseReadWriteTransaction;
//
//// This class can be safely accessed and used from any thread.
//@interface OWSProfileManager : NSObject <ProfileManagerProtocol>
//@interface OWSProfileManager : NSObject
//
//- (instancetype)init NS_UNAVAILABLE;
//

View File

@ -25,7 +25,6 @@ typedef NS_ENUM(NSInteger, OWSErrorCode) {
OWSErrorCodeSignalServiceFailure = 1001,
OWSErrorCodeSignalServiceRateLimited = 1010,
OWSErrorCodeUserError = 2001,
OWSErrorCodeNoSuchSignalRecipient = 777404,
OWSErrorCodeMessageSendDisabledDueToPreKeyUpdateFailures = 777405,
OWSErrorCodeMessageSendFailedToBlockList = 777406,
OWSErrorCodeMessageSendNoValidRecipients = 777407,
@ -62,7 +61,6 @@ extern NSError *OWSErrorWithCodeDescription(OWSErrorCode code, NSString *descrip
extern NSError *OWSErrorMakeUntrustedIdentityError(NSString *description, NSString *recipientId);
extern NSError *OWSErrorMakeUnableToProcessServerResponseError(void);
extern NSError *OWSErrorMakeFailedToSendOutgoingMessageError(void);
extern NSError *OWSErrorMakeNoSuchSignalRecipientError(void);
extern NSError *OWSErrorMakeAssertionError(NSString *description);
extern NSError *OWSErrorMakeMessageSendDisabledDueToPreKeyUpdateFailuresError(void);
extern NSError *OWSErrorMakeMessageSendFailedDueToBlockListError(void);

View File

@ -28,13 +28,6 @@ NSError *OWSErrorMakeFailedToSendOutgoingMessageError()
NSLocalizedString(@"ERROR_DESCRIPTION_CLIENT_SENDING_FAILURE", @"Generic notice when message failed to send."));
}
NSError *OWSErrorMakeNoSuchSignalRecipientError()
{
return OWSErrorWithCodeDescription(OWSErrorCodeNoSuchSignalRecipient,
NSLocalizedString(
@"ERROR_DESCRIPTION_UNREGISTERED_RECIPIENT", @"Error message when attempting to send message"));
}
NSError *OWSErrorMakeAssertionError(NSString *description)
{
OWSCFailDebug(@"Assertion failed: %@", description);

View File

@ -1,44 +0,0 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import <SessionUtilitiesKit/TSYapDatabaseObject.h>
NS_ASSUME_NONNULL_BEGIN
@class Contact;
@class SignalRecipient;
@class YapDatabaseReadTransaction;
// This class represents a single valid Signal account.
//
// * Contacts with multiple signal accounts will correspond to
// multiple instances of SignalAccount.
// * For non-contacts, the contact property will be nil.
@interface SignalAccount : TSYapDatabaseObject
// An E164 value identifying the signal account.
//
// This is the key property of this class and it
// will always be non-null.
@property (nonatomic, readonly) NSString *recipientId;
// This property is optional and will not be set for
// non-contact account.
@property (nonatomic, nullable) Contact *contact;
@property (nonatomic) BOOL hasMultipleAccountContact;
// For contacts with more than one signal account,
// this is a label for the account.
@property (nonatomic) NSString *multipleAccountLabelText;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithSignalRecipient:(SignalRecipient *)signalRecipient;
- (instancetype)initWithRecipientId:(NSString *)recipientId;
@end
NS_ASSUME_NONNULL_END

View File

@ -1,51 +0,0 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "SignalAccount.h"
#import "NSString+SSK.h"
#import "OWSPrimaryStorage.h"
#import "SignalRecipient.h"
NS_ASSUME_NONNULL_BEGIN
@interface SignalAccount ()
@property (nonatomic) NSString *recipientId;
@end
#pragma mark -
@implementation SignalAccount
- (instancetype)initWithSignalRecipient:(SignalRecipient *)signalRecipient
{
OWSAssertDebug(signalRecipient);
return [self initWithRecipientId:signalRecipient.recipientId];
}
- (instancetype)initWithRecipientId:(NSString *)recipientId
{
if (self = [super init]) {
OWSAssertDebug(recipientId.length > 0);
_recipientId = recipientId;
}
return self;
}
- (nullable NSString *)uniqueId
{
return _recipientId;
}
- (NSString *)multipleAccountLabelText
{
return _multipleAccountLabelText.filterStringForDisplay;
}
@end
NS_ASSUME_NONNULL_END

View File

@ -125,7 +125,6 @@ NS_ASSUME_NONNULL_BEGIN
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) {
[transaction removeAllObjectsInCollection:@"TSRecipient"];
}];
OWSLogInfo(@"Removed all TSRecipient records - will be replaced by SignalRecipients at next address sync.");
} else {
OWSLogError(@"Failed to remove bloom filter cache with error: %@", deleteError.localizedDescription);
}