mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
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:
parent
a6c7e252a7
commit
aabf656d89
81 changed files with 3413 additions and 4412 deletions
|
@ -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)
|
||||
|
|
|
@ -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 */,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
@ -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?)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
|
||||
#import "NotificationSettingsOptionsViewController.h"
|
||||
#import "Session-Swift.h"
|
||||
#import <SessionMessagingKit/Environment.h>
|
||||
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
|
||||
|
||||
@implementation NotificationSettingsOptionsViewController
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -8,7 +8,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
@class AvatarViewHelper;
|
||||
@class OWSContactsManager;
|
||||
@class SignalAccount;
|
||||
@class TSThread;
|
||||
|
||||
@protocol AvatarViewHelperDelegate <NSObject>
|
||||
|
|
10
Session/Utilities/DifferenceKit+Utilities.swift
Normal file
10
Session/Utilities/DifferenceKit+Utilities.swift
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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")"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
// }
|
||||
//}
|
||||
|
|
50
SessionMessagingKit/Jobs/Types/GarbageCollectionJob.swift
Normal file
50
SessionMessagingKit/Jobs/Types/GarbageCollectionJob.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
// }
|
||||
//}
|
||||
//
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -55,7 +55,6 @@ typedef NS_ENUM(NSInteger, TSGroupMetaMessage) {
|
|||
@class SNProtoContentBuilder;
|
||||
@class SNProtoDataMessage;
|
||||
@class SNProtoDataMessageBuilder;
|
||||
@class SignalRecipient;
|
||||
|
||||
@interface TSOutgoingMessageRecipientState : MTLModel
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -8,7 +8,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
BOOL IsNoteToSelfEnabled(void);
|
||||
|
||||
@class OWSDisappearingMessagesConfiguration;
|
||||
@class TSInteraction;
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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;
|
||||
|
|
|
@ -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]];
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -269,6 +269,10 @@ public final class GRDBStorage {
|
|||
onChange: onChange
|
||||
)
|
||||
}
|
||||
|
||||
public func addObserver(_ observer: TransactionObserver) {
|
||||
dbPool.add(transactionObserver: observer)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Promise Extensions
|
||||
|
|
|
@ -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
|
||||
///
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -19,6 +19,10 @@ class AddMoreRailItem: GalleryRailItem {
|
|||
|
||||
return view
|
||||
}
|
||||
|
||||
func isEqual(to other: GalleryRailItem?) -> Bool {
|
||||
return (other is AddMoreRailItem)
|
||||
}
|
||||
}
|
||||
|
||||
class SignalAttachmentItem: Hashable {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
//
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue