mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
Further work on SessionMessagingKit migrations
Added migrations for contacts and started working through thread migration (have contact and closed group threads migrating) Deprecated usage of ECKeyPair in the migrations (want to be able to remove Curve25519Kit in the future)
This commit is contained in:
parent
0f4df804ed
commit
cf66edb723
138 changed files with 4255 additions and 2397 deletions
|
@ -230,7 +230,6 @@
|
|||
B897621C25D201F7004F83B2 /* ScrollToBottomButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B897621B25D201F7004F83B2 /* ScrollToBottomButton.swift */; };
|
||||
B8AE75A425A6C6A6001A84D2 /* Data+Trimming.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8AE75A325A6C6A6001A84D2 /* Data+Trimming.swift */; };
|
||||
B8AF4BB426A5204600583500 /* SendSeedModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8AF4BB326A5204600583500 /* SendSeedModal.swift */; };
|
||||
B8B32033258B235D0020074B /* Storage+Contacts.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B32032258B235D0020074B /* Storage+Contacts.swift */; };
|
||||
B8B3204E258C15C80020074B /* ContactsMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B32044258C117C0020074B /* ContactsMigration.swift */; };
|
||||
B8B320B7258C30D70020074B /* HTMLMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B320B6258C30D70020074B /* HTMLMetadata.swift */; };
|
||||
B8BB82A5238F627000BA5194 /* HomeVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BB82A4238F627000BA5194 /* HomeVC.swift */; };
|
||||
|
@ -739,6 +738,21 @@
|
|||
FD09796B27F6C67500936362 /* Failable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09796A27F6C67500936362 /* Failable.swift */; };
|
||||
FD09796E27FA6D0000936362 /* Contact.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09796D27FA6D0000936362 /* Contact.swift */; };
|
||||
FD09797027FA6FF300936362 /* Profile.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09796F27FA6FF300936362 /* Profile.swift */; };
|
||||
FD09797227FAA2F500936362 /* Optional+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09797127FAA2F500936362 /* Optional+Utilities.swift */; };
|
||||
FD09797527FAB64300936362 /* ProfileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09797327FAB3E200936362 /* ProfileManager.swift */; };
|
||||
FD09797727FAB7A600936362 /* Data+Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09797627FAB7A600936362 /* Data+Image.swift */; };
|
||||
FD09797927FAB7E800936362 /* ImageFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09797827FAB7E800936362 /* ImageFormat.swift */; };
|
||||
FD09797B27FBB25900936362 /* Updatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09797A27FBB25900936362 /* Updatable.swift */; };
|
||||
FD09797D27FBDB2000936362 /* Notification+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09797C27FBDB2000936362 /* Notification+Utilities.swift */; };
|
||||
FD09797F27FCFBFF00936362 /* OWSAES256Key+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09797E27FCFBFF00936362 /* OWSAES256Key+Utilities.swift */; };
|
||||
FD09798127FCFEE800936362 /* SessionThread.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09798027FCFEE800936362 /* SessionThread.swift */; };
|
||||
FD09798327FD1A1500936362 /* ClosedGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09798227FD1A1500936362 /* ClosedGroup.swift */; };
|
||||
FD09798527FD1A6500936362 /* ClosedGroupKeyPair.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09798427FD1A6500936362 /* ClosedGroupKeyPair.swift */; };
|
||||
FD09798727FD1B7800936362 /* GroupMember.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09798627FD1B7800936362 /* GroupMember.swift */; };
|
||||
FD09798927FD1C5A00936362 /* OpenGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09798827FD1C5A00936362 /* OpenGroup.swift */; };
|
||||
FD09798B27FD1CFE00936362 /* Capability.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09798A27FD1CFE00936362 /* Capability.swift */; };
|
||||
FD09798D27FD1D8900936362 /* DisappearingMessageConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09798C27FD1D8900936362 /* DisappearingMessageConfiguration.swift */; };
|
||||
FD09799127FD499200936362 /* BoxKeyPair+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09799027FD499200936362 /* BoxKeyPair+Utilities.swift */; };
|
||||
FD17D79927F40AB800122BE0 /* _002_YDBToGRDBMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D79827F40AB800122BE0 /* _002_YDBToGRDBMigration.swift */; };
|
||||
FD17D79C27F40B2E00122BE0 /* SMKLegacyModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D79B27F40B2E00122BE0 /* SMKLegacyModels.swift */; };
|
||||
FD17D7A027F40CC800122BE0 /* _001_InitialSetupMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D79F27F40CC800122BE0 /* _001_InitialSetupMigration.swift */; };
|
||||
|
@ -1227,7 +1241,6 @@
|
|||
B897621B25D201F7004F83B2 /* ScrollToBottomButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollToBottomButton.swift; sourceTree = "<group>"; };
|
||||
B8AE75A325A6C6A6001A84D2 /* Data+Trimming.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Trimming.swift"; sourceTree = "<group>"; };
|
||||
B8AF4BB326A5204600583500 /* SendSeedModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendSeedModal.swift; sourceTree = "<group>"; };
|
||||
B8B32032258B235D0020074B /* Storage+Contacts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Storage+Contacts.swift"; sourceTree = "<group>"; };
|
||||
B8B32044258C117C0020074B /* ContactsMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsMigration.swift; sourceTree = "<group>"; };
|
||||
B8B320B6258C30D70020074B /* HTMLMetadata.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTMLMetadata.swift; sourceTree = "<group>"; };
|
||||
B8B5BCEB2394D869003823C9 /* Button.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Button.swift; sourceTree = "<group>"; };
|
||||
|
@ -1772,6 +1785,21 @@
|
|||
FD09796A27F6C67500936362 /* Failable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Failable.swift; sourceTree = "<group>"; };
|
||||
FD09796D27FA6D0000936362 /* Contact.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Contact.swift; sourceTree = "<group>"; };
|
||||
FD09796F27FA6FF300936362 /* Profile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Profile.swift; sourceTree = "<group>"; };
|
||||
FD09797127FAA2F500936362 /* Optional+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+Utilities.swift"; sourceTree = "<group>"; };
|
||||
FD09797327FAB3E200936362 /* ProfileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileManager.swift; sourceTree = "<group>"; };
|
||||
FD09797627FAB7A600936362 /* Data+Image.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Image.swift"; sourceTree = "<group>"; };
|
||||
FD09797827FAB7E800936362 /* ImageFormat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageFormat.swift; sourceTree = "<group>"; };
|
||||
FD09797A27FBB25900936362 /* Updatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Updatable.swift; sourceTree = "<group>"; };
|
||||
FD09797C27FBDB2000936362 /* Notification+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Notification+Utilities.swift"; sourceTree = "<group>"; };
|
||||
FD09797E27FCFBFF00936362 /* OWSAES256Key+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OWSAES256Key+Utilities.swift"; sourceTree = "<group>"; };
|
||||
FD09798027FCFEE800936362 /* SessionThread.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionThread.swift; sourceTree = "<group>"; };
|
||||
FD09798227FD1A1500936362 /* ClosedGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClosedGroup.swift; sourceTree = "<group>"; };
|
||||
FD09798427FD1A6500936362 /* ClosedGroupKeyPair.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClosedGroupKeyPair.swift; sourceTree = "<group>"; };
|
||||
FD09798627FD1B7800936362 /* GroupMember.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMember.swift; sourceTree = "<group>"; };
|
||||
FD09798827FD1C5A00936362 /* OpenGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroup.swift; sourceTree = "<group>"; };
|
||||
FD09798A27FD1CFE00936362 /* Capability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Capability.swift; sourceTree = "<group>"; };
|
||||
FD09798C27FD1D8900936362 /* DisappearingMessageConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisappearingMessageConfiguration.swift; sourceTree = "<group>"; };
|
||||
FD09799027FD499200936362 /* BoxKeyPair+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BoxKeyPair+Utilities.swift"; sourceTree = "<group>"; };
|
||||
FD17D79527F3E04600122BE0 /* _001_InitialSetupMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _001_InitialSetupMigration.swift; sourceTree = "<group>"; };
|
||||
FD17D79827F40AB800122BE0 /* _002_YDBToGRDBMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _002_YDBToGRDBMigration.swift; sourceTree = "<group>"; };
|
||||
FD17D79B27F40B2E00122BE0 /* SMKLegacyModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SMKLegacyModels.swift; sourceTree = "<group>"; };
|
||||
|
@ -2314,13 +2342,16 @@
|
|||
children = (
|
||||
C33FDB54255A580D00E217F9 /* DataSource.h */,
|
||||
C33FDBB6255A581600E217F9 /* DataSource.m */,
|
||||
FD09797827FAB7E800936362 /* ImageFormat.swift */,
|
||||
C33FDAFC255A580600E217F9 /* MIMETypeUtil.h */,
|
||||
C33FDB41255A580C00E217F9 /* MIMETypeUtil.m */,
|
||||
C33FDB29255A580A00E217F9 /* NSData+Image.h */,
|
||||
C33FDAEF255A580500E217F9 /* NSData+Image.m */,
|
||||
FD09797627FAB7A600936362 /* Data+Image.swift */,
|
||||
C33FDB22255A580900E217F9 /* OWSMediaUtils.swift */,
|
||||
C33FDB1C255A580900E217F9 /* UIImage+OWS.h */,
|
||||
C33FDB81255A581100E217F9 /* UIImage+OWS.m */,
|
||||
FD09797A27FBB25900936362 /* Updatable.swift */,
|
||||
);
|
||||
path = Media;
|
||||
sourceTree = "<group>";
|
||||
|
@ -2666,7 +2697,6 @@
|
|||
C33FDAB1255A580000E217F9 /* OWSStorage.m */,
|
||||
C33FDAB9255A580100E217F9 /* OWSStorage+Subclass.h */,
|
||||
B8D8F1372566120F0092EF10 /* Storage+ClosedGroups.swift */,
|
||||
B8B32032258B235D0020074B /* Storage+Contacts.swift */,
|
||||
B8D8F17625661AFA0092EF10 /* Storage+Jobs.swift */,
|
||||
B8D8F19225661BF80092EF10 /* Storage+Messaging.swift */,
|
||||
B8D8F18825661BA50092EF10 /* Storage+OpenGroups.swift */,
|
||||
|
@ -3148,6 +3178,7 @@
|
|||
children = (
|
||||
C33FDB01255A580700E217F9 /* AppReadiness.h */,
|
||||
C33FDB75255A581000E217F9 /* AppReadiness.m */,
|
||||
FD09799027FD499200936362 /* BoxKeyPair+Utilities.swift */,
|
||||
C38EF309255B6DBE007E1867 /* DeviceSleepManager.swift */,
|
||||
C37F53E8255BA9BB002AEA92 /* Environment.h */,
|
||||
C37F5402255BA9ED002AEA92 /* Environment.m */,
|
||||
|
@ -3157,6 +3188,7 @@
|
|||
C3A71D0A2558989C0043A11F /* MessageWrapper.swift */,
|
||||
C3A71D4E25589FF30043A11F /* NSData+messagePadding.h */,
|
||||
C3A71D4825589FF20043A11F /* NSData+messagePadding.m */,
|
||||
FD09797E27FCFBFF00936362 /* OWSAES256Key+Utilities.swift */,
|
||||
C38EF2F5255B6DBC007E1867 /* OWSAudioPlayer.h */,
|
||||
C38EF2F7255B6DBC007E1867 /* OWSAudioPlayer.m */,
|
||||
C38EF281255B6D84007E1867 /* OWSAudioSession.swift */,
|
||||
|
@ -3175,6 +3207,7 @@
|
|||
7B1581E1271E743B00848B49 /* OWSSounds.swift */,
|
||||
C38EF2FB255B6DBD007E1867 /* OWSWindowManager.h */,
|
||||
C38EF306255B6DBE007E1867 /* OWSWindowManager.m */,
|
||||
FD09797327FAB3E200936362 /* ProfileManager.swift */,
|
||||
C38EF2EC255B6DBA007E1867 /* ProximityMonitoringManager.swift */,
|
||||
C3BBE0B42554F0E10050F1E3 /* ProofOfWork.swift */,
|
||||
C33FDB91255A581200E217F9 /* ProtoUtils.h */,
|
||||
|
@ -3558,6 +3591,8 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
FD09796A27F6C67500936362 /* Failable.swift */,
|
||||
FD09797127FAA2F500936362 /* Optional+Utilities.swift */,
|
||||
FD09797C27FBDB2000936362 /* Notification+Utilities.swift */,
|
||||
C3E7134E251C867C009649BB /* Sodium+Conversion.swift */,
|
||||
);
|
||||
path = Utilities;
|
||||
|
@ -3568,6 +3603,13 @@
|
|||
children = (
|
||||
FD09796D27FA6D0000936362 /* Contact.swift */,
|
||||
FD09796F27FA6FF300936362 /* Profile.swift */,
|
||||
FD09798027FCFEE800936362 /* SessionThread.swift */,
|
||||
FD09798C27FD1D8900936362 /* DisappearingMessageConfiguration.swift */,
|
||||
FD09798227FD1A1500936362 /* ClosedGroup.swift */,
|
||||
FD09798427FD1A6500936362 /* ClosedGroupKeyPair.swift */,
|
||||
FD09798827FD1C5A00936362 /* OpenGroup.swift */,
|
||||
FD09798627FD1B7800936362 /* GroupMember.swift */,
|
||||
FD09798A27FD1CFE00936362 /* Capability.swift */,
|
||||
);
|
||||
path = Models;
|
||||
sourceTree = "<group>";
|
||||
|
@ -4720,11 +4762,13 @@
|
|||
FD17D7C127F5200100122BE0 /* TypedTableDefinition.swift in Sources */,
|
||||
FD17D7EA27F6A1C600122BE0 /* SUKLegacyModels.swift in Sources */,
|
||||
C352A36D2557858E00338F3E /* NSTimer+Proxying.m in Sources */,
|
||||
FD09797B27FBB25900936362 /* Updatable.swift in Sources */,
|
||||
C32C5A2D256DB849003C73A2 /* LKGroupUtilities.m in Sources */,
|
||||
C3C2ABD22553C6C900C340D1 /* Data+SecureRandom.swift in Sources */,
|
||||
B8856E09256F1676001CE70E /* UIDevice+featureSupport.swift in Sources */,
|
||||
B8856DEF256F161F001CE70E /* NSString+SSK.m in Sources */,
|
||||
FD09796727F6B0B600936362 /* Sodium+Conversion.swift in Sources */,
|
||||
FD09797927FAB7E800936362 /* ImageFormat.swift in Sources */,
|
||||
C32C5DC9256DD935003C73A2 /* ProxiedContentDownloader.swift in Sources */,
|
||||
C3D9E4C02567767F0040E4F3 /* DataSource.m in Sources */,
|
||||
C3D9E43125676D3D0040E4F3 /* Configuration.swift in Sources */,
|
||||
|
@ -4740,6 +4784,7 @@
|
|||
C3D9E4DA256778410040E4F3 /* UIImage+OWS.m in Sources */,
|
||||
C32C600F256E07F5003C73A2 /* NSUserDefaults+OWS.m in Sources */,
|
||||
C3D9E35E25675F640040E4F3 /* OWSFileSystem.m in Sources */,
|
||||
FD09797D27FBDB2000936362 /* Notification+Utilities.swift in Sources */,
|
||||
C3C2AC2E2553CBEB00C340D1 /* String+Trimming.swift in Sources */,
|
||||
C32C5B48256DC211003C73A2 /* NSNotificationCenter+OWS.m in Sources */,
|
||||
FD17D7C727F5207C00122BE0 /* DatabaseMigrator+Utilities.swift in Sources */,
|
||||
|
@ -4777,10 +4822,12 @@
|
|||
FD17D7BF27F51F8200122BE0 /* ColumnExpressible.swift in Sources */,
|
||||
FD17D7E527F6A09900122BE0 /* Identity.swift in Sources */,
|
||||
B87EF18126377A1D00124B3C /* Features.swift in Sources */,
|
||||
FD09797727FAB7A600936362 /* Data+Image.swift in Sources */,
|
||||
C300A60D2554B31900555489 /* Logging.swift in Sources */,
|
||||
B8FF8EA625C11FEF004D1F22 /* IPv4.swift in Sources */,
|
||||
C3D9E35525675EE10040E4F3 /* MIMETypeUtil.m in Sources */,
|
||||
FD705A8E278CE29800F16121 /* String+Localization.swift in Sources */,
|
||||
FD09797227FAA2F500936362 /* Optional+Utilities.swift in Sources */,
|
||||
C3A7219A2558C1660043A11F /* AnyPromise+Conversion.swift in Sources */,
|
||||
C300A6322554B6D100555489 /* NSDate+Timestamp.mm in Sources */,
|
||||
FD17D7E727F6A16700122BE0 /* _002_YDBToGRDBMigration.swift in Sources */,
|
||||
|
@ -4816,6 +4863,7 @@
|
|||
C300A5FC2554B0A000555489 /* MessageReceiver.swift in Sources */,
|
||||
7B1581E2271E743B00848B49 /* OWSSounds.swift in Sources */,
|
||||
C32C5A76256DBBCF003C73A2 /* SignalAttachment.swift in Sources */,
|
||||
FD09798927FD1C5A00936362 /* OpenGroup.swift in Sources */,
|
||||
C32C5CA4256DD1DC003C73A2 /* TSAccountManager.m in Sources */,
|
||||
C352A3892557876500338F3E /* JobQueue.swift in Sources */,
|
||||
C3BBE0B52554F0E10050F1E3 /* ProofOfWork.swift in Sources */,
|
||||
|
@ -4824,14 +4872,19 @@
|
|||
B8856D1A256F114D001CE70E /* ProximityMonitoringManager.swift in Sources */,
|
||||
C3D9E52725677DF20040E4F3 /* OWSThumbnailService.swift in Sources */,
|
||||
C32C5E75256DE020003C73A2 /* YapDatabaseTransaction+OWS.m in Sources */,
|
||||
FD09797527FAB64300936362 /* ProfileManager.swift in Sources */,
|
||||
C3BBE0802554CDD70050F1E3 /* Storage.swift in Sources */,
|
||||
C3DB66AC260ACA42001EFC55 /* OpenGroupManagerV2.swift in Sources */,
|
||||
B8F5F61B25EDE4BF003BF8D4 /* DataExtractionNotificationInfoMessage.swift in Sources */,
|
||||
C379DCF4256735770002D4EB /* VisibleMessage+Attachment.swift in Sources */,
|
||||
FD09797F27FCFBFF00936362 /* OWSAES256Key+Utilities.swift in Sources */,
|
||||
FD09798327FD1A1500936362 /* ClosedGroup.swift in Sources */,
|
||||
B8856D34256F1192001CE70E /* Environment.m in Sources */,
|
||||
B8B320B7258C30D70020074B /* HTMLMetadata.swift in Sources */,
|
||||
C32C5AB1256DBE8F003C73A2 /* TSIncomingMessage.m in Sources */,
|
||||
C3A3A107256E1A5C004D228D /* OWSDisappearingMessagesFinder.m in Sources */,
|
||||
FD09798727FD1B7800936362 /* GroupMember.swift in Sources */,
|
||||
FD09799127FD499200936362 /* BoxKeyPair+Utilities.swift in Sources */,
|
||||
C32C59C3256DB41F003C73A2 /* TSGroupModel.m in Sources */,
|
||||
B8856ECE256F1E58001CE70E /* OWSPreferences.m in Sources */,
|
||||
FD17D79927F40AB800122BE0 /* _002_YDBToGRDBMigration.swift in Sources */,
|
||||
|
@ -4861,10 +4914,10 @@
|
|||
C3DA9C0725AE7396008F7C7E /* ConfigurationMessage.swift in Sources */,
|
||||
B8856CEE256F1054001CE70E /* OWSAudioPlayer.m in Sources */,
|
||||
FD5D200F27AA2B6000FEA984 /* MessageRequestResponse.swift in Sources */,
|
||||
FD09798D27FD1D8900936362 /* DisappearingMessageConfiguration.swift in Sources */,
|
||||
C32C5EDC256DF501003C73A2 /* YapDatabaseConnection+OWS.m in Sources */,
|
||||
C3BBE0762554CDA60050F1E3 /* Configuration.swift in Sources */,
|
||||
C35D76DB26606304009AA5FB /* ThreadUpdateBatcher.swift in Sources */,
|
||||
B8B32033258B235D0020074B /* Storage+Contacts.swift in Sources */,
|
||||
B8856D69256F141F001CE70E /* OWSWindowManager.m in Sources */,
|
||||
C3D9E3BE25676AD70040E4F3 /* TSAttachmentPointer.m in Sources */,
|
||||
C3ECBF7B257056B700EA7FCE /* Threading.swift in Sources */,
|
||||
|
@ -4908,6 +4961,7 @@
|
|||
C3A3A18A256E2092004D228D /* SignalRecipient.m in Sources */,
|
||||
C3C2A74425539EB700C340D1 /* Message.swift in Sources */,
|
||||
C32C5F11256DF79A003C73A2 /* SSKIncrementingIdFinder.swift in Sources */,
|
||||
FD09798527FD1A6500936362 /* ClosedGroupKeyPair.swift in Sources */,
|
||||
C32C5DBF256DD743003C73A2 /* ClosedGroupPoller.swift in Sources */,
|
||||
C32C5EEE256DF54E003C73A2 /* TSDatabaseView.m in Sources */,
|
||||
C352A35B2557824E00338F3E /* AttachmentUploadJob.swift in Sources */,
|
||||
|
@ -4916,11 +4970,13 @@
|
|||
C32C5AAE256DBE8F003C73A2 /* TSInteraction.m in Sources */,
|
||||
C32C5D23256DD4C0003C73A2 /* Mention.swift in Sources */,
|
||||
C32C5FD6256E0346003C73A2 /* Notification+Thread.swift in Sources */,
|
||||
FD09798B27FD1CFE00936362 /* Capability.swift in Sources */,
|
||||
C3BBE0C72554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift in Sources */,
|
||||
C32C5C88256DD0D2003C73A2 /* Storage+Messaging.swift in Sources */,
|
||||
FD17D79C27F40B2E00122BE0 /* SMKLegacyModels.swift in Sources */,
|
||||
C32C59C7256DB41F003C73A2 /* TSThread.m in Sources */,
|
||||
C300A5B22554AF9800555489 /* VisibleMessage+Profile.swift in Sources */,
|
||||
FD09798127FCFEE800936362 /* SessionThread.swift in Sources */,
|
||||
C32C5A75256DBBCF003C73A2 /* TSAttachmentPointer+Conversion.swift in Sources */,
|
||||
C32C5AF8256DC051003C73A2 /* OWSDisappearingMessagesConfiguration.m in Sources */,
|
||||
C32C5EBA256DE130003C73A2 /* OWSQuotedReplyModel.m in Sources */,
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import PromiseKit
|
||||
import SessionMessagingKit
|
||||
|
||||
@objc(SNEditClosedGroupVC)
|
||||
final class EditClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelegate {
|
||||
|
@ -73,7 +77,7 @@ final class EditClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelega
|
|||
backButton.tintColor = Colors.text
|
||||
navigationItem.backBarButtonItem = backButton
|
||||
func getDisplayName(for publicKey: String) -> String {
|
||||
return Storage.shared.getContact(with: publicKey)?.displayName(for: .regular) ?? publicKey
|
||||
return Profile.displayName(for: publicKey)
|
||||
}
|
||||
setUpViewHierarchy()
|
||||
// Always show zombies at the bottom
|
||||
|
@ -107,7 +111,7 @@ final class EditClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelega
|
|||
membersLabel.font = .systemFont(ofSize: Values.mediumFontSize)
|
||||
membersLabel.text = "Members"
|
||||
// Add members button
|
||||
let hasContactsToAdd = !Set(ContactUtilities.getAllContacts()).subtracting(self.membersAndZombies).isEmpty
|
||||
let hasContactsToAdd = !Set(Contact.fetchAllIds()).subtracting(self.membersAndZombies).isEmpty
|
||||
if (!hasContactsToAdd) {
|
||||
addMembersButton.isUserInteractionEnabled = false
|
||||
let disabledColor = Colors.text.withAlphaComponent(Values.mediumOpacity)
|
||||
|
@ -246,10 +250,10 @@ final class EditClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelega
|
|||
var members = self.membersAndZombies
|
||||
members.append(contentsOf: selectedUsers)
|
||||
func getDisplayName(for publicKey: String) -> String {
|
||||
return Storage.shared.getContact(with: publicKey)?.displayName(for: .regular) ?? publicKey
|
||||
return Profile.displayName(for: publicKey)
|
||||
}
|
||||
self.membersAndZombies = members.sorted { getDisplayName(for: $0) < getDisplayName(for: $1) }
|
||||
let hasContactsToAdd = !Set(ContactUtilities.getAllContacts()).subtracting(self.membersAndZombies).isEmpty
|
||||
let hasContactsToAdd = !Set(Contact.fetchAllIds()).subtracting(self.membersAndZombies).isEmpty
|
||||
self.addMembersButton.isUserInteractionEnabled = hasContactsToAdd
|
||||
let color = hasContactsToAdd ? Colors.accent : Colors.text.withAlphaComponent(Values.mediumOpacity)
|
||||
self.addMembersButton.layer.borderColor = color.cgColor
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import PromiseKit
|
||||
import SessionMessagingKit
|
||||
|
||||
private protocol TableViewTouchDelegate {
|
||||
|
||||
|
@ -15,7 +19,7 @@ private final class TableView : UITableView {
|
|||
}
|
||||
|
||||
final class NewClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelegate, TableViewTouchDelegate, UITextFieldDelegate, UIScrollViewDelegate {
|
||||
private let contacts = ContactUtilities.getAllContacts()
|
||||
private let contacts = Contact.fetchAllIds()
|
||||
private var selectedContacts: Set<String> = []
|
||||
|
||||
// MARK: Components
|
||||
|
@ -174,7 +178,10 @@ final class NewClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelegat
|
|||
promise = MessageSender.createClosedGroup(name: name, members: selectedContacts, transaction: transaction)
|
||||
}
|
||||
let _ = promise.done(on: DispatchQueue.main) { thread in
|
||||
MessageSender.syncConfiguration(forceSyncNow: true).retainUntilComplete()
|
||||
GRDBStorage.shared.write { db in
|
||||
MessageSender.syncConfiguration(db, forceSyncNow: true).retainUntilComplete()
|
||||
}
|
||||
|
||||
self?.presentingViewController?.dismiss(animated: true, completion: nil)
|
||||
SignalApp.shared().presentConversation(for: thread, action: .compose, animated: false)
|
||||
}
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import CoreServices
|
||||
import Photos
|
||||
import PhotosUI
|
||||
import PromiseKit
|
||||
import GRDB
|
||||
import SessionUtilitiesKit
|
||||
import SignalUtilitiesKit
|
||||
|
||||
|
@ -14,7 +17,7 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
|
|||
// Don't take the user to settings for message requests
|
||||
guard
|
||||
let contactThread: TSContactThread = thread as? TSContactThread,
|
||||
let contact: Contact = Storage.shared.getContact(with: contactThread.contactSessionID()),
|
||||
let contact: Contact = GRDBStorage.shared.read({ db in try Contact.fetchOne(db, id: contactThread.contactSessionID()) }),
|
||||
contact.isApproved,
|
||||
contact.didApproveMe
|
||||
else {
|
||||
|
@ -44,23 +47,31 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
|
|||
@objc func unblock() {
|
||||
guard let thread = thread as? TSContactThread else { return }
|
||||
let publicKey = thread.contactSessionID()
|
||||
UIView.animate(withDuration: 0.25, animations: {
|
||||
self.blockedBanner.alpha = 0
|
||||
}, completion: { _ in
|
||||
if let contact: Contact = Storage.shared.getContact(with: publicKey) {
|
||||
Storage.shared.write(
|
||||
with: { transaction in
|
||||
guard let transaction = transaction as? YapDatabaseReadWriteTransaction else { return }
|
||||
|
||||
contact.isBlocked = false
|
||||
Storage.shared.setContact(contact, using: transaction)
|
||||
|
||||
UIView.animate(
|
||||
withDuration: 0.25,
|
||||
animations: {
|
||||
self.blockedBanner.alpha = 0
|
||||
},
|
||||
completion: { _ in
|
||||
GRDBStorage.shared.writeAsync(
|
||||
updates: { db in
|
||||
try Contact
|
||||
.fetchOne(db, id: publicKey)?
|
||||
.with(isBlocked: false)
|
||||
.update(db)
|
||||
},
|
||||
completion: {
|
||||
MessageSender.syncConfiguration(forceSyncNow: true).retainUntilComplete()
|
||||
completion: { db, result in
|
||||
switch result {
|
||||
case .success:
|
||||
MessageSender.syncConfiguration(db, forceSyncNow: true).retainUntilComplete()
|
||||
|
||||
default: break
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
func showBlockedModalIfNeeded() -> Bool {
|
||||
|
@ -374,7 +385,7 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
|
|||
|
||||
// Update the input state if this is a contact thread
|
||||
if let contactThread: TSContactThread = thread as? TSContactThread {
|
||||
let contact: Contact? = Storage.shared.getContact(with: contactThread.contactSessionID())
|
||||
let contact: Contact? = GRDBStorage.shared.read { db in try Contact.fetchOne(db, id: contactThread.contactSessionID()) }
|
||||
|
||||
// If the contact doesn't exist yet then it's a message request without the first message sent
|
||||
// so only allow text-based messages
|
||||
|
@ -525,9 +536,11 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
|
|||
} else {
|
||||
switch viewItem.messageCellType {
|
||||
case .audio:
|
||||
if viewItem.interaction is TSIncomingMessage,
|
||||
if
|
||||
viewItem.interaction is TSIncomingMessage,
|
||||
let thread = self.thread as? TSContactThread,
|
||||
Storage.shared.getContact(with: thread.contactSessionID())?.isTrusted != true {
|
||||
let contact: Contact? = GRDBStorage.shared.read({ db in try Contact.fetchOne(db, id: thread.contactSessionID()) }),
|
||||
contact?.isTrusted != true {
|
||||
confirmDownload()
|
||||
} else {
|
||||
playOrPauseAudio(for: viewItem)
|
||||
|
@ -535,9 +548,11 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
|
|||
case .mediaMessage:
|
||||
guard let index = viewItems.firstIndex(where: { $0 === viewItem }),
|
||||
let cell = messagesTableView.cellForRow(at: IndexPath(row: index, section: 0)) as? VisibleMessageCell else { return }
|
||||
if viewItem.interaction is TSIncomingMessage,
|
||||
if
|
||||
viewItem.interaction is TSIncomingMessage,
|
||||
let thread = self.thread as? TSContactThread,
|
||||
Storage.shared.getContact(with: thread.contactSessionID())?.isTrusted != true {
|
||||
let contact: Contact? = GRDBStorage.shared.read({ db in try Contact.fetchOne(db, id: thread.contactSessionID()) }),
|
||||
contact?.isTrusted != true {
|
||||
confirmDownload()
|
||||
} else {
|
||||
guard let albumView = cell.albumView else { return }
|
||||
|
@ -559,9 +574,11 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
|
|||
gallery.presentDetailView(fromViewController: self, mediaAttachment: stream)
|
||||
}
|
||||
case .genericAttachment:
|
||||
if viewItem.interaction is TSIncomingMessage,
|
||||
if
|
||||
viewItem.interaction is TSIncomingMessage,
|
||||
let thread = self.thread as? TSContactThread,
|
||||
Storage.shared.getContact(with: thread.contactSessionID())?.isTrusted != true {
|
||||
let contact: Contact? = GRDBStorage.shared.read({ db in try Contact.fetchOne(db, id: thread.contactSessionID()) }),
|
||||
contact?.isTrusted != true {
|
||||
confirmDownload()
|
||||
}
|
||||
else if (
|
||||
|
@ -1108,9 +1125,13 @@ extension ConversationVC {
|
|||
// (it'll be updated with correct profile info if they accept the message request so this
|
||||
// shouldn't cause weird behaviours)
|
||||
let sessionId: String = contactThread.contactSessionID()
|
||||
let contact: Contact = (Storage.shared.getContact(with: sessionId) ?? Contact(sessionID: sessionId))
|
||||
|
||||
guard !contact.isApproved else { return Promise.value(()) }
|
||||
guard
|
||||
let contact: Contact = GRDBStorage.shared.read({ db in Contact.fetchOrCreate(db, id: sessionId) }),
|
||||
!contact.isApproved
|
||||
else {
|
||||
return Promise.value(())
|
||||
}
|
||||
|
||||
return Promise.value(())
|
||||
.then { [weak self] _ -> Promise<Void> in
|
||||
|
@ -1151,49 +1172,58 @@ extension ConversationVC {
|
|||
}
|
||||
.map { _ in
|
||||
// Default 'didApproveMe' to true for the person approving the message request
|
||||
Storage.write { transaction in
|
||||
contact.isApproved = true
|
||||
contact.didApproveMe = (contact.didApproveMe || !isNewThread)
|
||||
Storage.shared.setContact(contact, using: transaction)
|
||||
}
|
||||
|
||||
// Send a sync message with the details of the contact
|
||||
MessageSender.syncConfiguration(forceSyncNow: true).retainUntilComplete()
|
||||
|
||||
// Hide the 'messageRequestView' since the request has been approved and force a config
|
||||
// sync to propagate the contact approval state (both must run on the main thread)
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
let messageRequestViewWasVisible: Bool = (self?.messageRequestView.isHidden == false)
|
||||
|
||||
UIView.animate(withDuration: 0.3) {
|
||||
self?.messageRequestView.isHidden = true
|
||||
self?.scrollButtonMessageRequestsBottomConstraint?.isActive = false
|
||||
self?.scrollButtonBottomConstraint?.isActive = true
|
||||
|
||||
// Update the table content inset and offset to account for the dissapearance of
|
||||
// the messageRequestsView
|
||||
if messageRequestViewWasVisible {
|
||||
let messageRequestsOffset: CGFloat = ((self?.messageRequestView.bounds.height ?? 0) + 16)
|
||||
let oldContentInset: UIEdgeInsets = (self?.messagesTableView.contentInset ?? UIEdgeInsets.zero)
|
||||
self?.messagesTableView.contentInset = UIEdgeInsets(
|
||||
top: 0,
|
||||
leading: 0,
|
||||
bottom: max(oldContentInset.bottom - messageRequestsOffset, 0),
|
||||
trailing: 0
|
||||
GRDBStorage.shared.writeAsync(
|
||||
updates: { db in
|
||||
try contact
|
||||
.with(
|
||||
isApproved: true,
|
||||
didApproveMe: .update(contact.didApproveMe || !isNewThread)
|
||||
)
|
||||
.save(db)
|
||||
},
|
||||
completion: { db, _ in
|
||||
// Send a sync message with the details of the contact
|
||||
MessageSender.syncConfiguration(db, forceSyncNow: true).retainUntilComplete()
|
||||
|
||||
// Hide the 'messageRequestView' since the request has been approved
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
let messageRequestViewWasVisible: Bool = (self?.messageRequestView.isHidden == false)
|
||||
|
||||
UIView.animate(withDuration: 0.3) {
|
||||
self?.messageRequestView.isHidden = true
|
||||
self?.scrollButtonMessageRequestsBottomConstraint?.isActive = false
|
||||
self?.scrollButtonBottomConstraint?.isActive = true
|
||||
|
||||
// Update the table content inset and offset to account for
|
||||
// the dissapearance of the messageRequestsView
|
||||
if messageRequestViewWasVisible {
|
||||
let messageRequestsOffset: CGFloat = ((self?.messageRequestView.bounds.height ?? 0) + 16)
|
||||
let oldContentInset: UIEdgeInsets = (self?.messagesTableView.contentInset ?? UIEdgeInsets.zero)
|
||||
self?.messagesTableView.contentInset = UIEdgeInsets(
|
||||
top: 0,
|
||||
leading: 0,
|
||||
bottom: max(oldContentInset.bottom - messageRequestsOffset, 0),
|
||||
trailing: 0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Update UI
|
||||
self?.updateNavBarButtons()
|
||||
|
||||
// Remove the 'MessageRequestsViewController' from the nav hierarchy if present
|
||||
if
|
||||
let viewControllers: [UIViewController] = self?.navigationController?.viewControllers,
|
||||
let messageRequestsIndex = viewControllers.firstIndex(where: { $0 is MessageRequestsViewController }),
|
||||
messageRequestsIndex > 0
|
||||
{
|
||||
var newViewControllers = viewControllers
|
||||
newViewControllers.remove(at: messageRequestsIndex)
|
||||
self?.navigationController?.setViewControllers(newViewControllers, animated: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update UI
|
||||
self?.updateNavBarButtons()
|
||||
if let viewControllers: [UIViewController] = self?.navigationController?.viewControllers,
|
||||
let messageRequestsIndex = viewControllers.firstIndex(where: { $0 is MessageRequestsViewController }),
|
||||
messageRequestsIndex > 0 {
|
||||
var newViewControllers = viewControllers
|
||||
newViewControllers.remove(at: messageRequestsIndex)
|
||||
self?.navigationController?.setViewControllers(newViewControllers, animated: false)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1220,44 +1250,52 @@ extension ConversationVC {
|
|||
let alertVC: UIAlertController = UIAlertController(title: NSLocalizedString("MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON", comment: ""), message: nil, preferredStyle: .actionSheet)
|
||||
alertVC.addAction(UIAlertAction(title: NSLocalizedString("TXT_DELETE_TITLE", comment: ""), style: .destructive) { _ in
|
||||
// Delete the request
|
||||
Storage.write(
|
||||
with: { [weak self] transaction in
|
||||
Storage.shared.cancelPendingMessageSendJobs(for: uniqueId, using: transaction)
|
||||
|
||||
GRDBStorage.shared.writeAsync(
|
||||
updates: { [weak self] db in
|
||||
// Update the contact
|
||||
if let contactThread: TSContactThread = self?.thread as? TSContactThread {
|
||||
let sessionId: String = contactThread.contactSessionID()
|
||||
|
||||
if let contact: Contact = Storage.shared.getContact(with: sessionId) {
|
||||
// Stop observing the `BlockListDidChange` notification (we are about to pop the screen
|
||||
// so showing the banner just looks buggy)
|
||||
if let strongSelf = self {
|
||||
NotificationCenter.default.removeObserver(strongSelf, name: .contactBlockedStateChanged, object: nil)
|
||||
}
|
||||
|
||||
contact.isApproved = false
|
||||
contact.isBlocked = true
|
||||
|
||||
// Note: We set this to true so the current user will be able to send a
|
||||
// message to the person who originally sent them the message request in
|
||||
// the future if they unblock them
|
||||
contact.didApproveMe = true
|
||||
|
||||
Storage.shared.setContact(contact, using: transaction)
|
||||
// Stop observing the `BlockListDidChange` notification (we are about to pop the screen
|
||||
// so showing the banner just looks buggy)
|
||||
if let strongSelf = self {
|
||||
NotificationCenter.default.removeObserver(strongSelf, name: .contactBlockedStateChanged, object: nil)
|
||||
}
|
||||
|
||||
try? Contact
|
||||
.fetchOne(db, id: sessionId)?
|
||||
.with(
|
||||
isApproved: false,
|
||||
isBlocked: true,
|
||||
|
||||
// Note: We set this to true so the current user will be able to send a
|
||||
// message to the person who originally sent them the message request in
|
||||
// the future if they unblock them
|
||||
didApproveMe: true
|
||||
)
|
||||
.update(db)
|
||||
}
|
||||
|
||||
// Delete all thread content
|
||||
self?.thread.removeAllThreadInteractions(with: transaction)
|
||||
self?.thread.remove(with: transaction)
|
||||
},
|
||||
completion: { [weak self] in
|
||||
// Force a config sync and pop to the previous screen
|
||||
MessageSender.syncConfiguration(forceSyncNow: true).retainUntilComplete()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self?.navigationController?.popViewController(animated: true)
|
||||
}
|
||||
completion: { db, _ in
|
||||
Storage.write(
|
||||
with: { [weak self] transaction in
|
||||
// TODO: This should be above the contact updating
|
||||
Storage.shared.cancelPendingMessageSendJobs(for: uniqueId, using: transaction)
|
||||
|
||||
// Delete all thread content
|
||||
self?.thread.removeAllThreadInteractions(with: transaction)
|
||||
self?.thread.remove(with: transaction)
|
||||
},
|
||||
completion: { [weak self] in
|
||||
// Force a config sync and pop to the previous screen
|
||||
// TODO: This might cause an "incorrect thread" crash
|
||||
MessageSender.syncConfiguration(db, forceSyncNow: true).retainUntilComplete()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self?.navigationController?.popViewController(animated: true)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
|
|
@ -77,7 +77,7 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
|
|||
}
|
||||
|
||||
// Legacy account
|
||||
return Mnemonic.encode(hexEncodedString: Identity.fetchUserKeyPair()!.hexEncodedPrivateKey)
|
||||
return Mnemonic.encode(hexEncodedString: Identity.fetchUserPrivateKey()!.toHexString())
|
||||
}()
|
||||
|
||||
lazy var viewModel = ConversationViewModel(thread: thread, focusMessageIdOnOpen: nil, delegate: self)
|
||||
|
@ -150,10 +150,9 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
|
|||
lazy var blockedBanner: InfoBanner = {
|
||||
let name: String
|
||||
if let thread = thread as? TSContactThread {
|
||||
let publicKey = thread.contactSessionID()
|
||||
let context = Contact.context(for: thread)
|
||||
name = Storage.shared.getContact(with: publicKey)?.displayName(for: context) ?? publicKey
|
||||
} else {
|
||||
name = Profile.displayName(for: thread.contactSessionID(), thread: thread)
|
||||
}
|
||||
else {
|
||||
name = "Thread"
|
||||
}
|
||||
let message = "\(name) is blocked. Unblock them?"
|
||||
|
@ -378,7 +377,7 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
|
|||
|
||||
// Update the input state if this is a contact thread
|
||||
if let contactThread: TSContactThread = thread as? TSContactThread {
|
||||
let contact: Contact? = Storage.shared.getContact(with: contactThread.contactSessionID())
|
||||
let contact: Contact? = GRDBStorage.shared.read { db in try Contact.fetchOne(db, id: contactThread.contactSessionID()) }
|
||||
|
||||
// If the contact doesn't exist yet then it's a message request without the first message sent
|
||||
// so only allow text-based messages
|
||||
|
@ -473,7 +472,11 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
|
|||
else {
|
||||
if let contactThread: TSContactThread = thread as? TSContactThread {
|
||||
// Don't show the settings button for message requests
|
||||
if let contact: Contact = Storage.shared.getContact(with: contactThread.contactSessionID()), contact.isApproved, contact.didApproveMe {
|
||||
if
|
||||
let contact: Contact = GRDBStorage.shared.read({ db in try Contact.fetchOne(db, id: contactThread.contactSessionID()) }),
|
||||
contact.isApproved,
|
||||
contact.didApproveMe
|
||||
{
|
||||
let size = Values.verySmallProfilePictureSize
|
||||
let profilePictureView = ProfilePictureView()
|
||||
profilePictureView.size = size
|
||||
|
@ -657,7 +660,7 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
|
|||
|
||||
// Update the input state if this is a contact thread
|
||||
if let contactThread: TSContactThread = thread as? TSContactThread {
|
||||
let contact: Contact? = Storage.shared.getContact(with: contactThread.contactSessionID())
|
||||
let contact: Contact? = GRDBStorage.shared.read { db in try Contact.fetchOne(db, id: contactThread.contactSessionID()) }
|
||||
|
||||
// If the contact doesn't exist yet then it's a message request without the first message sent
|
||||
// so only allow text-based messages
|
||||
|
@ -718,18 +721,17 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: General
|
||||
// MARK: - General
|
||||
|
||||
@objc func addOrRemoveBlockedBanner() {
|
||||
func detach() {
|
||||
blockedBanner.removeFromSuperview()
|
||||
}
|
||||
guard let thread = thread as? TSContactThread else { return detach() }
|
||||
if thread.isBlocked() {
|
||||
view.addSubview(blockedBanner)
|
||||
blockedBanner.pin([ UIView.HorizontalEdge.left, UIView.VerticalEdge.top, UIView.HorizontalEdge.right ], to: view)
|
||||
}
|
||||
else {
|
||||
detach()
|
||||
DispatchQueue.main.async {
|
||||
guard let thread = self.thread as? TSContactThread, thread.isBlocked() else {
|
||||
self.blockedBanner.removeFromSuperview()
|
||||
return
|
||||
}
|
||||
|
||||
self.view.addSubview(self.blockedBanner)
|
||||
self.blockedBanner.pin([ UIView.HorizontalEdge.left, UIView.VerticalEdge.top, UIView.HorizontalEdge.right ], to: self.view)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
#import <SessionMessagingKit/TSThread.h>
|
||||
#import <SessionMessagingKit/TSGroupThread.h>
|
||||
#import <SessionMessagingKit/TSGroupModel.h>
|
||||
#import <SessionMessagingKit/SessionMessagingKit-Swift.h>
|
||||
#import <YapDatabase/YapDatabase.h>
|
||||
#import <YapDatabase/YapDatabaseAutoView.h>
|
||||
#import <YapDatabase/YapDatabaseViewChange.h>
|
||||
|
@ -258,11 +259,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
return SSKEnvironment.shared.tsAccountManager;
|
||||
}
|
||||
|
||||
- (OWSProfileManager *)profileManager
|
||||
{
|
||||
return [OWSProfileManager sharedManager];
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
- (void)addNotificationListeners
|
||||
|
@ -281,7 +277,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(localProfileDidChange:)
|
||||
name:kNSNotificationName_LocalProfileDidChange
|
||||
name:NSNotification.localProfileDidChange
|
||||
object:nil];
|
||||
}
|
||||
|
||||
|
@ -1271,8 +1267,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
}
|
||||
|
||||
if (shouldShowSenderName) {
|
||||
SNContactContext context = [SNContact contextForThread:self.thread];
|
||||
senderName = [[NSAttributedString alloc] initWithString:[[LKStorage.shared getContactWithSessionID:incomingSenderId] displayNameFor:context] ?: incomingSenderId];
|
||||
senderName = [[NSAttributedString alloc] initWithString:[SMKProfile displayNameWithId:incomingSenderId thread:self.thread]];
|
||||
}
|
||||
|
||||
// Show the sender profile picture for incoming group messages unless the
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import SessionUIKit
|
||||
import SessionMessagingKit
|
||||
|
||||
final class QuoteView : UIView {
|
||||
private let mode: Mode
|
||||
|
@ -180,8 +185,7 @@ final class QuoteView : UIView {
|
|||
if let groupThread = thread as? TSGroupThread {
|
||||
let authorLabel = UILabel()
|
||||
authorLabel.lineBreakMode = .byTruncatingTail
|
||||
let context: Contact.Context = groupThread.isOpenGroup ? .openGroup : .regular
|
||||
authorLabel.text = Storage.shared.getContact(with: authorID)?.displayName(for: context) ?? authorID
|
||||
authorLabel.text = Profile.displayName(for: authorID, thread: groupThread)
|
||||
authorLabel.textColor = textColor
|
||||
authorLabel.font = .boldSystemFont(ofSize: Values.smallFontSize)
|
||||
let authorLabelSize = authorLabel.systemLayoutSizeFitting(availableSpace)
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
final class InfoMessageCell : MessageCell {
|
||||
import UIKit
|
||||
import SessionUIKit
|
||||
import SessionMessagingKit
|
||||
|
||||
final class InfoMessageCell: MessageCell {
|
||||
private lazy var iconImageViewWidthConstraint = iconImageView.set(.width, to: InfoMessageCell.iconSize)
|
||||
private lazy var iconImageViewHeightConstraint = iconImageView.set(.height, to: InfoMessageCell.iconSize)
|
||||
|
||||
|
@ -48,7 +53,7 @@ final class InfoMessageCell : MessageCell {
|
|||
let icon: UIImage?
|
||||
switch message.messageType {
|
||||
case .disappearingMessagesUpdate:
|
||||
var configuration: OWSDisappearingMessagesConfiguration?
|
||||
var configuration: SessionMessagingKit.Legacy.DisappearingMessagesConfiguration?
|
||||
Storage.read { transaction in
|
||||
configuration = message.thread(with: transaction).disappearingMessagesConfiguration(with: transaction)
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import SessionUtilitiesKit
|
||||
|
||||
final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate {
|
||||
private var unloadContent: (() -> Void)?
|
||||
|
@ -351,11 +352,14 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate {
|
|||
stackView.pin(to: snContentView, withInset: inset)
|
||||
}
|
||||
case .mediaMessage:
|
||||
if viewItem.interaction is TSIncomingMessage,
|
||||
if
|
||||
viewItem.interaction is TSIncomingMessage,
|
||||
let thread = thread as? TSContactThread,
|
||||
Storage.shared.getContact(with: thread.contactSessionID())?.isTrusted != true {
|
||||
let contact: Contact? = GRDBStorage.shared.read({ db in try Contact.fetchOne(db, id: thread.contactSessionID()) }),
|
||||
contact?.isTrusted != true {
|
||||
showMediaPlaceholder()
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
guard let cache = delegate?.getMediaCache() else { preconditionFailure() }
|
||||
// Stack view
|
||||
let stackView = UIStackView(arrangedSubviews: [])
|
||||
|
@ -385,11 +389,14 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate {
|
|||
stackView.pin(to: snContentView)
|
||||
}
|
||||
case .audio:
|
||||
if viewItem.interaction is TSIncomingMessage,
|
||||
if
|
||||
viewItem.interaction is TSIncomingMessage,
|
||||
let thread = thread as? TSContactThread,
|
||||
Storage.shared.getContact(with: thread.contactSessionID())?.isTrusted != true {
|
||||
let contact: Contact? = GRDBStorage.shared.read({ db in try Contact.fetchOne(db, id: thread.contactSessionID()) }),
|
||||
contact?.isTrusted != true {
|
||||
showMediaPlaceholder()
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
let voiceMessageView = VoiceMessageView(viewItem: viewItem)
|
||||
snContentView.addSubview(voiceMessageView)
|
||||
voiceMessageView.pin(to: snContentView)
|
||||
|
@ -397,11 +404,14 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate {
|
|||
viewItem.lastAudioMessageView = voiceMessageView
|
||||
}
|
||||
case .genericAttachment:
|
||||
if viewItem.interaction is TSIncomingMessage,
|
||||
if
|
||||
viewItem.interaction is TSIncomingMessage,
|
||||
let thread = thread as? TSContactThread,
|
||||
Storage.shared.getContact(with: thread.contactSessionID())?.isTrusted != true {
|
||||
let contact: Contact? = GRDBStorage.shared.read({ db in try Contact.fetchOne(db, id: thread.contactSessionID()) }),
|
||||
contact?.isTrusted != true {
|
||||
showMediaPlaceholder()
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
let inset: CGFloat = 12
|
||||
let maxWidth = VisibleMessageCell.getMaxWidth(for: viewItem) - 2 * inset
|
||||
// Stack view
|
||||
|
|
|
@ -105,18 +105,13 @@ CGFloat kIconViewLength = 24;
|
|||
return SSKEnvironment.shared.tsAccountManager;
|
||||
}
|
||||
|
||||
- (OWSProfileManager *)profileManager
|
||||
{
|
||||
return [OWSProfileManager sharedManager];
|
||||
}
|
||||
|
||||
#pragma mark
|
||||
|
||||
- (void)observeNotifications
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(otherUsersProfileDidChange:)
|
||||
name:kNSNotificationName_OtherUsersProfileDidChange
|
||||
name:NSNotification.otherUsersProfileDidChange
|
||||
object:nil];
|
||||
}
|
||||
|
||||
|
@ -130,7 +125,7 @@ CGFloat kIconViewLength = 24;
|
|||
NSString *threadName = self.thread.name;
|
||||
if ([self.thread isKindOfClass:TSContactThread.class]) {
|
||||
TSContactThread *thread = (TSContactThread *)self.thread;
|
||||
return [[LKStorage.shared getContactWithSessionID:thread.contactSessionID] displayNameFor:SNContactContextRegular] ?: @"Anonymous";
|
||||
return [SMKProfile displayNameWithId:thread.contactSessionID customFallback: @"Anonymous"];
|
||||
} else if (threadName.length == 0 && [self isGroupThread]) {
|
||||
threadName = [MessageStrings newGroupDefaultTitle];
|
||||
}
|
||||
|
@ -235,13 +230,12 @@ CGFloat kIconViewLength = 24;
|
|||
SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, _disappearingMessagesDurationLabel);
|
||||
|
||||
self.disappearingMessagesDurations = [OWSDisappearingMessagesConfiguration validDurationsSeconds];
|
||||
|
||||
|
||||
self.disappearingMessagesConfiguration =
|
||||
[OWSDisappearingMessagesConfiguration fetchObjectWithUniqueID:self.thread.uniqueId];
|
||||
[OWSDisappearingMessagesConfiguration fetchObjectWithUniqueId:self.thread.uniqueId];
|
||||
|
||||
if (!self.disappearingMessagesConfiguration) {
|
||||
self.disappearingMessagesConfiguration =
|
||||
[[OWSDisappearingMessagesConfiguration alloc] initDefaultWithThreadId:self.thread.uniqueId];
|
||||
self.disappearingMessagesConfiguration = [OWSDisappearingMessagesConfiguration defaultWith: self.thread.uniqueId];
|
||||
}
|
||||
|
||||
[self updateTableContents];
|
||||
|
@ -361,7 +355,7 @@ CGFloat kIconViewLength = 24;
|
|||
displayName = @"the group";
|
||||
} else {
|
||||
TSContactThread *thread = (TSContactThread *)self.thread;
|
||||
displayName = [[LKStorage.shared getContactWithSessionID:thread.contactSessionID] displayNameFor:SNContactContextRegular] ?: @"anonymous";
|
||||
displayName = [SMKProfile displayNameWithId:thread.contactSessionID customFallback:@"anonymous"];
|
||||
}
|
||||
subtitleLabel.text = [NSString stringWithFormat:NSLocalizedString(@"When enabled, messages between you and %@ will disappear after they have been seen.", ""), displayName];
|
||||
subtitleLabel.textColor = LKColors.text;
|
||||
|
@ -762,7 +756,7 @@ CGFloat kIconViewLength = 24;
|
|||
[infoMessage saveWithTransaction:transaction];
|
||||
|
||||
SNExpirationTimerUpdate *expirationTimerUpdate = [SNExpirationTimerUpdate new];
|
||||
BOOL isEnabled = self.disappearingMessagesConfiguration.enabled;
|
||||
BOOL isEnabled = self.disappearingMessagesConfiguration.isEnabled;
|
||||
expirationTimerUpdate.duration = isEnabled ? self.disappearingMessagesConfiguration.durationSeconds : 0;
|
||||
[SNMessageSender send:expirationTimerUpdate inThread:self.thread usingTransaction:transaction];
|
||||
}];
|
||||
|
@ -908,7 +902,7 @@ CGFloat kIconViewLength = 24;
|
|||
|
||||
- (void)toggleDisappearingMessages:(BOOL)flag
|
||||
{
|
||||
self.disappearingMessagesConfiguration.enabled = flag;
|
||||
self.disappearingMessagesConfiguration.isEnabled = flag;
|
||||
|
||||
[self updateTableContents];
|
||||
}
|
||||
|
@ -1027,16 +1021,11 @@ CGFloat kIconViewLength = 24;
|
|||
{
|
||||
if (![self.thread isKindOfClass:TSContactThread.class]) { return; }
|
||||
NSString *sessionID = ((TSContactThread *)self.thread).contactSessionID;
|
||||
SNContact *contact = [LKStorage.shared getContactWithSessionID:sessionID];
|
||||
if (contact == nil) {
|
||||
contact = [[SNContact alloc] initWithSessionID:sessionID];
|
||||
}
|
||||
SMKProfile *profile = [SMKProfile fetchOrCreateWithId:sessionID];
|
||||
NSString *text = [self.displayNameTextField.text stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet];
|
||||
contact.nickname = text.length > 0 ? text : nil;
|
||||
[LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
[LKStorage.shared setContact:contact usingTransaction:transaction];
|
||||
}];
|
||||
self.displayNameLabel.text = text.length > 0 ? text : contact.name;
|
||||
profile.nickname = text.length > 0 ? text : nil;
|
||||
[SMKProfile saveProfile: profile];
|
||||
self.displayNameLabel.text = text.length > 0 ? text : profile.name;
|
||||
[self hideEditNameUI];
|
||||
}
|
||||
|
||||
|
@ -1069,7 +1058,7 @@ CGFloat kIconViewLength = 24;
|
|||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
NSString *recipientId = notification.userInfo[kNSNotificationKey_ProfileRecipientId];
|
||||
NSString *recipientId = notification.userInfo[NSNotification.profileRecipientIdKey];
|
||||
OWSAssertDebug(recipientId.length > 0);
|
||||
|
||||
if (recipientId.length > 0 && [self.thread isKindOfClass:[TSContactThread class]] &&
|
||||
|
|
|
@ -1,3 +1,9 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import GRDB
|
||||
import SessionUIKit
|
||||
import SessionUtilitiesKit
|
||||
import SessionMessagingKit
|
||||
|
||||
final class BlockedModal: Modal {
|
||||
|
@ -19,7 +25,7 @@ final class BlockedModal: Modal {
|
|||
|
||||
override func populateContentView() {
|
||||
// Name
|
||||
let name = Storage.shared.getContact(with: publicKey)?.displayName(for: .regular) ?? publicKey
|
||||
let name = Profile.displayName(for: publicKey)
|
||||
// Title
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.textColor = Colors.text
|
||||
|
@ -66,17 +72,15 @@ final class BlockedModal: Modal {
|
|||
@objc private func unblock() {
|
||||
let publicKey: String = self.publicKey
|
||||
|
||||
Storage.shared.write(
|
||||
with: { transaction in
|
||||
guard let transaction = transaction as? YapDatabaseReadWriteTransaction, let contact: Contact = Storage.shared.getContact(with: publicKey, using: transaction) else {
|
||||
return
|
||||
}
|
||||
|
||||
contact.isBlocked = false
|
||||
Storage.shared.setContact(contact, using: transaction as Any)
|
||||
GRDBStorage.shared.writeAsync(
|
||||
updates: { db in
|
||||
try? Contact
|
||||
.fetchOne(db, id: publicKey)?
|
||||
.with(isBlocked: true)
|
||||
.update(db)
|
||||
},
|
||||
completion: {
|
||||
MessageSender.syncConfiguration(forceSyncNow: true).retainUntilComplete()
|
||||
completion: { db, _ in
|
||||
MessageSender.syncConfiguration(db, forceSyncNow: true).retainUntilComplete()
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
@ -52,7 +52,7 @@ final class ConversationTitleView : UIView {
|
|||
let notificationCenter = NotificationCenter.default
|
||||
notificationCenter.addObserver(self, selector: #selector(update), name: Notification.Name.groupThreadUpdated, object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(update), name: Notification.Name.muteSettingUpdated, object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(update), name: Notification.Name.contactUpdated, object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(update), name: Notification.Name.profileUpdated, object: nil)
|
||||
update()
|
||||
}
|
||||
|
||||
|
@ -62,11 +62,18 @@ final class ConversationTitleView : UIView {
|
|||
|
||||
// MARK: Updating
|
||||
@objc private func update() {
|
||||
titleLabel.text = getTitle()
|
||||
let subtitle = getSubtitle()
|
||||
subtitleLabel.attributedText = subtitle
|
||||
let titleFontSize = (subtitle != nil) ? Values.mediumFontSize : Values.veryLargeFontSize
|
||||
titleLabel.font = .boldSystemFont(ofSize: titleFontSize)
|
||||
DispatchQueue.main.async {
|
||||
self.titleLabel.text = self.getTitle()
|
||||
|
||||
let subtitle: NSAttributedString? = self.getSubtitle()
|
||||
self.subtitleLabel.attributedText = subtitle
|
||||
self.titleLabel.font = .boldSystemFont(
|
||||
ofSize: (subtitle != nil ?
|
||||
Values.mediumFontSize :
|
||||
Values.veryLargeFontSize
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: General
|
||||
|
@ -79,13 +86,9 @@ final class ConversationTitleView : UIView {
|
|||
}
|
||||
else {
|
||||
let sessionID = (thread as! TSContactThread).contactSessionID()
|
||||
var result = sessionID
|
||||
Storage.read { transaction in
|
||||
let displayName: String = ((Storage.shared.getContact(with: sessionID)?.displayName(for: .regular)) ?? sessionID)
|
||||
let middleTruncatedHexKey: String = "\(sessionID.prefix(4))...\(sessionID.suffix(4))"
|
||||
result = (displayName == sessionID ? middleTruncatedHexKey : displayName)
|
||||
}
|
||||
return result
|
||||
let middleTruncatedHexKey: String = "\(sessionID.prefix(4))...\(sessionID.suffix(4))"
|
||||
|
||||
return Profile.displayName(for: sessionID, customFallback: middleTruncatedHexKey)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,10 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import GRDB
|
||||
import SessionUIKit
|
||||
import SessionUtilitiesKit
|
||||
import SessionMessagingKit
|
||||
|
||||
final class DownloadAttachmentModal : Modal {
|
||||
private let viewItem: ConversationViewItem
|
||||
|
@ -19,7 +26,7 @@ final class DownloadAttachmentModal : Modal {
|
|||
override func populateContentView() {
|
||||
guard let publicKey = (viewItem.interaction as? TSIncomingMessage)?.authorId else { return }
|
||||
// Name
|
||||
let name = Storage.shared.getContact(with: publicKey)?.displayName(for: .regular) ?? publicKey
|
||||
let name = Profile.displayName(for: publicKey)
|
||||
// Title
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.textColor = Colors.text
|
||||
|
@ -65,15 +72,23 @@ final class DownloadAttachmentModal : Modal {
|
|||
// MARK: Interaction
|
||||
@objc private func trust() {
|
||||
guard let message = viewItem.interaction as? TSIncomingMessage else { return }
|
||||
let publicKey = message.authorId
|
||||
let contact = Storage.shared.getContact(with: publicKey) ?? Contact(sessionID: publicKey)
|
||||
contact.isTrusted = true
|
||||
Storage.write(with: { transaction in
|
||||
Storage.shared.setContact(contact, using: transaction)
|
||||
MessageInvalidator.invalidate(message, with: transaction)
|
||||
}, completion: {
|
||||
Storage.shared.resumeAttachmentDownloadJobsIfNeeded(for: message.uniqueThreadId)
|
||||
})
|
||||
|
||||
GRDBStorage.shared.writeAsync(
|
||||
updates: { db in
|
||||
try? Contact
|
||||
.fetchOrCreate(db, id: message.authorId)
|
||||
.with(isTrusted: true)
|
||||
.save(db)
|
||||
},
|
||||
completion: { _, _ in
|
||||
Storage.write(with: { transaction in
|
||||
MessageInvalidator.invalidate(message, with: transaction)
|
||||
}, completion: {
|
||||
Storage.shared.resumeAttachmentDownloadJobsIfNeeded(for: message.uniqueThreadId)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
presentingViewController?.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -72,7 +72,9 @@ final class JoinOpenGroupModal : Modal {
|
|||
Storage.shared.write { [presentingViewController = self.presentingViewController!] transaction in
|
||||
OpenGroupManagerV2.shared.add(room: room, server: server, publicKey: publicKey, using: transaction)
|
||||
.done(on: DispatchQueue.main) { _ in
|
||||
MessageSender.syncConfiguration(forceSyncNow: true).retainUntilComplete() // FIXME: It's probably cleaner to do this inside addOpenGroup(...)
|
||||
GRDBStorage.shared.write { db in
|
||||
MessageSender.syncConfiguration(db, forceSyncNow: true).retainUntilComplete() // FIXME: It's probably cleaner to do this inside addOpenGroup(...)
|
||||
}
|
||||
}
|
||||
.catch(on: DispatchQueue.main) { error in
|
||||
let alert = UIAlertController(title: "Couldn't Join", message: error.localizedDescription, preferredStyle: .alert)
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
final class UserDetailsSheet : Sheet {
|
||||
import UIKit
|
||||
import SessionMessagingKit
|
||||
|
||||
final class UserDetailsSheet: Sheet {
|
||||
private let sessionID: String
|
||||
|
||||
init(for sessionID: String) {
|
||||
|
@ -26,7 +30,7 @@ final class UserDetailsSheet : Sheet {
|
|||
profilePictureView.update()
|
||||
// Display name label
|
||||
let displayNameLabel = UILabel()
|
||||
let displayName = Storage.shared.getContact(with: sessionID)?.displayName(for: .regular) ?? sessionID
|
||||
let displayName = Profile.displayName(for: sessionID)
|
||||
displayNameLabel.text = displayName
|
||||
displayNameLabel.font = .boldSystemFont(ofSize: Values.largeFontSize)
|
||||
displayNameLabel.textColor = Colors.text
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import Curve25519Kit
|
||||
import SessionUtilitiesKit
|
||||
|
||||
final class NewDMVC : BaseVC, UIPageViewControllerDataSource, UIPageViewControllerDelegate, OWSQRScannerDelegate {
|
||||
private let pageVC = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import GRDB
|
||||
import SessionMessagingKit
|
||||
import SessionUtilitiesKit
|
||||
|
||||
|
@ -155,8 +156,8 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
|
|||
// Notifications
|
||||
let notificationCenter = NotificationCenter.default
|
||||
notificationCenter.addObserver(self, selector: #selector(handleYapDatabaseModifiedNotification(_:)), name: .YapDatabaseModified, object: OWSPrimaryStorage.shared().dbNotificationObject)
|
||||
notificationCenter.addObserver(self, selector: #selector(handleProfileDidChangeNotification(_:)), name: NSNotification.Name(rawValue: kNSNotificationName_OtherUsersProfileDidChange), object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(handleLocalProfileDidChangeNotification(_:)), name: Notification.Name(kNSNotificationName_LocalProfileDidChange), object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(handleProfileDidChangeNotification(_:)), name: Notification.Name.otherUsersProfileDidChange, object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(handleLocalProfileDidChangeNotification(_:)), name: Notification.Name.localProfileDidChange, object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(handleSeedViewedNotification(_:)), name: .seedViewed, object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(handleBlockedContactsUpdatedNotification(_:)), name: .blockedContactsUpdated, object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(applicationDidBecomeActive(_:)), name: .OWSApplicationDidBecomeActive, object: nil)
|
||||
|
@ -167,7 +168,7 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
|
|||
self.threads.update(with: transaction) // Perform the initial update
|
||||
}
|
||||
// Start polling if needed (i.e. if the user just created or restored their Session ID)
|
||||
if Identity.fetchUserKeyPair() != nil {
|
||||
if Identity.userExists() {
|
||||
let appDelegate = UIApplication.shared.delegate as! AppDelegate
|
||||
appDelegate.startPollerIfNeeded()
|
||||
appDelegate.startClosedGroupPoller()
|
||||
|
@ -399,7 +400,9 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
|
|||
}
|
||||
|
||||
@objc private func handleLocalProfileDidChangeNotification(_ notification: Notification) {
|
||||
updateNavBarButtons()
|
||||
DispatchQueue.main.async {
|
||||
self.updateNavBarButtons()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func handleSeedViewedNotification(_ notification: Notification) {
|
||||
|
@ -531,40 +534,48 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
|
|||
let publicKey = thread.contactSessionID()
|
||||
|
||||
let block = UITableViewRowAction(style: .normal, title: NSLocalizedString("BLOCK_LIST_BLOCK_BUTTON", comment: "")) { _, _ in
|
||||
Storage.shared.write(
|
||||
with: { transaction in
|
||||
guard let transaction = transaction as? YapDatabaseReadWriteTransaction, let contact: Contact = Storage.shared.getContact(with: publicKey, using: transaction) else {
|
||||
return
|
||||
}
|
||||
|
||||
contact.isBlocked = true
|
||||
Storage.shared.setContact(contact, using: transaction as Any)
|
||||
GRDBStorage.shared.writeAsync(
|
||||
updates: { db in
|
||||
try Contact
|
||||
.fetchOrCreate(db, id: publicKey)
|
||||
.with(isBlocked: true)
|
||||
.save(db)
|
||||
},
|
||||
completion: {
|
||||
MessageSender.syncConfiguration(forceSyncNow: true).retainUntilComplete()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
tableView.reloadRows(at: [ indexPath ], with: UITableView.RowAnimation.fade)
|
||||
completion: { db, result in
|
||||
switch result {
|
||||
case .success:
|
||||
MessageSender.syncConfiguration(db, forceSyncNow: true)
|
||||
.retainUntilComplete()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
tableView.reloadRows(at: [ indexPath ], with: UITableView.RowAnimation.fade)
|
||||
}
|
||||
|
||||
default: break
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
block.backgroundColor = Colors.unimportant
|
||||
let unblock = UITableViewRowAction(style: .normal, title: NSLocalizedString("BLOCK_LIST_UNBLOCK_BUTTON", comment: "")) { _, _ in
|
||||
Storage.shared.write(
|
||||
with: { transaction in
|
||||
guard let transaction = transaction as? YapDatabaseReadWriteTransaction, let contact: Contact = Storage.shared.getContact(with: publicKey, using: transaction) else {
|
||||
return
|
||||
}
|
||||
|
||||
contact.isBlocked = false
|
||||
Storage.shared.setContact(contact, using: transaction as Any)
|
||||
GRDBStorage.shared.writeAsync(
|
||||
updates: { db in
|
||||
try Contact
|
||||
.fetchOrCreate(db, id: publicKey)
|
||||
.with(isBlocked: false)
|
||||
.save(db)
|
||||
},
|
||||
completion: {
|
||||
MessageSender.syncConfiguration(forceSyncNow: true).retainUntilComplete()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
tableView.reloadRows(at: [ indexPath ], with: UITableView.RowAnimation.fade)
|
||||
completion: { db, result in
|
||||
switch result {
|
||||
case .success:
|
||||
MessageSender.syncConfiguration(db, forceSyncNow: true)
|
||||
.retainUntilComplete()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
tableView.reloadRows(at: [ indexPath ], with: UITableView.RowAnimation.fade)
|
||||
}
|
||||
|
||||
default: break
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import GRDB
|
||||
import SessionUIKit
|
||||
import SessionMessagingKit
|
||||
|
||||
|
@ -107,7 +108,7 @@ class MessageRequestsViewController: BaseVC, UITableViewDelegate, UITableViewDat
|
|||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(handleProfileDidChangeNotification(_:)),
|
||||
name: NSNotification.Name(rawValue: kNSNotificationName_OtherUsersProfileDidChange),
|
||||
name: Notification.Name.otherUsersProfileDidChange,
|
||||
object: nil
|
||||
)
|
||||
NotificationCenter.default.addObserver(
|
||||
|
@ -302,32 +303,6 @@ class MessageRequestsViewController: BaseVC, UITableViewDelegate, UITableViewDat
|
|||
|
||||
// MARK: - Interaction
|
||||
|
||||
private func updateContactAndThread(thread: TSThread, with transaction: YapDatabaseReadWriteTransaction, onComplete: ((Bool) -> ())? = nil) {
|
||||
guard let contactThread: TSContactThread = thread as? TSContactThread else {
|
||||
onComplete?(false)
|
||||
return
|
||||
}
|
||||
|
||||
var needsSync: Bool = false
|
||||
|
||||
// Update the contact
|
||||
let sessionId: String = contactThread.contactSessionID()
|
||||
|
||||
if let contact: Contact = Storage.shared.getContact(with: sessionId), (contact.isApproved || !contact.isBlocked) {
|
||||
contact.isApproved = false
|
||||
contact.isBlocked = true
|
||||
|
||||
Storage.shared.setContact(contact, using: transaction)
|
||||
needsSync = true
|
||||
}
|
||||
|
||||
// Delete all thread content
|
||||
thread.removeAllThreadInteractions(with: transaction)
|
||||
thread.remove(with: transaction)
|
||||
|
||||
onComplete?(needsSync)
|
||||
}
|
||||
|
||||
@objc private func clearAllTapped() {
|
||||
let threadCount: Int = Int(messageRequestCount)
|
||||
let threads: [TSThread] = (0..<threadCount).compactMap { self.thread(at: $0) }
|
||||
|
@ -336,36 +311,47 @@ class MessageRequestsViewController: BaseVC, UITableViewDelegate, UITableViewDat
|
|||
let alertVC: UIAlertController = UIAlertController(title: NSLocalizedString("MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE", comment: ""), message: nil, preferredStyle: .actionSheet)
|
||||
alertVC.addAction(UIAlertAction(title: NSLocalizedString("MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON", comment: ""), style: .destructive) { _ in
|
||||
// Clear the requests
|
||||
Storage.write(
|
||||
with: { [weak self] transaction in
|
||||
|
||||
GRDBStorage.shared.writeAsync(
|
||||
updates: { db in
|
||||
threads.forEach { thread in
|
||||
if let uniqueId: String = thread.uniqueId {
|
||||
Storage.shared.cancelPendingMessageSendJobs(for: uniqueId, using: transaction)
|
||||
}
|
||||
|
||||
self?.updateContactAndThread(thread: thread, with: transaction) { threadNeedsSync in
|
||||
if threadNeedsSync {
|
||||
needsSync = true
|
||||
}
|
||||
}
|
||||
|
||||
// Block the contact
|
||||
if
|
||||
guard
|
||||
let sessionId: String = (thread as? TSContactThread)?.contactSessionID(),
|
||||
!thread.isBlocked(),
|
||||
let contact: Contact = Storage.shared.getContact(with: sessionId, using: transaction)
|
||||
{
|
||||
contact.isBlocked = true
|
||||
Storage.shared.setContact(contact, using: transaction)
|
||||
needsSync = true
|
||||
}
|
||||
let contact: Contact = try? Contact.fetchOrCreate(db, id: sessionId),
|
||||
!contact.isBlocked
|
||||
else { return }
|
||||
|
||||
try? contact
|
||||
.with(
|
||||
isApproved: false,
|
||||
isBlocked: true
|
||||
)
|
||||
.save(db)
|
||||
|
||||
needsSync = true
|
||||
}
|
||||
},
|
||||
completion: {
|
||||
// Force a config sync
|
||||
if needsSync {
|
||||
MessageSender.syncConfiguration(forceSyncNow: true).retainUntilComplete()
|
||||
}
|
||||
completion: { db, _ in
|
||||
Storage.write(
|
||||
with: { transaction in
|
||||
threads.forEach { thread in
|
||||
if let uniqueId: String = thread.uniqueId {
|
||||
Storage.shared.cancelPendingMessageSendJobs(for: uniqueId, using: transaction)
|
||||
}
|
||||
|
||||
// Delete all thread content
|
||||
thread.removeAllThreadInteractions(with: transaction)
|
||||
thread.remove(with: transaction)
|
||||
}
|
||||
},
|
||||
completion: {
|
||||
// Force a config sync
|
||||
if needsSync {
|
||||
// TODO: This might crash due to a "wrong thread" error
|
||||
MessageSender.syncConfiguration(db, forceSyncNow: true).retainUntilComplete()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
@ -378,24 +364,36 @@ class MessageRequestsViewController: BaseVC, UITableViewDelegate, UITableViewDat
|
|||
|
||||
let alertVC: UIAlertController = UIAlertController(title: NSLocalizedString("MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON", comment: ""), message: nil, preferredStyle: .actionSheet)
|
||||
alertVC.addAction(UIAlertAction(title: NSLocalizedString("TXT_DELETE_TITLE", comment: ""), style: .destructive) { _ in
|
||||
Storage.write(
|
||||
with: { [weak self] transaction in
|
||||
Storage.shared.cancelPendingMessageSendJobs(for: uniqueId, using: transaction)
|
||||
self?.updateContactAndThread(thread: thread, with: transaction)
|
||||
|
||||
// Block the contact
|
||||
if
|
||||
GRDBStorage.shared.writeAsync(
|
||||
updates: { db in
|
||||
guard
|
||||
let sessionId: String = (thread as? TSContactThread)?.contactSessionID(),
|
||||
!thread.isBlocked(),
|
||||
let contact: Contact = Storage.shared.getContact(with: sessionId, using: transaction)
|
||||
{
|
||||
contact.isBlocked = true
|
||||
Storage.shared.setContact(contact, using: transaction)
|
||||
}
|
||||
let contact: Contact = try? Contact.fetchOrCreate(db, id: sessionId),
|
||||
!contact.isBlocked
|
||||
else { return }
|
||||
|
||||
try? contact
|
||||
.with(
|
||||
isApproved: false,
|
||||
isBlocked: true
|
||||
)
|
||||
.save(db)
|
||||
},
|
||||
completion: {
|
||||
// Force a config sync
|
||||
MessageSender.syncConfiguration(forceSyncNow: true).retainUntilComplete()
|
||||
completion: { db, _ in
|
||||
Storage.write(
|
||||
with: { transaction in
|
||||
Storage.shared.cancelPendingMessageSendJobs(for: uniqueId, using: transaction)
|
||||
|
||||
// Delete all thread content
|
||||
thread.removeAllThreadInteractions(with: transaction)
|
||||
thread.remove(with: transaction)
|
||||
},
|
||||
completion: {
|
||||
// Force a config sync
|
||||
// TODO: This might crash due to a "wrong thread" error
|
||||
MessageSender.syncConfiguration(db, forceSyncNow: true).retainUntilComplete()
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
|
|
@ -680,9 +680,7 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
|
|||
private func senderName(message: TSMessage) -> String {
|
||||
switch message {
|
||||
case let incomingMessage as TSIncomingMessage:
|
||||
let publicKey = incomingMessage.authorId
|
||||
let context = Contact.context(for: incomingMessage.thread)
|
||||
return Storage.shared.getContact(with: publicKey)?.displayName(for: context) ?? publicKey
|
||||
return Profile.displayName(for: incomingMessage.authorId, thread: incomingMessage.thread)
|
||||
case is TSOutgoingMessage:
|
||||
return NSLocalizedString("MEDIA_GALLERY_SENDER_NAME_YOU", comment: "Short sender label for media sent by you")
|
||||
default:
|
||||
|
|
|
@ -56,11 +56,6 @@ static NSTimeInterval launchStartedAt;
|
|||
|
||||
#pragma mark - Dependencies
|
||||
|
||||
- (OWSProfileManager *)profileManager
|
||||
{
|
||||
return [OWSProfileManager sharedManager];
|
||||
}
|
||||
|
||||
- (OWSReadReceiptManager *)readReceiptManager
|
||||
{
|
||||
return [OWSReadReceiptManager sharedManager];
|
||||
|
@ -365,14 +360,10 @@ static NSTimeInterval launchStartedAt;
|
|||
NSDate *now = [NSDate new];
|
||||
NSDate *lastProfilePictureUpload = (NSDate *)[userDefaults objectForKey:@"lastProfilePictureUpload"];
|
||||
if (lastProfilePictureUpload != nil && [now timeIntervalSinceDate:lastProfilePictureUpload] > 14 * 24 * 60 * 60) {
|
||||
OWSProfileManager *profileManager = OWSProfileManager.sharedManager;
|
||||
NSString *name = [[LKStorage.shared getUser] name];
|
||||
UIImage *profilePicture = [profileManager profileAvatarForRecipientId:userPublicKey];
|
||||
[profileManager updateLocalProfileName:name avatarImage:profilePicture success:^{
|
||||
// Do nothing; the user defaults flag is updated in LokiFileServerAPI
|
||||
} failure:^(NSError *error) {
|
||||
// Do nothing
|
||||
} requiresSync:YES];
|
||||
// The user defaults flag is updated in ProfileManager
|
||||
NSString *name = [SMKProfile fetchCurrentUserName];
|
||||
UIImage *profilePicture = [SMKProfileManager profileAvatarWithRecipientId:userPublicKey];
|
||||
[SMKProfileManager updateLocalWithProfileName:name avatarImage:profilePicture requiresSync:YES];
|
||||
}
|
||||
|
||||
if (CurrentAppContext().isMainApp) {
|
||||
|
|
|
@ -9,24 +9,26 @@ extension AppDelegate {
|
|||
|
||||
@objc(syncConfigurationIfNeeded)
|
||||
func syncConfigurationIfNeeded() {
|
||||
guard Storage.shared.getUser()?.name != nil else { return }
|
||||
let userDefaults = UserDefaults.standard
|
||||
let lastSync = userDefaults[.lastConfigurationSync] ?? .distantPast
|
||||
guard Date().timeIntervalSince(lastSync) > 7 * 24 * 60 * 60 else { return } // Sync every 2 days
|
||||
let lastSync: Date = (UserDefaults.standard[.lastConfigurationSync] ?? .distantPast)
|
||||
|
||||
MessageSender.syncConfiguration(forceSyncNow: false)
|
||||
.done {
|
||||
// Only update the 'lastConfigurationSync' timestamp if we have done the first sync (Don't want
|
||||
// a new device config sync to override config syncs from other devices)
|
||||
if userDefaults[.hasSyncedInitialConfiguration] {
|
||||
userDefaults[.lastConfigurationSync] = Date()
|
||||
guard Date().timeIntervalSince(lastSync) > (7 * 24 * 60 * 60) else { return } // Sync every 2 days
|
||||
|
||||
GRDBStorage.shared.write { db in
|
||||
MessageSender.syncConfiguration(db, forceSyncNow: false)
|
||||
.done {
|
||||
// Only update the 'lastConfigurationSync' timestamp if we have done the
|
||||
// first sync (Don't want a new device config sync to override config
|
||||
// syncs from other devices)
|
||||
if UserDefaults.standard[.hasSyncedInitialConfiguration] {
|
||||
UserDefaults.standard[.lastConfigurationSync] = Date()
|
||||
}
|
||||
}
|
||||
}
|
||||
.retainUntilComplete()
|
||||
.retainUntilComplete()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func startClosedGroupPoller() {
|
||||
guard Identity.fetchUserKeyPair() != nil else { return }
|
||||
guard Identity.userExists() else { return }
|
||||
|
||||
ClosedGroupPoller.shared.start()
|
||||
}
|
||||
|
|
|
@ -197,8 +197,7 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
|||
return
|
||||
}
|
||||
|
||||
let context = Contact.context(for: thread)
|
||||
let senderName = Storage.shared.getContact(with: incomingMessage.authorId, using: transaction)?.displayName(for: context) ?? incomingMessage.authorId
|
||||
let senderName = Profile.displayName(for: incomingMessage.authorId, thread: incomingMessage.thread)
|
||||
|
||||
let notificationTitle: String?
|
||||
var notificationBody: String?
|
||||
|
|
|
@ -131,10 +131,12 @@ final class DisplayNameVC : BaseVC {
|
|||
guard !displayName.isEmpty else {
|
||||
return showError(title: NSLocalizedString("vc_display_name_display_name_missing_error", comment: ""))
|
||||
}
|
||||
guard !OWSProfileManager.shared().isProfileNameTooLong(displayName) else {
|
||||
guard !ProfileManager.isToLong(profileName: displayName) else {
|
||||
return showError(title: NSLocalizedString("vc_display_name_display_name_too_long_error", comment: ""))
|
||||
}
|
||||
OWSProfileManager.shared().updateLocalProfileName(displayName, avatarImage: nil, success: { }, failure: { _ in }, requiresSync: false) // Try to save the user name but ignore the result
|
||||
|
||||
// Try to save the user name but ignore the result
|
||||
ProfileManager.updateLocal(profileName: displayName, avatarImage: nil, requiredSync: false)
|
||||
let pnModeVC = PNModeVC()
|
||||
navigationController!.pushViewController(pnModeVC, animated: true)
|
||||
}
|
||||
|
|
|
@ -2,7 +2,10 @@
|
|||
|
||||
import Foundation
|
||||
import Sodium
|
||||
import GRDB
|
||||
import Curve25519Kit
|
||||
import SessionUtilitiesKit
|
||||
import SessionMessagingKit
|
||||
|
||||
enum Onboarding {
|
||||
|
||||
|
@ -14,11 +17,13 @@ enum Onboarding {
|
|||
Identity.store(seed: seed, ed25519KeyPair: ed25519KeyPair, x25519KeyPair: x25519KeyPair)
|
||||
let x25519PublicKey = x25519KeyPair.hexEncodedPublicKey
|
||||
TSAccountManager.sharedInstance().phoneNumberAwaitingVerification = x25519PublicKey
|
||||
Storage.writeSync { transaction in
|
||||
let user = Contact(sessionID: x25519PublicKey)
|
||||
user.isApproved = true
|
||||
user.didApproveMe = true
|
||||
Storage.shared.setContact(user, using: transaction)
|
||||
GRDBStorage.shared.write { db in
|
||||
try Contact(id: x25519PublicKey)
|
||||
.with(
|
||||
isApproved: true,
|
||||
didApproveMe: true
|
||||
)
|
||||
.save(db)
|
||||
}
|
||||
|
||||
switch self {
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import Sodium
|
||||
import Curve25519Kit
|
||||
|
||||
final class RegisterVC : BaseVC {
|
||||
private var seed: Data! { didSet { updateKeyPair() } }
|
||||
|
|
|
@ -10,7 +10,7 @@ final class SeedVC: BaseVC {
|
|||
}
|
||||
|
||||
// Legacy account
|
||||
return Mnemonic.encode(hexEncodedString: Identity.fetchUserKeyPair()!.hexEncodedPrivateKey)
|
||||
return Mnemonic.encode(hexEncodedString: Identity.fetchUserPrivateKey()!.toHexString())
|
||||
}()
|
||||
|
||||
private lazy var redactedMnemonic: String = {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import SessionUtilitiesKit
|
||||
|
||||
final class JoinOpenGroupVC : BaseVC, UIPageViewControllerDataSource, UIPageViewControllerDelegate, OWSQRScannerDelegate {
|
||||
private let pageVC = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
|
||||
|
@ -143,8 +144,11 @@ final class JoinOpenGroupVC : BaseVC, UIPageViewControllerDataSource, UIPageView
|
|||
Storage.shared.write { transaction in
|
||||
OpenGroupManagerV2.shared.add(room: room, server: server, publicKey: publicKey, using: transaction)
|
||||
.done(on: DispatchQueue.main) { [weak self] _ in
|
||||
GRDBStorage.shared.write { db in
|
||||
MessageSender.syncConfiguration(db, forceSyncNow: true).retainUntilComplete() // FIXME: It's probably cleaner to do this inside addOpenGroup(...)
|
||||
}
|
||||
|
||||
self?.presentingViewController?.dismiss(animated: true, completion: nil)
|
||||
MessageSender.syncConfiguration(forceSyncNow: true).retainUntilComplete() // FIXME: It's probably cleaner to do this inside addOpenGroup(...)
|
||||
}
|
||||
.catch(on: DispatchQueue.main) { [weak self] error in
|
||||
self?.dismiss(animated: true, completion: nil) // Dismiss the loader
|
||||
|
|
|
@ -124,12 +124,16 @@ final class NukeDataModal : Modal {
|
|||
|
||||
@objc private func clearDeviceOnly() {
|
||||
ModalActivityIndicatorViewController.present(fromViewController: self, canCancel: false) { [weak self] _ in
|
||||
MessageSender.syncConfiguration(forceSyncNow: true).ensure(on: DispatchQueue.main) {
|
||||
self?.dismiss(animated: true, completion: nil) // Dismiss the loader
|
||||
UserDefaults.removeAll() // Not done in the nuke data implementation as unlinking requires this to happen later
|
||||
General.Cache.cachedEncodedPublicKey.mutate { $0 = nil } // Remove the cached key so it gets re-cached on next access
|
||||
NotificationCenter.default.post(name: .dataNukeRequested, object: nil)
|
||||
}.retainUntilComplete()
|
||||
GRDBStorage.shared.write { db in
|
||||
MessageSender.syncConfiguration(db, forceSyncNow: true)
|
||||
.ensure(on: DispatchQueue.main) {
|
||||
self?.dismiss(animated: true, completion: nil) // Dismiss the loader
|
||||
UserDefaults.removeAll() // Not done in the nuke data implementation as unlinking requires this to happen later
|
||||
General.Cache.cachedEncodedPublicKey.mutate { $0 = nil } // Remove the cached key so it gets re-cached on next access
|
||||
NotificationCenter.default.post(name: .dataNukeRequested, object: nil)
|
||||
}
|
||||
.retainUntilComplete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import Curve25519Kit
|
||||
import SessionUtilitiesKit
|
||||
|
||||
final class QRCodeVC : BaseVC, UIPageViewControllerDataSource, UIPageViewControllerDelegate, OWSQRScannerDelegate {
|
||||
private let pageVC = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
|
||||
|
|
|
@ -12,7 +12,7 @@ final class SeedModal: Modal {
|
|||
}
|
||||
|
||||
// Legacy account
|
||||
return Mnemonic.encode(hexEncodedString: Identity.fetchUserKeyPair()!.hexEncodedPrivateKey)
|
||||
return Mnemonic.encode(hexEncodedString: Identity.fetchUserPrivateKey()!.toHexString())
|
||||
}()
|
||||
|
||||
// MARK: Lifecycle
|
||||
|
|
|
@ -147,7 +147,7 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
|
|||
profilePictureView.publicKey = getUserHexEncodedPublicKey()
|
||||
profilePictureView.update()
|
||||
// Display name label
|
||||
displayNameLabel.text = Storage.shared.getUser()?.name
|
||||
displayNameLabel.text = Profile.fetchOrCreateCurrentUser().name
|
||||
// Display name container
|
||||
let displayNameContainer = UIView()
|
||||
displayNameContainer.accessibilityLabel = "Edit display name text field"
|
||||
|
@ -346,7 +346,7 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
|
|||
}
|
||||
|
||||
func avatarDidChange(_ image: UIImage) {
|
||||
let maxSize = Int(kOWSProfileManager_MaxAvatarDiameter)
|
||||
let maxSize = Int(ProfileManager.maxAvatarDiameter)
|
||||
profilePictureToBeUploaded = image.resizedImage(toFillPixelSize: CGSize(width: maxSize, height: maxSize))
|
||||
updateProfile(isUpdatingDisplayName: false, isUpdatingProfilePicture: true)
|
||||
}
|
||||
|
@ -358,41 +358,47 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
|
|||
|
||||
private func updateProfile(isUpdatingDisplayName: Bool, isUpdatingProfilePicture: Bool) {
|
||||
let userDefaults = UserDefaults.standard
|
||||
let name = displayNameToBeUploaded ?? Storage.shared.getUser()?.name
|
||||
let profilePicture = profilePictureToBeUploaded ?? OWSProfileManager.shared().profileAvatar(forRecipientId: getUserHexEncodedPublicKey())
|
||||
let name: String? = (displayNameToBeUploaded ?? Profile.fetchOrCreateCurrentUser().name)
|
||||
let profilePicture: UIImage? = (profilePictureToBeUploaded ?? ProfileManager.profileAvatar(for: getUserHexEncodedPublicKey()))
|
||||
ModalActivityIndicatorViewController.present(fromViewController: navigationController!, canCancel: false) { [weak self, displayNameToBeUploaded, profilePictureToBeUploaded] modalActivityIndicator in
|
||||
OWSProfileManager.shared().updateLocalProfileName(name, avatarImage: profilePicture, success: {
|
||||
if displayNameToBeUploaded != nil {
|
||||
userDefaults[.lastDisplayNameUpdate] = Date()
|
||||
}
|
||||
if profilePictureToBeUploaded != nil {
|
||||
userDefaults[.lastProfilePictureUpdate] = Date()
|
||||
}
|
||||
MessageSender.syncConfiguration(forceSyncNow: true).retainUntilComplete()
|
||||
DispatchQueue.main.async {
|
||||
modalActivityIndicator.dismiss {
|
||||
guard let self = self else { return }
|
||||
self.profilePictureView.update()
|
||||
self.displayNameLabel.text = name
|
||||
self.profilePictureToBeUploaded = nil
|
||||
self.displayNameToBeUploaded = nil
|
||||
ProfileManager.updateLocal(
|
||||
profileName: (name ?? ""),
|
||||
avatarImage: profilePicture,
|
||||
requiredSync: true,
|
||||
success: {
|
||||
if displayNameToBeUploaded != nil {
|
||||
userDefaults[.lastDisplayNameUpdate] = Date()
|
||||
}
|
||||
}
|
||||
}, failure: { error in
|
||||
DispatchQueue.main.async {
|
||||
modalActivityIndicator.dismiss {
|
||||
var isMaxFileSizeExceeded = false
|
||||
if let error = error as? FileServerAPIV2.Error {
|
||||
isMaxFileSizeExceeded = (error == .maxFileSizeExceeded)
|
||||
if profilePictureToBeUploaded != nil {
|
||||
userDefaults[.lastProfilePictureUpdate] = Date()
|
||||
}
|
||||
GRDBStorage.shared.write { db in
|
||||
MessageSender.syncConfiguration(db, forceSyncNow: true).retainUntilComplete()
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
modalActivityIndicator.dismiss {
|
||||
guard let self = self else { return }
|
||||
self.profilePictureView.update()
|
||||
self.displayNameLabel.text = name
|
||||
self.profilePictureToBeUploaded = nil
|
||||
self.displayNameToBeUploaded = nil
|
||||
}
|
||||
}
|
||||
},
|
||||
failure: { error in
|
||||
DispatchQueue.main.async {
|
||||
modalActivityIndicator.dismiss {
|
||||
let isMaxFileSizeExceeded = (error == .avatarUploadMaxFileSizeExceeded)
|
||||
let title = isMaxFileSizeExceeded ? "Maximum File Size Exceeded" : "Couldn't Update Profile"
|
||||
let message = isMaxFileSizeExceeded ? "Please select a smaller photo and try again" : "Please check your internet connection and try again"
|
||||
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil))
|
||||
self?.present(alert, animated: true, completion: nil)
|
||||
}
|
||||
let title = isMaxFileSizeExceeded ? "Maximum File Size Exceeded" : "Couldn't Update Profile"
|
||||
let message = isMaxFileSizeExceeded ? "Please select a smaller photo and try again" : "Please check your internet connection and try again"
|
||||
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil))
|
||||
self?.present(alert, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
}, requiresSync: true)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -461,7 +467,7 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
|
|||
guard !displayName.isEmpty else {
|
||||
return showError(title: NSLocalizedString("vc_settings_display_name_missing_error", comment: ""))
|
||||
}
|
||||
guard !OWSProfileManager.shared().isProfileNameTooLong(displayName) else {
|
||||
guard !ProfileManager.isToLong(profileName: displayName) else {
|
||||
return showError(title: NSLocalizedString("vc_settings_display_name_too_long_error", comment: ""))
|
||||
}
|
||||
isEditingDisplayName = false
|
||||
|
|
|
@ -239,10 +239,9 @@ final class ConversationCell : UITableViewCell {
|
|||
// Contact
|
||||
if threadViewModel.isGroupThread, let thread = threadViewModel.threadRecord as? TSGroupThread {
|
||||
displayNameLabel.attributedText = getHighlightedSnippet(snippet: getDisplayName(), searchText: normalizedSearchText, fontSize: Values.mediumFontSize)
|
||||
let context: Contact.Context = thread.isOpenGroup ? .openGroup : .regular
|
||||
var rawSnippet: String = ""
|
||||
thread.groupModel.groupMemberIds.forEach{ id in
|
||||
if let displayName = Storage.shared.getContact(with: id)?.displayName(for: context) {
|
||||
thread.groupModel.groupMemberIds.forEach { id in
|
||||
if let displayName = Profile.displayNameNoFallback(for: id, thread: thread) {
|
||||
if !rawSnippet.isEmpty {
|
||||
rawSnippet += ", \(displayName)"
|
||||
}
|
||||
|
@ -348,7 +347,7 @@ final class ConversationCell : UITableViewCell {
|
|||
private func getMessageAuthorName(message: TSMessage) -> String? {
|
||||
guard threadViewModel.isGroupThread else { return nil }
|
||||
if let incomingMessage = message as? TSIncomingMessage {
|
||||
return Storage.shared.getContact(with: incomingMessage.authorId)?.displayName(for: .regular) ?? "Anonymous"
|
||||
return Profile.displayName(for: incomingMessage.authorId, customFallback: "Anonymous")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -356,14 +355,14 @@ final class ConversationCell : UITableViewCell {
|
|||
private func getDisplayNameForSearch(_ sessionID: String) -> String {
|
||||
if threadViewModel.threadRecord.isNoteToSelf() {
|
||||
return NSLocalizedString("NOTE_TO_SELF", comment: "")
|
||||
} else {
|
||||
var result = sessionID
|
||||
if let contact = Storage.shared.getContact(with: sessionID), let name = contact.name {
|
||||
result = name
|
||||
if let nickname = contact.nickname { result += "(\(nickname))"}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
return [
|
||||
Profile.displayName(for: sessionID),
|
||||
Profile.fetchOrCreate(id: sessionID).nickname.map { "(\($0)" }
|
||||
]
|
||||
.compactMap { $0 }
|
||||
.joined(separator: " ")
|
||||
}
|
||||
|
||||
private func getDisplayName() -> String {
|
||||
|
@ -381,9 +380,9 @@ final class ConversationCell : UITableViewCell {
|
|||
}
|
||||
else {
|
||||
let hexEncodedPublicKey: String = threadViewModel.contactSessionID!
|
||||
let displayName: String = (Storage.shared.getContact(with: hexEncodedPublicKey)?.displayName(for: .regular) ?? hexEncodedPublicKey)
|
||||
let middleTruncatedHexKey: String = "\(hexEncodedPublicKey.prefix(4))...\(hexEncodedPublicKey.suffix(4))"
|
||||
return (displayName == hexEncodedPublicKey ? middleTruncatedHexKey : displayName)
|
||||
|
||||
return Profile.displayName(for: hexEncodedPublicKey, customFallback: middleTruncatedHexKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -82,7 +82,7 @@ final class UserCell : UITableViewCell {
|
|||
func update() {
|
||||
profilePictureView.publicKey = publicKey
|
||||
profilePictureView.update()
|
||||
displayNameLabel.text = Storage.shared.getContact(with: publicKey)?.displayName(for: .regular) ?? publicKey
|
||||
displayNameLabel.text = Profile.displayName(for: publicKey)
|
||||
|
||||
switch accessory {
|
||||
case .none: accessoryImageView.isHidden = true
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import SessionMessagingKit
|
||||
|
||||
@objc(SNUserSelectionVC)
|
||||
final class UserSelectionVC : BaseVC, UITableViewDataSource, UITableViewDelegate {
|
||||
|
@ -7,7 +11,7 @@ final class UserSelectionVC : BaseVC, UITableViewDataSource, UITableViewDelegate
|
|||
private var selectedUsers: Set<String> = []
|
||||
|
||||
private lazy var users: [String] = {
|
||||
var result = ContactUtilities.getAllContacts()
|
||||
var result = Contact.fetchAllIds()
|
||||
result.removeAll { usersToExclude.contains($0) }
|
||||
return result
|
||||
}()
|
||||
|
|
|
@ -15,10 +15,6 @@ public class AccountManager: NSObject {
|
|||
|
||||
// MARK: - Dependencies
|
||||
|
||||
var profileManager: OWSProfileManager {
|
||||
return OWSProfileManager.shared()
|
||||
}
|
||||
|
||||
private var preferences: OWSPreferences {
|
||||
return Environment.shared.preferences
|
||||
}
|
||||
|
|
|
@ -17,9 +17,7 @@ public final class MentionUtilities : NSObject {
|
|||
while let match = outerMatch {
|
||||
let publicKey = String((string as NSString).substring(with: match.range).dropFirst()) // Drop the @
|
||||
let matchEnd: Int
|
||||
let context: Contact.Context = (openGroupV2 != nil) ? .openGroup : .regular
|
||||
let displayName = Storage.shared.getContact(with: publicKey)?.displayName(for: context)
|
||||
if let displayName = displayName {
|
||||
if let displayName = Profile.displayNameNoFallback(for: publicKey, context: (openGroupV2 != nil ? .openGroup : .regular)) {
|
||||
string = (string as NSString).replacingCharacters(in: match.range, with: "@\(displayName)")
|
||||
mentions.append((range: NSRange(location: match.range.location, length: displayName.utf16.count + 1), publicKey: publicKey)) // + 1 to include the @
|
||||
matchEnd = match.range.location + displayName.utf16.count
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import Foundation
|
||||
import Sodium
|
||||
import Curve25519Kit
|
||||
import SessionMessagingKit
|
||||
|
||||
enum MockDataGenerator {
|
||||
|
@ -146,7 +147,7 @@ enum MockDataGenerator {
|
|||
!isMessageRequest &&
|
||||
(((0..<10).randomElement(using: &dmThreadRandomGenerator) ?? 0) < 8) // 80% approved the current user
|
||||
)
|
||||
Storage.shared.setContact(contact, using: transaction)
|
||||
transaction.setObject(contact, forKey: contact.sessionID, inCollection: Legacy.contactCollection)
|
||||
|
||||
// Generate the message history (Note: Unapproved message requests will only include incoming messages)
|
||||
logProgress("DM Thread \(threadIndex)", "Generate \(numMessages) Messages")
|
||||
|
@ -209,7 +210,7 @@ enum MockDataGenerator {
|
|||
contact.name = (0..<contactNameLength)
|
||||
.compactMap { _ in stringContent.randomElement(using: &cgThreadRandomGenerator) }
|
||||
.joined()
|
||||
Storage.shared.setContact(contact, using: transaction)
|
||||
transaction.setObject(contact, forKey: contact.sessionID, inCollection: Legacy.contactCollection)
|
||||
|
||||
members.append(randomSessionId)
|
||||
}
|
||||
|
@ -229,8 +230,12 @@ enum MockDataGenerator {
|
|||
|
||||
// Add the group to the user's set of public keys to poll for and store the key pair
|
||||
let encryptionKeyPair = Curve25519.generateKeyPair()
|
||||
let keyPair: Box.KeyPair = Box.KeyPair(
|
||||
publicKey: encryptionKeyPair.publicKey.bytes,
|
||||
secretKey: encryptionKeyPair.privateKey.bytes
|
||||
)
|
||||
Storage.shared.addClosedGroupPublicKey(randomGroupPublicKey, using: transaction)
|
||||
Storage.shared.addClosedGroupEncryptionKeyPair(encryptionKeyPair, for: randomGroupPublicKey, using: transaction)
|
||||
Storage.shared.addClosedGroupEncryptionKeyPair(keyPair, for: randomGroupPublicKey, using: transaction)
|
||||
|
||||
// Generate the message history (Note: Unapproved message requests will only include incoming messages)
|
||||
logProgress("Closed Group Thread \(threadIndex)", "Generate \(numMessages) Messages")
|
||||
|
@ -291,7 +296,7 @@ enum MockDataGenerator {
|
|||
contact.name = (0..<contactNameLength)
|
||||
.compactMap { _ in stringContent.randomElement(using: &ogThreadRandomGenerator) }
|
||||
.joined()
|
||||
Storage.shared.setContact(contact, using: transaction)
|
||||
transaction.setObject(contact, forKey: contact.sessionID, inCollection: Legacy.contactCollection)
|
||||
|
||||
members.append(randomSessionId)
|
||||
}
|
||||
|
|
|
@ -1,18 +1,30 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import Mantle
|
||||
import YapDatabase
|
||||
import SignalCoreKit
|
||||
|
||||
public enum Legacy {
|
||||
// MARK: - Collections and Keys
|
||||
|
||||
internal static let contactThreadPrefix = "c"
|
||||
internal static let groupThreadPrefix = "g"
|
||||
internal static let closedGroupIdPrefix = "__textsecure_group__!"
|
||||
internal static let closedGroupKeyPairPrefix = "SNClosedGroupEncryptionKeyPairCollection-"
|
||||
|
||||
public static let contactCollection = "LokiContactCollection"
|
||||
internal static let threadCollection = "TSThread"
|
||||
internal static let contactCollection = "LokiContactCollection"
|
||||
internal static let disappearingMessagesCollection = "OWSDisappearingMessagesConfiguration"
|
||||
internal static let closedGroupPublicKeyCollection = "SNClosedGroupPublicKeyCollection"
|
||||
internal static let closedGroupFormationTimestampCollection = "SNClosedGroupFormationTimestampCollection"
|
||||
internal static let closedGroupZombieMembersCollection = "SNClosedGroupZombieMembersCollection"
|
||||
|
||||
// MARK: - Types
|
||||
|
||||
public typealias Contact = _LegacyContact
|
||||
|
||||
public typealias DisappearingMessagesConfiguration = _LegacyDisappearingMessagesConfiguration
|
||||
|
||||
@objc(SNProfile)
|
||||
public class Profile: NSObject, NSCoding {
|
||||
public var displayName: String?
|
||||
|
|
|
@ -39,5 +39,56 @@ enum _001_InitialSetupMigration: Migration {
|
|||
t.column(.profilePictureFileName, .text)
|
||||
t.column(.profileEncryptionKey, .blob)
|
||||
}
|
||||
|
||||
try db.create(table: SessionThread.self) { t in
|
||||
t.column(.id, .text)
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
t.column(.variant, .integer).notNull()
|
||||
t.column(.creationDateTimestamp, .double).notNull()
|
||||
t.column(.shouldBeVisible, .boolean).notNull()
|
||||
t.column(.isPinned, .boolean).notNull()
|
||||
t.column(.messageDraft, .text)
|
||||
t.column(.notificationMode, .integer).notNull()
|
||||
t.column(.mutedUntilTimestamp, .double)
|
||||
}
|
||||
|
||||
try db.create(table: DisappearingMessagesConfiguration.self) { t in
|
||||
t.column(.id, .text)
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.references(SessionThread.self)
|
||||
t.column(.isEnabled, .boolean)
|
||||
.defaults(to: false)
|
||||
.notNull()
|
||||
t.column(.durationSeconds, .double)
|
||||
.defaults(to: 0)
|
||||
.notNull()
|
||||
}
|
||||
|
||||
try db.create(table: ClosedGroup.self) { t in
|
||||
t.column(.publicKey, .text)
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
t.column(.name, .text).notNull()
|
||||
t.column(.formationTimestamp, .double).notNull()
|
||||
}
|
||||
|
||||
try db.create(table: ClosedGroupKeyPair.self) { t in
|
||||
t.column(.publicKey, .text)
|
||||
.notNull()
|
||||
.indexed()
|
||||
.references(ClosedGroup.self)
|
||||
t.column(.secretKey, .blob).notNull()
|
||||
t.column(.receivedTimestamp, .double).notNull()
|
||||
}
|
||||
|
||||
try db.create(table: GroupMember.self) { t in
|
||||
t.column(.groupId, .text)
|
||||
.notNull()
|
||||
.indexed()
|
||||
t.column(.profileId, .text).notNull()
|
||||
t.column(.role, .integer).notNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import Foundation
|
||||
import GRDB
|
||||
import Curve25519Kit
|
||||
import SessionUtilitiesKit
|
||||
|
||||
enum _002_YDBToGRDBMigration: Migration {
|
||||
|
@ -9,10 +10,19 @@ enum _002_YDBToGRDBMigration: Migration {
|
|||
|
||||
// TODO: Autorelease pool???.
|
||||
static func migrate(_ db: Database) throws {
|
||||
// MARK: - Contacts
|
||||
// MARK: - Contacts & Threads
|
||||
|
||||
var shouldFailMigration: Bool = false
|
||||
var contacts: Set<Legacy.Contact> = []
|
||||
var contactThreadIds: Set<String> = []
|
||||
var threads: Set<TSThread> = []
|
||||
var disappearingMessagesConfiguration: [String: Legacy.DisappearingMessagesConfiguration] = [:]
|
||||
var closedGroupKeys: [String: (timestamp: TimeInterval, keys: SessionUtilitiesKit.Legacy.KeyPair)] = [:]
|
||||
var closedGroupName: [String: String] = [:]
|
||||
var closedGroupFormation: [String: UInt64] = [:]
|
||||
var closedGroupModel: [String: TSGroupModel] = [:]
|
||||
var closedGroupZombieMemberIds: [String: Set<String>] = [:]
|
||||
var openGroupInfo: [String: OpenGroupV2] = [:]
|
||||
|
||||
Storage.read { transaction in
|
||||
// Process the Contacts
|
||||
|
@ -21,24 +31,105 @@ enum _002_YDBToGRDBMigration: Migration {
|
|||
contacts.insert(contact)
|
||||
}
|
||||
|
||||
// Process the contact threads (only want to create "real" contacts in the new structure)
|
||||
transaction.enumerateKeys(inCollection: Legacy.threadCollection) { key, _ in
|
||||
guard key.starts(with: Legacy.contactThreadPrefix) else { return }
|
||||
contactThreadIds.insert(key)
|
||||
let userClosedGroupPublicKeys: [String] = transaction.allKeys(inCollection: Legacy.closedGroupPublicKeyCollection)
|
||||
|
||||
// Process the threads
|
||||
transaction.enumerateKeysAndObjects(inCollection: Legacy.threadCollection) { key, object, _ in
|
||||
guard let thread: TSThread = object as? TSThread else { return }
|
||||
guard let threadId: String = thread.uniqueId else { return }
|
||||
|
||||
threads.insert(thread)
|
||||
|
||||
// Want to exclude threads which aren't visible (ie. threads which we started
|
||||
// but the user never ended up sending a message)
|
||||
if key.starts(with: Legacy.contactThreadPrefix) && thread.shouldBeVisible {
|
||||
contactThreadIds.insert(key)
|
||||
}
|
||||
|
||||
// Get the disappearing messages config
|
||||
disappearingMessagesConfiguration[threadId] = transaction
|
||||
.object(forKey: threadId, inCollection: Legacy.disappearingMessagesCollection)
|
||||
.asType(Legacy.DisappearingMessagesConfiguration.self)
|
||||
.defaulting(to: Legacy.DisappearingMessagesConfiguration.defaultWith(threadId))
|
||||
|
||||
// Process group-specific info
|
||||
guard let groupThread: TSGroupThread = thread as? TSGroupThread else { return }
|
||||
|
||||
if groupThread.isClosedGroup {
|
||||
// The old threadId for closed groups was in the below format, we don't
|
||||
// really need the unnecessary complexity so process the key and extract
|
||||
// the publicKey from it
|
||||
// `g{base64String(Data(__textsecure_group__!{publicKey}))}
|
||||
let base64GroupId: String = String(threadId.suffix(from: threadId.index(after: threadId.startIndex)))
|
||||
guard
|
||||
let groupIdData: Data = Data(base64Encoded: base64GroupId),
|
||||
let groupId: String = String(data: groupIdData, encoding: .utf8),
|
||||
let publicKey: String = groupId.split(separator: "!").last.map({ String($0) }),
|
||||
let formationTimestamp: UInt64 = transaction.object(forKey: publicKey, inCollection: Legacy.closedGroupFormationTimestampCollection) as? UInt64
|
||||
else {
|
||||
SNLog("Unable to decode Closed Group during migration")
|
||||
shouldFailMigration = true
|
||||
return
|
||||
}
|
||||
guard userClosedGroupPublicKeys.contains(publicKey) else {
|
||||
SNLog("Found unexpected invalid closed group public key during migration")
|
||||
shouldFailMigration = true
|
||||
return
|
||||
}
|
||||
|
||||
let keyCollection: String = "\(Legacy.closedGroupKeyPairPrefix)\(publicKey)"
|
||||
|
||||
closedGroupName[threadId] = groupThread.name(with: transaction)
|
||||
closedGroupModel[threadId] = groupThread.groupModel
|
||||
closedGroupFormation[threadId] = formationTimestamp
|
||||
closedGroupZombieMemberIds[threadId] = transaction.object(
|
||||
forKey: publicKey,
|
||||
inCollection: Legacy.closedGroupZombieMembersCollection
|
||||
) as? Set<String>
|
||||
|
||||
transaction.enumerateKeysAndObjects(inCollection: keyCollection) { key, object, _ in
|
||||
guard let timestamp: TimeInterval = TimeInterval(key), let keyPair: SessionUtilitiesKit.Legacy.KeyPair = object as? SessionUtilitiesKit.Legacy.KeyPair else {
|
||||
return
|
||||
}
|
||||
|
||||
closedGroupKeys[threadId] = (timestamp, keyPair)
|
||||
}
|
||||
}
|
||||
else if groupThread.isOpenGroup {
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// We can't properly throw within the 'enumerateKeysAndObjects' block so have to throw here
|
||||
guard !shouldFailMigration else { throw GRDBStorageError.migrationFailed }
|
||||
|
||||
// Insert the data into GRDB
|
||||
|
||||
// MARK: - Insert Contacts
|
||||
|
||||
let currentUserPublicKey: String = getUserHexEncodedPublicKey(db)
|
||||
|
||||
try contacts.forEach { contact in
|
||||
let isCurrentUser: Bool = (contact.sessionID == currentUserPublicKey)
|
||||
let contactThreadId: String = TSContactThread.threadID(fromContactSessionID: contact.sessionID)
|
||||
|
||||
// Determine if this contact is a "real" contact
|
||||
// Create the "Profile" for the legacy contact
|
||||
try Profile(
|
||||
id: contact.sessionID,
|
||||
name: (contact.name ?? contact.sessionID),
|
||||
nickname: contact.nickname,
|
||||
profilePictureUrl: contact.profilePictureURL,
|
||||
profilePictureFileName: contact.profilePictureFileName,
|
||||
profileEncryptionKey: contact.profileEncryptionKey
|
||||
).insert(db)
|
||||
|
||||
// Determine if this contact is a "real" contact (don't want to create contacts for
|
||||
// every user in the new structure but still want profiles for every user)
|
||||
if
|
||||
// TODO: Thread.shouldBeVisible???
|
||||
isCurrentUser ||
|
||||
contactThreadIds.contains(contactThreadId) ||
|
||||
contact.isApproved ||
|
||||
|
@ -56,16 +147,110 @@ enum _002_YDBToGRDBMigration: Migration {
|
|||
hasBeenBlocked: (!isCurrentUser && (contact.hasBeenBlocked || contact.isBlocked))
|
||||
).insert(db)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Insert Threads
|
||||
|
||||
try threads.forEach { thread in
|
||||
guard let legacyThreadId: String = thread.uniqueId else { return }
|
||||
|
||||
// Create the "Profile" for the legacy contact
|
||||
try Profile(
|
||||
id: contact.sessionID,
|
||||
name: (contact.name ?? contact.sessionID),
|
||||
nickname: contact.nickname,
|
||||
profilePictureUrl: contact.profilePictureURL,
|
||||
profilePictureFileName: contact.profilePictureFileName,
|
||||
profileEncryptionKey: contact.profileEncryptionKey
|
||||
let id: String
|
||||
let variant: SessionThread.Variant
|
||||
let notificationMode: SessionThread.NotificationMode
|
||||
|
||||
switch thread {
|
||||
case let groupThread as TSGroupThread:
|
||||
if groupThread.isOpenGroup {
|
||||
id = legacyThreadId//openGroup.id
|
||||
variant = .openGroup
|
||||
}
|
||||
else {
|
||||
guard let publicKey: Data = closedGroupKeys[legacyThreadId]?.keys.publicKey else {
|
||||
throw GRDBStorageError.migrationFailed
|
||||
}
|
||||
|
||||
id = publicKey.toHexString()
|
||||
variant = .closedGroup
|
||||
}
|
||||
|
||||
notificationMode = (thread.isMuted ? .none :
|
||||
(groupThread.isOnlyNotifyingForMentions ?
|
||||
.mentionsOnly :
|
||||
.all
|
||||
)
|
||||
)
|
||||
|
||||
default:
|
||||
id = legacyThreadId.substring(from: Legacy.contactThreadPrefix.count)
|
||||
variant = .contact
|
||||
notificationMode = (thread.isMuted ? .none : .all)
|
||||
}
|
||||
|
||||
try SessionThread(
|
||||
id: id,
|
||||
variant: variant,
|
||||
creationDateTimestamp: thread.creationDate.timeIntervalSince1970,
|
||||
shouldBeVisible: thread.shouldBeVisible,
|
||||
isPinned: thread.isPinned,
|
||||
messageDraft: thread.messageDraft,
|
||||
notificationMode: notificationMode,
|
||||
mutedUntilTimestamp: thread.mutedUntilDate?.timeIntervalSince1970
|
||||
).insert(db)
|
||||
|
||||
// Disappearing Messages Configuration
|
||||
if let config: Legacy.DisappearingMessagesConfiguration = disappearingMessagesConfiguration[id] {
|
||||
try DisappearingMessagesConfiguration(
|
||||
id: id,
|
||||
isEnabled: config.isEnabled,
|
||||
durationSeconds: TimeInterval(config.durationSeconds)
|
||||
).insert(db)
|
||||
}
|
||||
|
||||
// Closed Groups
|
||||
if (thread as? TSGroupThread)?.isClosedGroup == true {
|
||||
guard
|
||||
let keyInfo = closedGroupKeys[legacyThreadId],
|
||||
let name: String = closedGroupName[legacyThreadId],
|
||||
let groupModel: TSGroupModel = closedGroupModel[legacyThreadId],
|
||||
let formationTimestamp: UInt64 = closedGroupFormation[legacyThreadId]
|
||||
else { throw GRDBStorageError.migrationFailed }
|
||||
|
||||
try ClosedGroup(
|
||||
publicKey: keyInfo.keys.publicKey.toHexString(),
|
||||
name: name,
|
||||
formationTimestamp: TimeInterval(formationTimestamp)
|
||||
).insert(db)
|
||||
|
||||
try ClosedGroupKeyPair(
|
||||
publicKey: keyInfo.keys.publicKey.toHexString(),
|
||||
secretKey: keyInfo.keys.privateKey,
|
||||
receivedTimestamp: keyInfo.timestamp
|
||||
).insert(db)
|
||||
|
||||
try groupModel.groupMemberIds.forEach { memberId in
|
||||
try GroupMember(
|
||||
groupId: id,
|
||||
profileId: memberId,
|
||||
role: .standard
|
||||
).insert(db)
|
||||
}
|
||||
|
||||
try groupModel.groupAdminIds.forEach { adminId in
|
||||
try GroupMember(
|
||||
groupId: id,
|
||||
profileId: adminId,
|
||||
role: .admin
|
||||
).insert(db)
|
||||
}
|
||||
|
||||
try (closedGroupZombieMemberIds[legacyThreadId] ?? []).forEach { zombieId in
|
||||
try GroupMember(
|
||||
groupId: id,
|
||||
profileId: zombieId,
|
||||
role: .zombie
|
||||
).insert(db)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
22
SessionMessagingKit/Database/Models/Capability.swift
Normal file
22
SessionMessagingKit/Database/Models/Capability.swift
Normal file
|
@ -0,0 +1,22 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import GRDB
|
||||
import SessionUtilitiesKit
|
||||
|
||||
public struct Capability: Codable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
|
||||
public static var databaseTableName: String { "capability" }
|
||||
|
||||
public typealias Columns = CodingKeys
|
||||
public enum CodingKeys: String, CodingKey, ColumnExpression {
|
||||
case server
|
||||
case room
|
||||
case capability
|
||||
case isMissing
|
||||
}
|
||||
|
||||
public let server: String
|
||||
public let room: String
|
||||
public let capability: String
|
||||
public let isMissing: Bool
|
||||
}
|
48
SessionMessagingKit/Database/Models/ClosedGroup.swift
Normal file
48
SessionMessagingKit/Database/Models/ClosedGroup.swift
Normal file
|
@ -0,0 +1,48 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import GRDB
|
||||
import SessionUtilitiesKit
|
||||
|
||||
public struct ClosedGroup: Codable, Identifiable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
|
||||
public static var databaseTableName: String { "closedGroup" }
|
||||
static let keyPairs = hasMany(ClosedGroupKeyPair.self)
|
||||
static let members = hasMany(GroupMember.self)
|
||||
|
||||
public typealias Columns = CodingKeys
|
||||
public enum CodingKeys: String, CodingKey, ColumnExpression {
|
||||
case publicKey
|
||||
case name
|
||||
case formationTimestamp
|
||||
}
|
||||
|
||||
public var id: String { publicKey }
|
||||
|
||||
public let publicKey: String
|
||||
public let name: String
|
||||
public let formationTimestamp: TimeInterval
|
||||
|
||||
public var keyPairs: QueryInterfaceRequest<ClosedGroupKeyPair> {
|
||||
request(for: ClosedGroup.keyPairs)
|
||||
}
|
||||
|
||||
public var memberIds: QueryInterfaceRequest<GroupMember> {
|
||||
request(for: ClosedGroup.members)
|
||||
.filter(GroupMember.Columns.role == GroupMember.Role.standard)
|
||||
}
|
||||
|
||||
public var zombieIds: QueryInterfaceRequest<GroupMember> {
|
||||
request(for: ClosedGroup.members)
|
||||
.filter(GroupMember.Columns.role == GroupMember.Role.zombie)
|
||||
}
|
||||
|
||||
public var moderatorIds: QueryInterfaceRequest<GroupMember> {
|
||||
request(for: ClosedGroup.members)
|
||||
.filter(GroupMember.Columns.role == GroupMember.Role.moderator)
|
||||
}
|
||||
|
||||
public var adminIds: QueryInterfaceRequest<GroupMember> {
|
||||
request(for: ClosedGroup.members)
|
||||
.filter(GroupMember.Columns.role == GroupMember.Role.admin)
|
||||
}
|
||||
}
|
22
SessionMessagingKit/Database/Models/ClosedGroupKeyPair.swift
Normal file
22
SessionMessagingKit/Database/Models/ClosedGroupKeyPair.swift
Normal file
|
@ -0,0 +1,22 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import GRDB
|
||||
import SessionUtilitiesKit
|
||||
|
||||
public struct ClosedGroupKeyPair: Codable, Identifiable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
|
||||
public static var databaseTableName: String { "closedGroupKeyPair" }
|
||||
|
||||
public typealias Columns = CodingKeys
|
||||
public enum CodingKeys: String, CodingKey, ColumnExpression {
|
||||
case publicKey
|
||||
case secretKey
|
||||
case receivedTimestamp
|
||||
}
|
||||
|
||||
public var id: String { publicKey }
|
||||
|
||||
public let publicKey: String
|
||||
public let secretKey: Data
|
||||
public let receivedTimestamp: TimeInterval
|
||||
}
|
|
@ -4,7 +4,7 @@ import Foundation
|
|||
import GRDB
|
||||
import SessionUtilitiesKit
|
||||
|
||||
public struct Contact: Codable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
|
||||
public struct Contact: Codable, Identifiable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
|
||||
public static var databaseTableName: String { "contact" }
|
||||
|
||||
public typealias Columns = CodingKeys
|
||||
|
@ -22,23 +22,129 @@ public struct Contact: Codable, FetchableRecord, PersistableRecord, TableRecord,
|
|||
public let id: String
|
||||
|
||||
/// This flag is used to determine whether we should auto-download files sent by this contact.
|
||||
public var isTrusted = false
|
||||
public let isTrusted: Bool
|
||||
|
||||
/// This flag is used to determine whether message requests from this contact are approved
|
||||
public var isApproved = false
|
||||
public let isApproved: Bool
|
||||
|
||||
/// This flag is used to determine whether message requests from this contact are blocked
|
||||
public var isBlocked = false {
|
||||
didSet {
|
||||
if isBlocked {
|
||||
hasBeenBlocked = true
|
||||
public let isBlocked: Bool
|
||||
|
||||
/// This flag is used to determine whether this contact has approved the current users message request
|
||||
public let didApproveMe: Bool
|
||||
|
||||
/// This flag is used to determine whether this contact has ever been blocked (will be included in the config message if so)
|
||||
public let hasBeenBlocked: Bool
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
public init(
|
||||
id: String,
|
||||
isTrusted: Bool = false,
|
||||
isApproved: Bool = false,
|
||||
isBlocked: Bool = false,
|
||||
didApproveMe: Bool = false,
|
||||
hasBeenBlocked: Bool = false
|
||||
) {
|
||||
self.id = id
|
||||
self.isTrusted = (
|
||||
isTrusted ||
|
||||
id == getUserHexEncodedPublicKey() // Always trust ourselves
|
||||
)
|
||||
self.isApproved = isApproved
|
||||
self.isBlocked = isBlocked
|
||||
self.didApproveMe = didApproveMe
|
||||
self.hasBeenBlocked = (isBlocked || hasBeenBlocked)
|
||||
}
|
||||
|
||||
// MARK: - PersistableRecord
|
||||
|
||||
public func save(_ db: Database) throws {
|
||||
let oldContact: Contact? = try? Contact.fetchOne(db, id: id)
|
||||
|
||||
try performSave(db)
|
||||
|
||||
db.afterNextTransactionCommit { db in
|
||||
if isBlocked != oldContact?.isBlocked {
|
||||
NotificationCenter.default.post(name: .contactBlockedStateChanged, object: id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This flag is used to determine whether this contact has approved the current users message request
|
||||
public var didApproveMe = false
|
||||
|
||||
/// This flag is used to determine whether this contact has ever been blocked (will be included in the config message if so)
|
||||
public var hasBeenBlocked = false
|
||||
}
|
||||
|
||||
// MARK: - Convenience
|
||||
|
||||
public extension Contact {
|
||||
func with(
|
||||
isTrusted: Updatable<Bool> = .existing,
|
||||
isApproved: Updatable<Bool> = .existing,
|
||||
isBlocked: Updatable<Bool> = .existing,
|
||||
didApproveMe: Updatable<Bool> = .existing
|
||||
) -> Contact {
|
||||
return Contact(
|
||||
id: id,
|
||||
isTrusted: (
|
||||
(isTrusted ?? self.isTrusted) ||
|
||||
self.id == getUserHexEncodedPublicKey() // Always trust ourselves
|
||||
),
|
||||
isApproved: (isApproved ?? self.isApproved),
|
||||
isBlocked: (isBlocked ?? self.isBlocked),
|
||||
didApproveMe: (didApproveMe ?? self.didApproveMe),
|
||||
hasBeenBlocked: ((isBlocked ?? self.isBlocked) || self.hasBeenBlocked)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - GRDB Interactions
|
||||
|
||||
public extension Contact {
|
||||
static func fetchOrCreate(_ db: Database, id: ID) -> Contact {
|
||||
return ((try? fetchOne(db, id: id)) ?? Contact(id: id))
|
||||
}
|
||||
|
||||
static func fetchAllIds() -> [String] {
|
||||
return GRDBStorage.shared
|
||||
.read { db in
|
||||
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
||||
let contacts: [Contact] = try Contact
|
||||
.filter(Contact.Columns.id != userPublicKey)
|
||||
.filter(Contact.Columns.didApproveMe == true)
|
||||
.fetchAll(db)
|
||||
let profiles: [Profile] = try Profile
|
||||
.fetchAll(db, ids: contacts.map { $0.id })
|
||||
|
||||
// Sort the contacts by their displayName value
|
||||
return profiles
|
||||
.sorted(by: { lhs, rhs -> Bool in lhs.displayName() < rhs.displayName() })
|
||||
.map { $0.id }
|
||||
}
|
||||
.defaulting(to: [])
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Objective-C Support
|
||||
@objc(SMKContact)
|
||||
public class SMKContact: NSObject {
|
||||
@objc let isApproved: Bool
|
||||
@objc let isBlocked: Bool
|
||||
@objc let didApproveMe: Bool
|
||||
|
||||
init(isApproved: Bool, isBlocked: Bool, didApproveMe: Bool) {
|
||||
self.isApproved = isApproved
|
||||
self.isBlocked = isBlocked
|
||||
self.didApproveMe = didApproveMe
|
||||
}
|
||||
|
||||
@objc public static func fetchOrCreate(id: String) -> SMKContact {
|
||||
let existingContact: Contact? = GRDBStorage.shared.read { db in
|
||||
try Contact.fetchOne(db, id: id)
|
||||
}
|
||||
|
||||
return SMKContact(
|
||||
isApproved: existingContact?.isApproved ?? false,
|
||||
isBlocked: existingContact?.isBlocked ?? false,
|
||||
didApproveMe: existingContact?.didApproveMe ?? false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import GRDB
|
||||
import SessionUtilitiesKit
|
||||
|
||||
public struct DisappearingMessagesConfiguration: Codable, Identifiable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
|
||||
public static var databaseTableName: String { "disappearingMessagesConfiguration" }
|
||||
|
||||
public typealias Columns = CodingKeys
|
||||
public enum CodingKeys: String, CodingKey, ColumnExpression {
|
||||
case id
|
||||
case isEnabled
|
||||
case durationSeconds
|
||||
}
|
||||
|
||||
public let id: String
|
||||
public let isEnabled: Bool
|
||||
public let durationSeconds: TimeInterval
|
||||
}
|
||||
|
||||
// MARK: - Convenience
|
||||
|
||||
extension DisappearingMessagesConfiguration {
|
||||
public var durationIndex: Int {
|
||||
return DisappearingMessagesConfiguration.validDurationsSeconds
|
||||
.firstIndex(of: durationSeconds)
|
||||
.defaulting(to: 0)
|
||||
}
|
||||
|
||||
public var durationString: String {
|
||||
NSString.formatDurationSeconds(UInt32(durationSeconds), useShortFormat: false)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UI Constraints
|
||||
|
||||
extension DisappearingMessagesConfiguration {
|
||||
public static var validDurationsSeconds: [TimeInterval] {
|
||||
return [
|
||||
5,
|
||||
10,
|
||||
30,
|
||||
(1 * 60),
|
||||
(5 * 60),
|
||||
(30 * 60),
|
||||
(1 * 60 * 60),
|
||||
(6 * 60 * 60),
|
||||
(12 * 60 * 60),
|
||||
(24 * 60 * 60),
|
||||
(7 * 24 * 60 * 60)
|
||||
]
|
||||
}
|
||||
|
||||
public static var maxDurationSeconds: TimeInterval = {
|
||||
return (validDurationsSeconds.max() ?? 0)
|
||||
}()
|
||||
}
|
||||
|
||||
// MARK: - Objective-C Support
|
||||
@objc(SMKDisappearingMessagesConfiguration)
|
||||
public class SMKDisappearingMessagesConfiguration: NSObject {
|
||||
@objc public static var maxDurationSeconds: UInt = UInt(DisappearingMessagesConfiguration.maxDurationSeconds)
|
||||
}
|
27
SessionMessagingKit/Database/Models/GroupMember.swift
Normal file
27
SessionMessagingKit/Database/Models/GroupMember.swift
Normal file
|
@ -0,0 +1,27 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import GRDB
|
||||
import SessionUtilitiesKit
|
||||
|
||||
public struct GroupMember: Codable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
|
||||
public static var databaseTableName: String { "groupMember" }
|
||||
|
||||
public typealias Columns = CodingKeys
|
||||
public enum CodingKeys: String, CodingKey, ColumnExpression {
|
||||
case groupId
|
||||
case profileId
|
||||
case role
|
||||
}
|
||||
|
||||
public enum Role: Int, Codable, DatabaseValueConvertible {
|
||||
case standard
|
||||
case zombie
|
||||
case moderator
|
||||
case admin
|
||||
}
|
||||
|
||||
public let groupId: String
|
||||
public let profileId: String
|
||||
public let role: Role
|
||||
}
|
50
SessionMessagingKit/Database/Models/OpenGroup.swift
Normal file
50
SessionMessagingKit/Database/Models/OpenGroup.swift
Normal file
|
@ -0,0 +1,50 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import GRDB
|
||||
import SessionUtilitiesKit
|
||||
|
||||
public struct OpenGroup: Codable, Identifiable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
|
||||
public static var databaseTableName: String { "openGroup" }
|
||||
static let capabilities = hasMany(Capability.self)
|
||||
static let members = hasMany(GroupMember.self)
|
||||
|
||||
public typealias Columns = CodingKeys
|
||||
public enum CodingKeys: String, CodingKey, ColumnExpression {
|
||||
case server
|
||||
case room
|
||||
case publicKey
|
||||
case name
|
||||
case groupDescription = "description"
|
||||
case imageId
|
||||
case imageData
|
||||
case userCount
|
||||
case infoUpdates
|
||||
}
|
||||
|
||||
public var id: String { "\(server).\(room)" }
|
||||
|
||||
public let server: String
|
||||
public let room: String
|
||||
public let publicKey: String
|
||||
public let name: String
|
||||
public let groupDescription: String?
|
||||
public let imageId: Int?
|
||||
public let imageData: Data?
|
||||
public let userCount: Int
|
||||
public let infoUpdates: Int
|
||||
|
||||
public var capabilities: QueryInterfaceRequest<Capability> {
|
||||
request(for: OpenGroup.capabilities)
|
||||
}
|
||||
|
||||
public var moderatorIds: QueryInterfaceRequest<GroupMember> {
|
||||
request(for: OpenGroup.members)
|
||||
.filter(GroupMember.Columns.role == GroupMember.Role.moderator)
|
||||
}
|
||||
|
||||
public var adminIds: QueryInterfaceRequest<GroupMember> {
|
||||
request(for: OpenGroup.members)
|
||||
.filter(GroupMember.Columns.role == GroupMember.Role.admin)
|
||||
}
|
||||
}
|
|
@ -2,9 +2,10 @@
|
|||
|
||||
import Foundation
|
||||
import GRDB
|
||||
import SignalCoreKit
|
||||
import SessionUtilitiesKit
|
||||
|
||||
public struct Profile: Codable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible, CustomStringConvertible {
|
||||
public struct Profile: Codable, Identifiable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible, CustomStringConvertible {
|
||||
public static var databaseTableName: String { "profile" }
|
||||
|
||||
public typealias Columns = CodingKeys
|
||||
|
@ -23,19 +24,19 @@ public struct Profile: Codable, FetchableRecord, PersistableRecord, TableRecord,
|
|||
public let id: String
|
||||
|
||||
/// The name of the contact. Use this whenever you need the "real", underlying name of a user (e.g. when sending a message).
|
||||
public var name: String
|
||||
public let name: String
|
||||
|
||||
/// A custom name for the profile set by the current user
|
||||
public var nickname: String?
|
||||
public let nickname: String?
|
||||
|
||||
/// The URL from which to fetch the contact's profile picture.
|
||||
public var profilePictureUrl: String?
|
||||
public let profilePictureUrl: String?
|
||||
|
||||
/// The file name of the contact's profile picture on local storage.
|
||||
public var profilePictureFileName: String?
|
||||
public let profilePictureFileName: String?
|
||||
|
||||
/// The key with which the profile is encrypted.
|
||||
public var profileEncryptionKey: OWSAES256Key?
|
||||
public let profileEncryptionKey: OWSAES256Key?
|
||||
|
||||
// MARK: - Description
|
||||
|
||||
|
@ -48,6 +49,33 @@ public struct Profile: Codable, FetchableRecord, PersistableRecord, TableRecord,
|
|||
)
|
||||
"""
|
||||
}
|
||||
|
||||
// MARK: - PersistableRecord
|
||||
|
||||
public func save(_ db: Database) throws {
|
||||
let oldProfile: Profile? = try? Profile.fetchOne(db, id: id)
|
||||
|
||||
try performSave(db)
|
||||
|
||||
db.afterNextTransactionCommit { db in
|
||||
// Delete old profile picture if needed
|
||||
if let oldProfilePictureFileName: String = oldProfile?.profilePictureFileName, oldProfilePictureFileName != profilePictureFileName {
|
||||
let path: String = OWSUserProfile.profileAvatarFilepath(withFilename: oldProfilePictureFileName)
|
||||
DispatchQueue.global(qos: .default).async {
|
||||
OWSFileSystem.deleteFileIfExists(path)
|
||||
}
|
||||
}
|
||||
NotificationCenter.default.post(name: .profileUpdated, object: id)
|
||||
|
||||
if id == getUserHexEncodedPublicKey(db) {
|
||||
NotificationCenter.default.post(name: .localProfileDidChange, object: nil)
|
||||
}
|
||||
else {
|
||||
let userInfo = [ Notification.Key.profileRecipientId.rawValue: id ]
|
||||
NotificationCenter.default.post(name: .otherUsersProfileDidChange, object: nil, userInfo: userInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Codable
|
||||
|
@ -149,15 +177,32 @@ public extension Profile {
|
|||
// MARK: - Convenience
|
||||
|
||||
public extension Profile {
|
||||
func with(
|
||||
name: String? = nil,
|
||||
nickname: Updatable<String> = .existing,
|
||||
profilePictureUrl: Updatable<String> = .existing,
|
||||
profilePictureFileName: Updatable<String> = .existing,
|
||||
profileEncryptionKey: Updatable<OWSAES256Key> = .existing
|
||||
) -> Profile {
|
||||
return Profile(
|
||||
id: id,
|
||||
name: (name ?? self.name),
|
||||
nickname: (nickname ?? self.nickname),
|
||||
profilePictureUrl: (profilePictureUrl ?? self.profilePictureUrl),
|
||||
profilePictureFileName: (profilePictureFileName ?? self.profilePictureFileName),
|
||||
profileEncryptionKey: (profileEncryptionKey ?? self.profileEncryptionKey)
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Context
|
||||
|
||||
enum Context: Int {
|
||||
@objc enum Context: Int {
|
||||
case regular
|
||||
case openGroup
|
||||
}
|
||||
|
||||
/// The name to display in the UI. For local use only.
|
||||
func displayName(for context: Context) -> String? {
|
||||
func displayName(for context: Context = .regular) -> String {
|
||||
if let nickname: String = nickname { return nickname }
|
||||
|
||||
switch context {
|
||||
|
@ -172,3 +217,150 @@ public extension Profile {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - GRDB Interactions
|
||||
|
||||
public extension Profile {
|
||||
static func displayName(for id: ID, thread: TSThread, customFallback: String? = nil) -> String {
|
||||
return displayName(
|
||||
for: id,
|
||||
context: ((thread as? TSGroupThread)?.isOpenGroup == true ? .openGroup : .regular),
|
||||
customFallback: customFallback
|
||||
)
|
||||
}
|
||||
|
||||
static func displayName(for id: ID, context: Context = .regular, customFallback: String? = nil) -> String {
|
||||
let existingDisplayName: String? = GRDBStorage.shared
|
||||
.read { db in try Profile.fetchOne(db, id: id) }?
|
||||
.displayName(for: context)
|
||||
|
||||
return (existingDisplayName ?? (customFallback ?? id))
|
||||
}
|
||||
|
||||
static func displayNameNoFallback(for id: ID, thread: TSThread) -> String? {
|
||||
return displayName(
|
||||
for: id,
|
||||
context: ((thread as? TSGroupThread)?.isOpenGroup == true ? .openGroup : .regular)
|
||||
)
|
||||
}
|
||||
|
||||
static func displayNameNoFallback(for id: ID, context: Context = .regular) -> String? {
|
||||
return GRDBStorage.shared
|
||||
.read { db in try Profile.fetchOne(db, id: id) }?
|
||||
.displayName(for: context)
|
||||
}
|
||||
|
||||
// MARK: - Fetch or Create
|
||||
|
||||
private static func defaultFor(_ id: String) -> Profile {
|
||||
return Profile(
|
||||
id: id,
|
||||
name: id,
|
||||
nickname: nil,
|
||||
profilePictureUrl: nil,
|
||||
profilePictureFileName: nil,
|
||||
profileEncryptionKey: nil
|
||||
)
|
||||
}
|
||||
|
||||
static func fetchOrCreateCurrentUser() -> Profile {
|
||||
var userPublicKey: String = ""
|
||||
|
||||
let exisingProfile: Profile? = GRDBStorage.shared.read { db in
|
||||
userPublicKey = getUserHexEncodedPublicKey(db)
|
||||
|
||||
return try Profile.fetchOne(db, id: userPublicKey)
|
||||
}
|
||||
|
||||
return (exisingProfile ?? defaultFor(userPublicKey))
|
||||
}
|
||||
|
||||
static func fetchOrCreateCurrentUser(_ db: Database) -> Profile {
|
||||
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
||||
|
||||
return (
|
||||
(try? Profile.fetchOne(db, id: userPublicKey)) ??
|
||||
defaultFor(userPublicKey)
|
||||
)
|
||||
}
|
||||
|
||||
static func fetchOrCreate(id: String) -> Profile {
|
||||
let exisingProfile: Profile? = GRDBStorage.shared.read { db in
|
||||
try Profile.fetchOne(db, id: id)
|
||||
}
|
||||
|
||||
return (exisingProfile ?? defaultFor(id))
|
||||
}
|
||||
|
||||
static func fetchOrCreate(_ db: Database, id: String) -> Profile {
|
||||
return (
|
||||
(try? Profile.fetchOne(db, id: id)) ??
|
||||
defaultFor(id)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Objective-C Support
|
||||
@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(for: id)
|
||||
}
|
||||
|
||||
@objc public static func displayName(id: String, customFallback: String) -> String {
|
||||
return Profile.displayName(for: 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)
|
||||
}
|
||||
|
||||
@objc public static func displayName(id: String, thread: TSThread) -> String {
|
||||
return Profile.displayName(for: id, thread: thread)
|
||||
}
|
||||
|
||||
@objc public static var localProfileKey: OWSAES256Key? {
|
||||
Profile.fetchOrCreateCurrentUser().profileEncryptionKey
|
||||
}
|
||||
}
|
||||
|
|
59
SessionMessagingKit/Database/Models/SessionThread.swift
Normal file
59
SessionMessagingKit/Database/Models/SessionThread.swift
Normal file
|
@ -0,0 +1,59 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import GRDB
|
||||
import SessionUtilitiesKit
|
||||
|
||||
public struct SessionThread: Codable, Identifiable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
|
||||
public static var databaseTableName: String { "thread" }
|
||||
static let disappearingMessagesConfiguration = hasOne(DisappearingMessagesConfiguration.self)
|
||||
static let closedGroup = hasOne(ClosedGroup.self)
|
||||
static let openGroup = hasOne(OpenGroup.self)
|
||||
|
||||
public typealias Columns = CodingKeys
|
||||
public enum CodingKeys: String, CodingKey, ColumnExpression {
|
||||
case id
|
||||
case variant
|
||||
case creationDateTimestamp
|
||||
case shouldBeVisible
|
||||
case isPinned
|
||||
case messageDraft
|
||||
case notificationMode
|
||||
case mutedUntilTimestamp
|
||||
}
|
||||
|
||||
public enum Variant: Int, Codable, DatabaseValueConvertible {
|
||||
case contact
|
||||
case closedGroup
|
||||
case openGroup
|
||||
}
|
||||
|
||||
public enum NotificationMode: Int, Codable, DatabaseValueConvertible {
|
||||
case all
|
||||
case none
|
||||
case mentionsOnly // Only applicable to group threads
|
||||
}
|
||||
|
||||
public let id: String
|
||||
public let variant: Variant
|
||||
public let creationDateTimestamp: TimeInterval
|
||||
public let shouldBeVisible: Bool
|
||||
public let isPinned: Bool
|
||||
public let messageDraft: String?
|
||||
public let notificationMode: NotificationMode
|
||||
public let mutedUntilTimestamp: TimeInterval?
|
||||
|
||||
public var disappearingMessagesConfiguration: QueryInterfaceRequest<DisappearingMessagesConfiguration> {
|
||||
request(for: SessionThread.disappearingMessagesConfiguration)
|
||||
}
|
||||
|
||||
// public var lastInteraction
|
||||
|
||||
public var closedGroup: QueryInterfaceRequest<ClosedGroup> {
|
||||
request(for: SessionThread.closedGroup)
|
||||
}
|
||||
|
||||
public var openGroup: QueryInterfaceRequest<OpenGroup> {
|
||||
request(for: SessionThread.openGroup)
|
||||
}
|
||||
}
|
|
@ -1,12 +1,27 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import SessionUtilitiesKit
|
||||
|
||||
public extension Notification.Name {
|
||||
|
||||
static let contactUpdated = Notification.Name("contactUpdated")
|
||||
static let profileUpdated = Notification.Name("profileUpdated")
|
||||
static let localProfileDidChange = Notification.Name("localProfileDidChange")
|
||||
static let otherUsersProfileDidChange = Notification.Name("otherUsersProfileDidChange")
|
||||
static let contactBlockedStateChanged = Notification.Name("contactBlockedStateChanged")
|
||||
}
|
||||
|
||||
@objc public extension NSNotification {
|
||||
|
||||
@objc static let contactUpdated = Notification.Name.contactUpdated.rawValue as NSString
|
||||
@objc static let profileUpdated = Notification.Name.profileUpdated.rawValue as NSString
|
||||
@objc static let localProfileDidChange = Notification.Name.localProfileDidChange.rawValue as NSString
|
||||
@objc static let otherUsersProfileDidChange = Notification.Name.otherUsersProfileDidChange.rawValue as NSString
|
||||
@objc static let contactBlockedStateChanged = Notification.Name.contactBlockedStateChanged.rawValue as NSString
|
||||
}
|
||||
|
||||
extension Notification.Key {
|
||||
static let profileRecipientId = Notification.Key("profileRecipientId")
|
||||
}
|
||||
|
||||
@objc public extension NSNotification {
|
||||
static let profileRecipientIdKey = Notification.Key.profileRecipientId.rawValue as NSString
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import Sodium
|
||||
import Curve25519Kit
|
||||
|
||||
extension Storage {
|
||||
|
@ -13,12 +14,18 @@ extension Storage {
|
|||
private static let closedGroupFormationTimestampCollection = "SNClosedGroupFormationTimestampCollection"
|
||||
private static let closedGroupZombieMembersCollection = "SNClosedGroupZombieMembersCollection"
|
||||
|
||||
public func getClosedGroupEncryptionKeyPairs(for groupPublicKey: String) -> [ECKeyPair] {
|
||||
public func getClosedGroupEncryptionKeyPairs(for groupPublicKey: String) -> [Box.KeyPair] {
|
||||
var result: [ECKeyPair] = []
|
||||
Storage.read { transaction in
|
||||
result = self.getClosedGroupEncryptionKeyPairs(for: groupPublicKey, using: transaction)
|
||||
}
|
||||
return result
|
||||
.map { keyPair -> Box.KeyPair in
|
||||
Box.KeyPair(
|
||||
publicKey: keyPair.publicKey.bytes,
|
||||
secretKey: keyPair.privateKey.bytes
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public func getClosedGroupEncryptionKeyPairs(for groupPublicKey: String, using transaction: YapDatabaseReadTransaction) -> [ECKeyPair] {
|
||||
|
@ -31,7 +38,7 @@ extension Storage {
|
|||
return timestampsAndKeyPairs.sorted { $0.timestamp < $1.timestamp }.map { $0.keyPair }
|
||||
}
|
||||
|
||||
public func getLatestClosedGroupEncryptionKeyPair(for groupPublicKey: String) -> ECKeyPair? {
|
||||
public func getLatestClosedGroupEncryptionKeyPair(for groupPublicKey: String) -> Box.KeyPair? {
|
||||
return getClosedGroupEncryptionKeyPairs(for: groupPublicKey).last
|
||||
}
|
||||
|
||||
|
@ -39,10 +46,14 @@ extension Storage {
|
|||
return getClosedGroupEncryptionKeyPairs(for: groupPublicKey, using: transaction).last
|
||||
}
|
||||
|
||||
public func addClosedGroupEncryptionKeyPair(_ keyPair: ECKeyPair, for groupPublicKey: String, using transaction: Any) {
|
||||
public func addClosedGroupEncryptionKeyPair(_ keyPair: Box.KeyPair, for groupPublicKey: String, using transaction: Any) {
|
||||
let ecKeyPair: ECKeyPair = try! ECKeyPair(
|
||||
publicKeyData: Data(keyPair.publicKey),
|
||||
privateKeyData: Data(keyPair.secretKey)
|
||||
)
|
||||
let collection = Storage.getClosedGroupEncryptionKeyPairCollection(for: groupPublicKey)
|
||||
let timestamp = String(Date().timeIntervalSince1970)
|
||||
(transaction as! YapDatabaseReadWriteTransaction).setObject(keyPair, forKey: timestamp, inCollection: collection)
|
||||
(transaction as! YapDatabaseReadWriteTransaction).setObject(ecKeyPair, forKey: timestamp, inCollection: collection)
|
||||
}
|
||||
|
||||
public func removeAllClosedGroupEncryptionKeyPairs(for groupPublicKey: String, using transaction: Any) {
|
||||
|
|
|
@ -1,77 +0,0 @@
|
|||
|
||||
extension Storage {
|
||||
|
||||
private static let contactCollection = "LokiContactCollection"
|
||||
|
||||
@objc(getContactWithSessionID:)
|
||||
public func getContact(with sessionID: String) -> Contact? {
|
||||
var result: Contact?
|
||||
Storage.read { transaction in
|
||||
result = self.getContact(with: sessionID, using: transaction)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@objc(getContactWithSessionID:using:)
|
||||
public func getContact(with sessionID: String, using transaction: Any) -> Contact? {
|
||||
var result: Contact?
|
||||
let transaction = transaction as! YapDatabaseReadTransaction
|
||||
result = transaction.object(forKey: sessionID, inCollection: Storage.contactCollection) as? Contact
|
||||
if let result = result, result.sessionID == getUserHexEncodedPublicKey() {
|
||||
result.isTrusted = true // Always trust ourselves
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@objc(setContact:usingTransaction:)
|
||||
public func setContact(_ contact: Contact, using transaction: Any) {
|
||||
let transaction = transaction as! YapDatabaseReadWriteTransaction
|
||||
let oldContact = getContact(with: contact.sessionID, using: transaction)
|
||||
if contact.sessionID == getUserHexEncodedPublicKey() {
|
||||
contact.isTrusted = true // Always trust ourselves
|
||||
}
|
||||
transaction.setObject(contact, forKey: contact.sessionID, inCollection: Storage.contactCollection)
|
||||
transaction.addCompletionQueue(DispatchQueue.main) {
|
||||
// Delete old profile picture if needed
|
||||
if let oldProfilePictureFileName = oldContact?.profilePictureFileName,
|
||||
oldProfilePictureFileName != contact.profilePictureFileName {
|
||||
let path = OWSUserProfile.profileAvatarFilepath(withFilename: oldProfilePictureFileName)
|
||||
DispatchQueue.global(qos: .default).async {
|
||||
OWSFileSystem.deleteFileIfExists(path)
|
||||
}
|
||||
}
|
||||
// Post notification
|
||||
let notificationCenter = NotificationCenter.default
|
||||
notificationCenter.post(name: .contactUpdated, object: contact.sessionID)
|
||||
|
||||
if contact.sessionID == getUserHexEncodedPublicKey() {
|
||||
notificationCenter.post(name: Notification.Name(kNSNotificationName_LocalProfileDidChange), object: nil)
|
||||
}
|
||||
else {
|
||||
let userInfo = [ kNSNotificationKey_ProfileRecipientId : contact.sessionID ]
|
||||
notificationCenter.post(name: Notification.Name(kNSNotificationName_OtherUsersProfileDidChange), object: nil, userInfo: userInfo)
|
||||
}
|
||||
|
||||
if contact.isBlocked != oldContact?.isBlocked {
|
||||
notificationCenter.post(name: .contactBlockedStateChanged, object: contact.sessionID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc public func getAllContacts() -> Set<Contact> {
|
||||
var result: Set<Contact> = []
|
||||
Storage.read { transaction in
|
||||
result = self.getAllContacts(with: transaction)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@objc public func getAllContacts(with transaction: YapDatabaseReadTransaction) -> Set<Contact> {
|
||||
var result: Set<Contact> = []
|
||||
transaction.enumerateRows(inCollection: Storage.contactCollection) { _, object, _, _ in
|
||||
guard let contact = object as? Contact else { return }
|
||||
result.insert(contact)
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
|
@ -1,3 +1,6 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import PromiseKit
|
||||
import Sodium
|
||||
|
||||
|
@ -16,23 +19,22 @@ extension Storage {
|
|||
public func writeSync(with block: @escaping (Any) -> Void) {
|
||||
Storage.writeSync { block($0) }
|
||||
}
|
||||
|
||||
@objc public func getUser() -> Contact? {
|
||||
return getUser(using: nil)
|
||||
}
|
||||
|
||||
public func getUser(using transaction: YapDatabaseReadTransaction?) -> Contact? {
|
||||
let userPublicKey = getUserHexEncodedPublicKey()
|
||||
var result: Contact?
|
||||
|
||||
if let transaction = transaction {
|
||||
result = Storage.shared.getContact(with: userPublicKey, using: transaction)
|
||||
}
|
||||
else {
|
||||
Storage.read { transaction in
|
||||
result = Storage.shared.getContact(with: userPublicKey, using: transaction)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
// @objc public func getUser() -> Legacy.Contact? {
|
||||
// return getUser(using: nil)
|
||||
// }
|
||||
//
|
||||
// public func getUser(using transaction: YapDatabaseReadTransaction?) -> Legacy.Contact? {
|
||||
// let userPublicKey = getUserHexEncodedPublicKey()
|
||||
// var result: Legacy.Contact?
|
||||
//
|
||||
// if let transaction = transaction {
|
||||
// result = Storage.shared.getContact(with: userPublicKey, using: transaction)
|
||||
// }
|
||||
// else {
|
||||
// Storage.read { transaction in
|
||||
// result = Storage.shared.getContact(with: userPublicKey, using: transaction)
|
||||
// }
|
||||
// }
|
||||
// return result
|
||||
// }
|
||||
}
|
||||
|
|
|
@ -318,7 +318,7 @@ NSString *const TSLazyRestoreAttachmentsGroup = @"TSLazyRestoreAttachmentsGroup"
|
|||
|
||||
if (!thread.isGroupThread) {
|
||||
TSContactThread *contactThead = (TSContactThread *)thread;
|
||||
SNContact *contact = [LKStorage.shared getContactWithSessionID:[contactThead contactSessionID]];
|
||||
SMKContact *contact = [SMKContact fetchOrCreateWithId:[contactThead contactSessionID]];
|
||||
|
||||
if (contact == nil || !contact.didApproveMe) {
|
||||
return nil;
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import PromiseKit
|
||||
import SignalCoreKit
|
||||
import SessionUtilitiesKit
|
||||
|
||||
public final class AttachmentUploadJob : NSObject, Job, NSCoding { // NSObject/NSCoding conformance is needed for YapDatabase compatibility
|
||||
|
|
|
@ -62,25 +62,37 @@ public final class MessageReceiveJob : NSObject, Job, NSCoding { // NSObject/NSC
|
|||
JobQueue.currentlyExecutingJobs.insert(id)
|
||||
}
|
||||
let (promise, seal) = Promise<Void>.pending()
|
||||
SNMessagingKitConfiguration.shared.storage.write(with: { transaction in // Intentionally capture self
|
||||
do {
|
||||
let isRetry = (self.failureCount != 0)
|
||||
let (message, proto) = try MessageReceiver.parse(self.data, openGroupMessageServerID: self.openGroupMessageServerID, isRetry: isRetry, using: transaction)
|
||||
message.serverHash = self.serverHash
|
||||
try MessageReceiver.handle(message, associatedWithProto: proto, openGroupID: self.openGroupID, isBackgroundPoll: self.isBackgroundPoll, using: transaction)
|
||||
self.handleSuccess()
|
||||
seal.fulfill(())
|
||||
} catch {
|
||||
if let error = error as? MessageReceiver.Error, !error.isRetryable {
|
||||
SNLog("Message receive job permanently failed due to error: \(error).")
|
||||
self.handlePermanentFailure(error: error)
|
||||
} else {
|
||||
SNLog("Couldn't receive message due to error: \(error).")
|
||||
self.handleFailure(error: error)
|
||||
|
||||
GRDBStorage.shared.writeAsync(
|
||||
updates: { db in
|
||||
SNMessagingKitConfiguration.shared.storage.write(with: { transaction in // Intentionally capture self
|
||||
do {
|
||||
let isRetry = (self.failureCount != 0)
|
||||
let (message, proto) = try MessageReceiver.parse(db, self.data, openGroupMessageServerID: self.openGroupMessageServerID, isRetry: isRetry, using: transaction)
|
||||
message.serverHash = self.serverHash
|
||||
try MessageReceiver.handle(db, message, associatedWithProto: proto, openGroupID: self.openGroupID, isBackgroundPoll: self.isBackgroundPoll, using: transaction)
|
||||
self.handleSuccess()
|
||||
seal.fulfill(())
|
||||
} catch {
|
||||
if let error = error as? MessageReceiver.Error, !error.isRetryable {
|
||||
SNLog("Message receive job permanently failed due to error: \(error).")
|
||||
self.handlePermanentFailure(error: error)
|
||||
} else {
|
||||
SNLog("Couldn't receive message due to error: \(error).")
|
||||
self.handleFailure(error: error)
|
||||
}
|
||||
seal.fulfill(()) // The promise is just used to keep track of when we're done
|
||||
}
|
||||
}, completion: { })
|
||||
},
|
||||
completion: { _, result in
|
||||
switch result {
|
||||
case .failure(let error): self.handleFailure(error: error)
|
||||
default: break
|
||||
}
|
||||
seal.fulfill(()) // The promise is just used to keep track of when we're done
|
||||
}
|
||||
}, completion: { })
|
||||
)
|
||||
|
||||
return promise
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import Sodium
|
||||
import Curve25519Kit
|
||||
import SessionUtilitiesKit
|
||||
|
||||
|
@ -18,7 +19,7 @@ public final class ClosedGroupControlMessage : ControlMessage {
|
|||
|
||||
// MARK: Kind
|
||||
public enum Kind : CustomStringConvertible {
|
||||
case new(publicKey: Data, name: String, encryptionKeyPair: ECKeyPair, members: [Data], admins: [Data], expirationTimer: UInt32)
|
||||
case new(publicKey: Data, name: String, encryptionKeyPair: Box.KeyPair, members: [Data], admins: [Data], expirationTimer: UInt32)
|
||||
/// An encryption key pair encrypted for each member individually.
|
||||
///
|
||||
/// - Note: `publicKey` is only set when an encryption key pair is sent in a one-to-one context (i.e. not in a group).
|
||||
|
@ -95,7 +96,7 @@ public final class ClosedGroupControlMessage : ControlMessage {
|
|||
switch kind {
|
||||
case .new(let publicKey, let name, let encryptionKeyPair, let members, let admins, let expirationTimer):
|
||||
return !publicKey.isEmpty && !name.isEmpty && !encryptionKeyPair.publicKey.isEmpty
|
||||
&& !encryptionKeyPair.privateKey.isEmpty && !members.isEmpty && !admins.isEmpty
|
||||
&& !encryptionKeyPair.secretKey.isEmpty && !members.isEmpty && !admins.isEmpty
|
||||
case .encryptionKeyPair: return true
|
||||
case .nameChange(let name): return !name.isEmpty
|
||||
case .membersAdded(let members): return !members.isEmpty
|
||||
|
@ -113,11 +114,15 @@ public final class ClosedGroupControlMessage : ControlMessage {
|
|||
case "new":
|
||||
guard let publicKey = coder.decodeObject(forKey: "publicKey") as? Data,
|
||||
let name = coder.decodeObject(forKey: "name") as? String,
|
||||
let encryptionKeyPair = coder.decodeObject(forKey: "encryptionKeyPair") as? ECKeyPair,
|
||||
let encryptionKeyPair = coder.decodeObject(forKey: "encryptionKeyPair") as? SessionUtilitiesKit.Legacy.KeyPair,
|
||||
let members = coder.decodeObject(forKey: "members") as? [Data],
|
||||
let admins = coder.decodeObject(forKey: "admins") as? [Data] else { return nil }
|
||||
let expirationTimer = coder.decodeObject(forKey: "expirationTimer") as? UInt32 ?? 0
|
||||
self.kind = .new(publicKey: publicKey, name: name, encryptionKeyPair: encryptionKeyPair, members: members, admins: admins, expirationTimer: expirationTimer)
|
||||
let keyPair: Box.KeyPair = Box.KeyPair(
|
||||
publicKey: encryptionKeyPair.publicKey.bytes,
|
||||
secretKey: encryptionKeyPair.privateKey.bytes
|
||||
)
|
||||
self.kind = .new(publicKey: publicKey, name: name, encryptionKeyPair: keyPair, members: members, admins: admins, expirationTimer: expirationTimer)
|
||||
case "encryptionKeyPair":
|
||||
let publicKey = coder.decodeObject(forKey: "publicKey") as? Data
|
||||
guard let wrappers = coder.decodeObject(forKey: "wrappers") as? [KeyPairWrapper] else { return nil }
|
||||
|
@ -172,7 +177,7 @@ public final class ClosedGroupControlMessage : ControlMessage {
|
|||
}
|
||||
|
||||
// MARK: Proto Conversion
|
||||
public override class func fromProto(_ proto: SNProtoContent) -> ClosedGroupControlMessage? {
|
||||
public override class func fromProto(_ proto: SNProtoContent, sender: String) -> ClosedGroupControlMessage? {
|
||||
guard let closedGroupControlMessageProto = proto.dataMessage?.closedGroupControlMessage else { return nil }
|
||||
let kind: Kind
|
||||
switch closedGroupControlMessageProto.type {
|
||||
|
@ -180,14 +185,9 @@ public final class ClosedGroupControlMessage : ControlMessage {
|
|||
guard let publicKey = closedGroupControlMessageProto.publicKey, let name = closedGroupControlMessageProto.name,
|
||||
let encryptionKeyPairAsProto = closedGroupControlMessageProto.encryptionKeyPair else { return nil }
|
||||
let expirationTimer = closedGroupControlMessageProto.expirationTimer
|
||||
do {
|
||||
let encryptionKeyPair = try ECKeyPair(publicKeyData: encryptionKeyPairAsProto.publicKey.removing05PrefixIfNeeded(), privateKeyData: encryptionKeyPairAsProto.privateKey)
|
||||
kind = .new(publicKey: publicKey, name: name, encryptionKeyPair: encryptionKeyPair,
|
||||
members: closedGroupControlMessageProto.members, admins: closedGroupControlMessageProto.admins, expirationTimer: expirationTimer)
|
||||
} catch {
|
||||
SNLog("Couldn't parse key pair.")
|
||||
return nil
|
||||
}
|
||||
let encryptionKeyPair = Box.KeyPair(publicKey: encryptionKeyPairAsProto.publicKey.removing05PrefixIfNeeded().bytes, secretKey: encryptionKeyPairAsProto.privateKey.bytes)
|
||||
kind = .new(publicKey: publicKey, name: name, encryptionKeyPair: encryptionKeyPair,
|
||||
members: closedGroupControlMessageProto.members, admins: closedGroupControlMessageProto.admins, expirationTimer: expirationTimer)
|
||||
case .encryptionKeyPair:
|
||||
let publicKey = closedGroupControlMessageProto.publicKey
|
||||
let wrappers = closedGroupControlMessageProto.wrappers.compactMap { KeyPairWrapper.fromProto($0) }
|
||||
|
@ -219,7 +219,7 @@ public final class ClosedGroupControlMessage : ControlMessage {
|
|||
closedGroupControlMessage = SNProtoDataMessageClosedGroupControlMessage.builder(type: .new)
|
||||
closedGroupControlMessage.setPublicKey(publicKey)
|
||||
closedGroupControlMessage.setName(name)
|
||||
let encryptionKeyPairAsProto = SNProtoKeyPair.builder(publicKey: encryptionKeyPair.publicKey, privateKey: encryptionKeyPair.privateKey)
|
||||
let encryptionKeyPairAsProto = SNProtoKeyPair.builder(publicKey: Data(encryptionKeyPair.publicKey), privateKey: Data(encryptionKeyPair.secretKey))
|
||||
do {
|
||||
closedGroupControlMessage.setEncryptionKeyPair(try encryptionKeyPairAsProto.build())
|
||||
} catch {
|
||||
|
|
|
@ -1,88 +1,73 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import GRDB
|
||||
import SessionUtilitiesKit
|
||||
|
||||
extension ConfigurationMessage {
|
||||
|
||||
public static func getCurrent(with transaction: YapDatabaseReadTransaction) -> ConfigurationMessage? {
|
||||
let storage = Storage.shared
|
||||
guard let user = storage.getUser(using: transaction) else { return nil }
|
||||
public static func getCurrent(_ db: Database) throws -> ConfigurationMessage? {
|
||||
let profile: Profile = Profile.fetchOrCreateCurrentUser(db)
|
||||
|
||||
let displayName = user.name
|
||||
let profilePictureURL = user.profilePictureURL
|
||||
let profileKey = user.profileEncryptionKey?.keyData
|
||||
let displayName: String = profile.name
|
||||
let profilePictureUrl: String? = profile.profilePictureUrl
|
||||
let profileKey: Data? = profile.profileEncryptionKey?.keyData
|
||||
var closedGroups: Set<ClosedGroup> = []
|
||||
var openGroups: Set<String> = []
|
||||
var contacts: Set<ConfigurationMessage.Contact> = []
|
||||
|
||||
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.isClosedGroup(groupPublicKey, using: transaction),
|
||||
let encryptionKeyPair = storage.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.getV2OpenGroup(for: threadId) {
|
||||
openGroups.insert("\(v2OpenGroup.server)/\(v2OpenGroup.room)?public_key=\(v2OpenGroup.publicKey)")
|
||||
}
|
||||
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
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let currentUserPublicKey: String = getUserHexEncodedPublicKey()
|
||||
|
||||
contacts = storage.getAllContacts(with: transaction)
|
||||
.compactMap { contact -> ConfigurationMessage.Contact? in
|
||||
let threadID = TSContactThread.threadID(fromContactSessionID: contact.sessionID)
|
||||
|
||||
guard
|
||||
// Skip the current user
|
||||
contact.sessionID != currentUserPublicKey &&
|
||||
// Contacts which have visible threads
|
||||
TSContactThread.fetch(uniqueId: threadID, transaction: transaction)?.shouldBeVisible == true && (
|
||||
|
||||
// Include already approved contacts
|
||||
contact.isApproved ||
|
||||
contact.didApproveMe ||
|
||||
|
||||
// Sync blocked contacts
|
||||
contact.isBlocked ||
|
||||
contact.hasBeenBlocked
|
||||
)
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
let contacts: Set<CMContact> = try Contact.fetchAll(db)
|
||||
.compactMap { contact -> CMContact? in
|
||||
guard contact.id != currentUserPublicKey else { return nil }
|
||||
|
||||
// Can just default the 'hasX' values to true as they will be set to this
|
||||
// when converting to proto anyway
|
||||
let profilePictureURL = contact.profilePictureURL
|
||||
let profileKey = contact.profileEncryptionKey?.keyData
|
||||
let profile: Profile? = try? Profile.fetchOne(db, id: contact.id)
|
||||
|
||||
return ConfigurationMessage.Contact(
|
||||
publicKey: contact.sessionID,
|
||||
displayName: (contact.name ?? contact.sessionID),
|
||||
profilePictureURL: profilePictureURL,
|
||||
profileKey: profileKey,
|
||||
return CMContact(
|
||||
publicKey: contact.id,
|
||||
displayName: (profile?.name ?? contact.id),
|
||||
profilePictureURL: profile?.profilePictureUrl,
|
||||
profileKey: profile?.profileEncryptionKey?.keyData,
|
||||
hasIsApproved: true,
|
||||
isApproved: contact.isApproved,
|
||||
hasIsBlocked: true,
|
||||
|
@ -95,7 +80,7 @@ extension ConfigurationMessage {
|
|||
|
||||
return ConfigurationMessage(
|
||||
displayName: displayName,
|
||||
profilePictureURL: profilePictureURL,
|
||||
profilePictureURL: profilePictureUrl,
|
||||
profileKey: profileKey,
|
||||
closedGroups: closedGroups,
|
||||
openGroups: openGroups,
|
||||
|
|
|
@ -11,14 +11,14 @@ public final class ConfigurationMessage : ControlMessage {
|
|||
public var displayName: String?
|
||||
public var profilePictureURL: String?
|
||||
public var profileKey: Data?
|
||||
public var contacts: Set<Contact> = []
|
||||
public var contacts: Set<CMContact> = []
|
||||
|
||||
public override var isSelfSendValid: Bool { true }
|
||||
|
||||
// MARK: Initialization
|
||||
public override init() { super.init() }
|
||||
|
||||
public init(displayName: String?, profilePictureURL: String?, profileKey: Data?, closedGroups: Set<ClosedGroup>, openGroups: Set<String>, contacts: Set<Contact>) {
|
||||
public init(displayName: String?, profilePictureURL: String?, profileKey: Data?, closedGroups: Set<ClosedGroup>, openGroups: Set<String>, contacts: Set<CMContact>) {
|
||||
super.init()
|
||||
self.displayName = displayName
|
||||
self.profilePictureURL = profilePictureURL
|
||||
|
@ -36,7 +36,7 @@ public final class ConfigurationMessage : ControlMessage {
|
|||
if let displayName = coder.decodeObject(forKey: "displayName") as! String? { self.displayName = displayName }
|
||||
if let profilePictureURL = coder.decodeObject(forKey: "profilePictureURL") as! String? { self.profilePictureURL = profilePictureURL }
|
||||
if let profileKey = coder.decodeObject(forKey: "profileKey") as! Data? { self.profileKey = profileKey }
|
||||
if let contacts = coder.decodeObject(forKey: "contacts") as! Set<Contact>? { self.contacts = contacts }
|
||||
if let contacts = coder.decodeObject(forKey: "contacts") as! Set<CMContact>? { self.contacts = contacts }
|
||||
}
|
||||
|
||||
public override func encode(with coder: NSCoder) {
|
||||
|
@ -50,14 +50,14 @@ public final class ConfigurationMessage : ControlMessage {
|
|||
}
|
||||
|
||||
// MARK: Proto Conversion
|
||||
public override class func fromProto(_ proto: SNProtoContent) -> ConfigurationMessage? {
|
||||
public override class func fromProto(_ proto: SNProtoContent, sender: String) -> ConfigurationMessage? {
|
||||
guard let configurationProto = proto.configurationMessage else { return nil }
|
||||
let displayName = configurationProto.displayName
|
||||
let profilePictureURL = configurationProto.profilePicture
|
||||
let profileKey = configurationProto.profileKey
|
||||
let closedGroups = Set(configurationProto.closedGroups.compactMap { ClosedGroup.fromProto($0) })
|
||||
let openGroups = Set(configurationProto.openGroups)
|
||||
let contacts = Set(configurationProto.contacts.compactMap { Contact.fromProto($0) })
|
||||
let contacts = Set(configurationProto.contacts.compactMap { CMContact.fromProto($0) })
|
||||
return ConfigurationMessage(displayName: displayName, profilePictureURL: profilePictureURL, profileKey: profileKey,
|
||||
closedGroups: closedGroups, openGroups: openGroups, contacts: contacts)
|
||||
}
|
||||
|
@ -89,7 +89,7 @@ public final class ConfigurationMessage : ControlMessage {
|
|||
displayName: \(displayName ?? "null"),
|
||||
profilePictureURL: \(profilePictureURL ?? "null"),
|
||||
profileKey: \(profileKey?.toHexString() ?? "null"),
|
||||
contacts: \([Contact](contacts).prettifiedDescription)
|
||||
contacts: \([CMContact](contacts).prettifiedDescription)
|
||||
)
|
||||
"""
|
||||
}
|
||||
|
@ -192,7 +192,7 @@ extension ConfigurationMessage {
|
|||
extension ConfigurationMessage {
|
||||
|
||||
@objc(SNConfigurationMessageContact)
|
||||
public final class Contact : NSObject, NSCoding { // NSObject/NSCoding conformance is needed for YapDatabase compatibility
|
||||
public final class CMContact : NSObject, NSCoding { // NSObject/NSCoding conformance is needed for YapDatabase compatibility
|
||||
public var publicKey: String?
|
||||
public var displayName: String?
|
||||
public var profilePictureURL: String?
|
||||
|
@ -259,8 +259,8 @@ extension ConfigurationMessage {
|
|||
coder.encode(didApproveMe, forKey: "didApproveMe")
|
||||
}
|
||||
|
||||
public static func fromProto(_ proto: SNProtoConfigurationMessageContact) -> Contact? {
|
||||
let result: Contact = Contact(
|
||||
public static func fromProto(_ proto: SNProtoConfigurationMessageContact) -> CMContact? {
|
||||
let result: CMContact = CMContact(
|
||||
publicKey: proto.publicKey.toHexString(),
|
||||
displayName: proto.name,
|
||||
profilePictureURL: proto.profilePicture,
|
||||
|
|
|
@ -60,7 +60,7 @@ public final class DataExtractionNotification : ControlMessage {
|
|||
}
|
||||
|
||||
// MARK: Proto Conversion
|
||||
public override class func fromProto(_ proto: SNProtoContent) -> DataExtractionNotification? {
|
||||
public override class func fromProto(_ proto: SNProtoContent, sender: String) -> DataExtractionNotification? {
|
||||
guard let dataExtractionNotification = proto.dataExtractionNotification else { return nil }
|
||||
let kind: Kind
|
||||
switch dataExtractionNotification.type {
|
||||
|
|
|
@ -39,7 +39,7 @@ public final class ExpirationTimerUpdate : ControlMessage {
|
|||
}
|
||||
|
||||
// MARK: Proto Conversion
|
||||
public override class func fromProto(_ proto: SNProtoContent) -> ExpirationTimerUpdate? {
|
||||
public override class func fromProto(_ proto: SNProtoContent, sender: String) -> ExpirationTimerUpdate? {
|
||||
guard let dataMessageProto = proto.dataMessage else { return nil }
|
||||
let isExpirationTimerUpdate = (dataMessageProto.flags & UInt32(SNProtoDataMessage.SNProtoDataMessageFlags.expirationTimerUpdate.rawValue)) != 0
|
||||
guard isExpirationTimerUpdate else { return nil }
|
||||
|
|
|
@ -30,7 +30,7 @@ public final class MessageRequestResponse: ControlMessage {
|
|||
|
||||
// MARK: - Proto Conversion
|
||||
|
||||
public override class func fromProto(_ proto: SNProtoContent) -> MessageRequestResponse? {
|
||||
public override class func fromProto(_ proto: SNProtoContent, sender: String) -> MessageRequestResponse? {
|
||||
guard let messageRequestResponseProto = proto.messageRequestResponse else { return nil }
|
||||
|
||||
let isApproved = messageRequestResponseProto.isApproved
|
||||
|
|
|
@ -31,7 +31,7 @@ public final class ReadReceipt : ControlMessage {
|
|||
}
|
||||
|
||||
// MARK: Proto Conversion
|
||||
public override class func fromProto(_ proto: SNProtoContent) -> ReadReceipt? {
|
||||
public override class func fromProto(_ proto: SNProtoContent, sender: String) -> ReadReceipt? {
|
||||
guard let receiptProto = proto.receiptMessage, receiptProto.type == .read else { return nil }
|
||||
let timestamps = receiptProto.timestamp
|
||||
guard !timestamps.isEmpty else { return nil }
|
||||
|
|
|
@ -58,7 +58,7 @@ public final class TypingIndicator : ControlMessage {
|
|||
}
|
||||
|
||||
// MARK: Proto Conversion
|
||||
public override class func fromProto(_ proto: SNProtoContent) -> TypingIndicator? {
|
||||
public override class func fromProto(_ proto: SNProtoContent, sender: String) -> TypingIndicator? {
|
||||
guard let typingIndicatorProto = proto.typingMessage else { return nil }
|
||||
let kind = Kind.fromProto(typingIndicatorProto.action)
|
||||
return TypingIndicator(kind: kind)
|
||||
|
|
|
@ -36,7 +36,7 @@ public final class UnsendRequest: ControlMessage {
|
|||
}
|
||||
|
||||
// MARK: Proto Conversion
|
||||
public override class func fromProto(_ proto: SNProtoContent) -> UnsendRequest? {
|
||||
public override class func fromProto(_ proto: SNProtoContent, sender: String) -> UnsendRequest? {
|
||||
guard let unsendRequestProto = proto.unsendRequest else { return nil }
|
||||
let timestamp = unsendRequestProto.timestamp
|
||||
let author = unsendRequestProto.author
|
||||
|
|
|
@ -53,8 +53,8 @@ public class Message : NSObject, NSCoding { // NSObject/NSCoding conformance is
|
|||
}
|
||||
|
||||
// MARK: Proto Conversion
|
||||
public class func fromProto(_ proto: SNProtoContent) -> Self? {
|
||||
preconditionFailure("fromProto(_:) is abstract and must be overridden.")
|
||||
public class func fromProto(_ proto: SNProtoContent, sender: String) -> Self? {
|
||||
preconditionFailure("fromProto(_:sender:) is abstract and must be overridden.")
|
||||
}
|
||||
|
||||
public func toProto(using transaction: YapDatabaseReadWriteTransaction) -> SNProtoContent? {
|
||||
|
|
|
@ -144,7 +144,7 @@ const NSUInteger kOversizeTextMessageSizeThreshold = 2 * 1024;
|
|||
|
||||
- (void)setExpiresInSeconds:(uint32_t)expiresInSeconds
|
||||
{
|
||||
uint32_t maxExpirationDuration = [OWSDisappearingMessagesConfiguration maxDurationSeconds];
|
||||
uint32_t maxExpirationDuration = [SMKDisappearingMessagesConfiguration maxDurationSeconds];
|
||||
|
||||
_expiresInSeconds = MIN(expiresInSeconds, maxExpirationDuration);
|
||||
[self updateExpiresAt];
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import SessionUtilitiesKit
|
||||
|
||||
@objc public extension TSOutgoingMessage {
|
||||
|
@ -9,11 +12,11 @@ import SessionUtilitiesKit
|
|||
|
||||
static func from(_ visibleMessage: VisibleMessage, associatedWith thread: TSThread, using transaction: YapDatabaseReadWriteTransaction? = nil) -> TSOutgoingMessage {
|
||||
var expiration: UInt32 = 0
|
||||
let disappearingMessagesConfigurationOrNil: OWSDisappearingMessagesConfiguration?
|
||||
let disappearingMessagesConfigurationOrNil: Legacy.DisappearingMessagesConfiguration?
|
||||
if let transaction = transaction {
|
||||
disappearingMessagesConfigurationOrNil = OWSDisappearingMessagesConfiguration.fetch(uniqueId: thread.uniqueId!, transaction: transaction)
|
||||
disappearingMessagesConfigurationOrNil = Legacy.DisappearingMessagesConfiguration.fetch(uniqueId: thread.uniqueId!, transaction: transaction)
|
||||
} else {
|
||||
disappearingMessagesConfigurationOrNil = OWSDisappearingMessagesConfiguration.fetch(uniqueId: thread.uniqueId!)
|
||||
disappearingMessagesConfigurationOrNil = Legacy.DisappearingMessagesConfiguration.fetch(uniqueId: thread.uniqueId!)
|
||||
}
|
||||
if let disappearingMessagesConfiguration = disappearingMessagesConfigurationOrNil {
|
||||
expiration = disappearingMessagesConfiguration.isEnabled ? disappearingMessagesConfiguration.durationSeconds : 0
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import SignalCoreKit
|
||||
|
||||
@objc(OWSTypingIndicatorInteraction)
|
||||
public class TypingIndicatorInteraction: TSInteraction {
|
||||
|
|
|
@ -1,72 +1,72 @@
|
|||
import SessionUtilitiesKit
|
||||
|
||||
public extension VisibleMessage {
|
||||
|
||||
@objc(SNProfile)
|
||||
class Profile : NSObject, NSCoding {
|
||||
public var displayName: String?
|
||||
public var profileKey: Data?
|
||||
public var profilePictureURL: String?
|
||||
|
||||
internal init(displayName: String, profileKey: Data? = nil, profilePictureURL: String? = nil) {
|
||||
self.displayName = displayName
|
||||
self.profileKey = profileKey
|
||||
self.profilePictureURL = profilePictureURL
|
||||
}
|
||||
|
||||
public required init?(coder: NSCoder) {
|
||||
if let displayName = coder.decodeObject(forKey: "displayName") as! String? { self.displayName = displayName }
|
||||
if let profileKey = coder.decodeObject(forKey: "profileKey") as! Data? { self.profileKey = profileKey }
|
||||
if let profilePictureURL = coder.decodeObject(forKey: "profilePictureURL") as! String? { self.profilePictureURL = profilePictureURL }
|
||||
}
|
||||
|
||||
public func encode(with coder: NSCoder) {
|
||||
coder.encode(displayName, forKey: "displayName")
|
||||
coder.encode(profileKey, forKey: "profileKey")
|
||||
coder.encode(profilePictureURL, forKey: "profilePictureURL")
|
||||
}
|
||||
|
||||
public static func fromProto(_ proto: SNProtoDataMessage) -> Profile? {
|
||||
guard let profileProto = proto.profile, let displayName = profileProto.displayName else { return nil }
|
||||
let profileKey = proto.profileKey
|
||||
let profilePictureURL = profileProto.profilePicture
|
||||
if let profileKey = profileKey, let profilePictureURL = profilePictureURL {
|
||||
return Profile(displayName: displayName, profileKey: profileKey, profilePictureURL: profilePictureURL)
|
||||
} else {
|
||||
return Profile(displayName: displayName)
|
||||
}
|
||||
}
|
||||
|
||||
public func toProto() -> SNProtoDataMessage? {
|
||||
guard let displayName = displayName else {
|
||||
SNLog("Couldn't construct profile proto from: \(self).")
|
||||
return nil
|
||||
}
|
||||
let dataMessageProto = SNProtoDataMessage.builder()
|
||||
let profileProto = SNProtoDataMessageLokiProfile.builder()
|
||||
profileProto.setDisplayName(displayName)
|
||||
if let profileKey = profileKey, let profilePictureURL = profilePictureURL {
|
||||
dataMessageProto.setProfileKey(profileKey)
|
||||
profileProto.setProfilePicture(profilePictureURL)
|
||||
}
|
||||
do {
|
||||
dataMessageProto.setProfile(try profileProto.build())
|
||||
return try dataMessageProto.build()
|
||||
} catch {
|
||||
SNLog("Couldn't construct profile proto from: \(self).")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Description
|
||||
public override var description: String {
|
||||
"""
|
||||
Profile(
|
||||
displayName: \(displayName ?? "null"),
|
||||
profileKey: \(profileKey?.description ?? "null"),
|
||||
profilePictureURL: \(profilePictureURL ?? "null")
|
||||
)
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
//import SessionUtilitiesKit
|
||||
//
|
||||
//public extension VisibleMessage {
|
||||
//
|
||||
// @objc(SNProfile)
|
||||
// class Profile : NSObject, NSCoding {
|
||||
// public var displayName: String?
|
||||
// public var profileKey: Data?
|
||||
// public var profilePictureURL: String?
|
||||
//
|
||||
// internal init(displayName: String, profileKey: Data? = nil, profilePictureURL: String? = nil) {
|
||||
// self.displayName = displayName
|
||||
// self.profileKey = profileKey
|
||||
// self.profilePictureURL = profilePictureURL
|
||||
// }
|
||||
//
|
||||
// public required init?(coder: NSCoder) {
|
||||
// if let displayName = coder.decodeObject(forKey: "displayName") as! String? { self.displayName = displayName }
|
||||
// if let profileKey = coder.decodeObject(forKey: "profileKey") as! Data? { self.profileKey = profileKey }
|
||||
// if let profilePictureURL = coder.decodeObject(forKey: "profilePictureURL") as! String? { self.profilePictureURL = profilePictureURL }
|
||||
// }
|
||||
//
|
||||
// public func encode(with coder: NSCoder) {
|
||||
// coder.encode(displayName, forKey: "displayName")
|
||||
// coder.encode(profileKey, forKey: "profileKey")
|
||||
// coder.encode(profilePictureURL, forKey: "profilePictureURL")
|
||||
// }
|
||||
//
|
||||
// public static func fromProto(_ proto: SNProtoDataMessage, sessionId: String) -> Profile? {
|
||||
// guard let profileProto = proto.profile, let displayName = profileProto.displayName else { return nil }
|
||||
// let profileKey = proto.profileKey
|
||||
// let profilePictureURL = profileProto.profilePicture
|
||||
// if let profileKey = profileKey, let profilePictureURL = profilePictureURL {
|
||||
// return Profile(displayName: displayName, profileKey: profileKey, profilePictureURL: profilePictureURL)
|
||||
// } else {
|
||||
// return Profile(displayName: displayName)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// public func toProto() -> SNProtoDataMessage? {
|
||||
// guard let displayName = displayName else {
|
||||
// SNLog("Couldn't construct profile proto from: \(self).")
|
||||
// return nil
|
||||
// }
|
||||
// let dataMessageProto = SNProtoDataMessage.builder()
|
||||
// let profileProto = SNProtoDataMessageLokiProfile.builder()
|
||||
// profileProto.setDisplayName(displayName)
|
||||
// if let profileKey = profileKey, let profilePictureURL = profilePictureURL {
|
||||
// dataMessageProto.setProfileKey(profileKey)
|
||||
// profileProto.setProfilePicture(profilePictureURL)
|
||||
// }
|
||||
// do {
|
||||
// dataMessageProto.setProfile(try profileProto.build())
|
||||
// return try dataMessageProto.build()
|
||||
// } catch {
|
||||
// SNLog("Couldn't construct profile proto from: \(self).")
|
||||
// return nil
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // MARK: Description
|
||||
// public override var description: String {
|
||||
// """
|
||||
// Profile(
|
||||
// displayName: \(displayName ?? "null"),
|
||||
// profileKey: \(profileKey?.description ?? "null"),
|
||||
// profilePictureURL: \(profilePictureURL ?? "null")
|
||||
// )
|
||||
// """
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
|
|
@ -10,8 +10,8 @@ public final class VisibleMessage : Message {
|
|||
@objc public var attachmentIDs: [String] = []
|
||||
@objc public var quote: Quote?
|
||||
@objc public var linkPreview: LinkPreview?
|
||||
@objc public var contact: Contact?
|
||||
@objc public var profile: Profile?
|
||||
@objc public var contact: Legacy.Contact?
|
||||
@objc public var profile: Legacy.Profile?
|
||||
@objc public var openGroupInvitation: OpenGroupInvitation?
|
||||
|
||||
public override var isSelfSendValid: Bool { true }
|
||||
|
@ -37,7 +37,7 @@ public final class VisibleMessage : Message {
|
|||
if let quote = coder.decodeObject(forKey: "quote") as! Quote? { self.quote = quote }
|
||||
if let linkPreview = coder.decodeObject(forKey: "linkPreview") as! LinkPreview? { self.linkPreview = linkPreview }
|
||||
// TODO: Contact
|
||||
if let profile = coder.decodeObject(forKey: "profile") as! Profile? { self.profile = profile }
|
||||
if let profile = coder.decodeObject(forKey: "profile") as! Legacy.Profile? { self.profile = profile }
|
||||
if let openGroupInvitation = coder.decodeObject(forKey: "openGroupInvitation") as! OpenGroupInvitation? { self.openGroupInvitation = openGroupInvitation }
|
||||
}
|
||||
|
||||
|
@ -54,7 +54,7 @@ public final class VisibleMessage : Message {
|
|||
}
|
||||
|
||||
// MARK: Proto Conversion
|
||||
public override class func fromProto(_ proto: SNProtoContent) -> VisibleMessage? {
|
||||
public override class func fromProto(_ proto: SNProtoContent, sender: String) -> VisibleMessage? {
|
||||
guard let dataMessage = proto.dataMessage else { return nil }
|
||||
let result = VisibleMessage()
|
||||
result.text = dataMessage.body
|
||||
|
@ -62,7 +62,7 @@ public final class VisibleMessage : Message {
|
|||
if let quoteProto = dataMessage.quote, let quote = Quote.fromProto(quoteProto) { result.quote = quote }
|
||||
if let linkPreviewProto = dataMessage.preview.first, let linkPreview = LinkPreview.fromProto(linkPreviewProto) { result.linkPreview = linkPreview }
|
||||
// TODO: Contact
|
||||
if let profile = Profile.fromProto(dataMessage) { result.profile = profile }
|
||||
if let profile = Legacy.Profile.fromProto(dataMessage) { result.profile = profile }
|
||||
if let openGroupInvitationProto = dataMessage.openGroupInvitation,
|
||||
let openGroupInvitation = OpenGroupInvitation.fromProto(openGroupInvitationProto) { result.openGroupInvitation = openGroupInvitation }
|
||||
result.syncTarget = dataMessage.syncTarget
|
||||
|
|
|
@ -8,7 +8,7 @@ import SessionUtilitiesKit
|
|||
|
||||
@objc(SNOpenGroupAPIV2)
|
||||
public final class OpenGroupAPIV2 : NSObject {
|
||||
private static var authTokenPromises: [String: Promise<String>] = [:]
|
||||
private static var authTokenPromises: Atomic<[String: Promise<String>]> = Atomic([:])
|
||||
private static var hasPerformedInitialPoll: [String: Bool] = [:]
|
||||
private static var hasUpdatedLastOpenDate = false
|
||||
public static let workQueue = DispatchQueue(label: "OpenGroupAPIV2.workQueue", qos: .userInitiated) // It's important that this is a serial queue
|
||||
|
@ -215,7 +215,7 @@ public final class OpenGroupAPIV2 : NSObject {
|
|||
if let authToken = storage.getAuthToken(for: room, on: server) {
|
||||
return Promise.value(authToken)
|
||||
} else {
|
||||
if let authTokenPromise = authTokenPromises["\(server).\(room)"] {
|
||||
if let authTokenPromise = authTokenPromises.wrappedValue["\(server).\(room)"] {
|
||||
return authTokenPromise
|
||||
} else {
|
||||
let promise = requestNewAuthToken(for: room, on: server)
|
||||
|
@ -230,11 +230,11 @@ public final class OpenGroupAPIV2 : NSObject {
|
|||
return promise
|
||||
}
|
||||
promise.done(on: OpenGroupAPIV2.workQueue) { _ in
|
||||
authTokenPromises["\(server).\(room)"] = nil
|
||||
authTokenPromises.mutate { $0["\(server).\(room)"] = nil }
|
||||
}.catch(on: OpenGroupAPIV2.workQueue) { _ in
|
||||
authTokenPromises["\(server).\(room)"] = nil
|
||||
authTokenPromises.mutate { $0["\(server).\(room)"] = nil }
|
||||
}
|
||||
authTokenPromises["\(server).\(room)"] = promise
|
||||
authTokenPromises.mutate { $0["\(server).\(room)"] = promise }
|
||||
return promise
|
||||
}
|
||||
}
|
||||
|
@ -251,7 +251,7 @@ public final class OpenGroupAPIV2 : NSObject {
|
|||
let ephemeralPublicKey = Data(base64Encoded: base64EncodedEphemeralPublicKey) else {
|
||||
throw Error.parsingFailed
|
||||
}
|
||||
let symmetricKey = try AESGCM.generateSymmetricKey(x25519PublicKey: ephemeralPublicKey, x25519PrivateKey: userKeyPair.privateKey)
|
||||
let symmetricKey = try AESGCM.generateSymmetricKey(x25519PublicKey: ephemeralPublicKey, x25519PrivateKey: Data(userKeyPair.secretKey))
|
||||
guard let tokenAsData = try? AESGCM.decrypt(ciphertext, with: symmetricKey) else { throw Error.decryptionFailed }
|
||||
return tokenAsData.toHexString()
|
||||
}
|
||||
|
|
|
@ -17,10 +17,11 @@ public struct OpenGroupMessageV2 {
|
|||
public func sign() -> OpenGroupMessageV2? {
|
||||
guard
|
||||
let userKeyPair = Identity.fetchUserKeyPair(),
|
||||
let legacyKeyPair: ECKeyPair = try? ECKeyPair(publicKeyData: Data(userKeyPair.publicKey), privateKeyData: Data(userKeyPair.secretKey)),
|
||||
let data: Data = Data(base64Encoded: base64EncodedData)
|
||||
else { return nil }
|
||||
|
||||
guard let signature = try? Ed25519.sign(data, with: userKeyPair) else {
|
||||
guard let signature = try? Ed25519.sign(data, with: legacyKeyPair) else {
|
||||
SNLog("Failed to sign open group message.")
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import GRDB
|
||||
import SessionUtilitiesKit
|
||||
|
||||
@objc(SNDataExtractionNotificationInfoMessage)
|
||||
final class DataExtractionNotificationInfoMessage : TSInfoMessage {
|
||||
|
@ -10,20 +15,24 @@ final class DataExtractionNotificationInfoMessage : TSInfoMessage {
|
|||
super.init(coder: coder)
|
||||
}
|
||||
|
||||
required init(dictionary dictionaryValue: [String:Any]!) throws {
|
||||
required init(dictionary dictionaryValue: [String: Any]!) throws {
|
||||
try super.init(dictionary: dictionaryValue)
|
||||
}
|
||||
|
||||
override func previewText(with transaction: YapDatabaseReadTransaction) -> String {
|
||||
guard let thread = thread as? TSContactThread else { return "" } // Should never occur
|
||||
let sessionID = thread.contactSessionID()
|
||||
let displayName = Storage.shared.getContact(with: sessionID)?.displayName(for: .regular) ?? sessionID
|
||||
|
||||
let displayName = Profile.displayName(for: thread.contactSessionID())
|
||||
|
||||
switch messageType {
|
||||
case .screenshotNotification: return String(format: NSLocalizedString("screenshot_taken", comment: ""), displayName)
|
||||
case .mediaSavedNotification:
|
||||
// TODO: Use referencedAttachmentTimestamp to tell the user * which * media was saved
|
||||
return String(format: NSLocalizedString("meida_saved", comment: ""), displayName)
|
||||
default: preconditionFailure()
|
||||
case .screenshotNotification:
|
||||
return String(format: NSLocalizedString("screenshot_taken", comment: ""), displayName)
|
||||
|
||||
case .mediaSavedNotification:
|
||||
// TODO: Use referencedAttachmentTimestamp to tell the user * which * media was saved
|
||||
return String(format: NSLocalizedString("meida_saved", comment: ""), displayName)
|
||||
|
||||
default: preconditionFailure()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,9 +3,10 @@
|
|||
//
|
||||
|
||||
#import "OWSDisappearingConfigurationUpdateInfoMessage.h"
|
||||
#import "OWSDisappearingMessagesConfiguration.h"
|
||||
#import <SessionMessagingKit/SessionMessagingKit-Swift.h>
|
||||
#import <SignalCoreKit/NSString+OWS.h>
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface OWSDisappearingConfigurationUpdateInfoMessage ()
|
||||
|
|
|
@ -1,35 +1,35 @@
|
|||
////
|
||||
//// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
////
|
||||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//#import <SessionUtilitiesKit/TSYapDatabaseObject.h>
|
||||
//#import <SignalCoreKit/SignalCoreKit.h>
|
||||
//
|
||||
|
||||
#import <SessionUtilitiesKit/TSYapDatabaseObject.h>
|
||||
#import <SignalCoreKit/SignalCoreKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
#define OWSDisappearingMessagesConfigurationDefaultExpirationDuration kDayInterval
|
||||
|
||||
@class YapDatabaseReadTransaction;
|
||||
|
||||
@interface OWSDisappearingMessagesConfiguration : TSYapDatabaseObject
|
||||
|
||||
- (instancetype)initDefaultWithThreadId:(NSString *)threadId;
|
||||
|
||||
- (instancetype)initWithThreadId:(NSString *)threadId enabled:(BOOL)isEnabled durationSeconds:(uint32_t)seconds;
|
||||
|
||||
@property (nonatomic, getter=isEnabled) BOOL enabled;
|
||||
@property (nonatomic) uint32_t durationSeconds;
|
||||
@property (nonatomic, readonly) NSUInteger durationIndex;
|
||||
@property (nonatomic, readonly) NSString *durationString;
|
||||
@property (nonatomic, readonly) BOOL dictionaryValueDidChange;
|
||||
@property (readonly, getter=isNewRecord) BOOL newRecord;
|
||||
|
||||
+ (instancetype)fetchOrBuildDefaultWithThreadId:(NSString *)threadId
|
||||
transaction:(YapDatabaseReadTransaction *)transaction;
|
||||
|
||||
+ (NSArray<NSNumber *> *)validDurationsSeconds;
|
||||
+ (uint32_t)maxDurationSeconds;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
//NS_ASSUME_NONNULL_BEGIN
|
||||
//
|
||||
//#define OWSDisappearingMessagesConfigurationDefaultExpirationDuration kDayInterval
|
||||
//
|
||||
//@class YapDatabaseReadTransaction;
|
||||
//
|
||||
//@interface OWSDisappearingMessagesConfiguration : TSYapDatabaseObject
|
||||
//
|
||||
//- (instancetype)initDefaultWithThreadId:(NSString *)threadId;
|
||||
//
|
||||
//- (instancetype)initWithThreadId:(NSString *)threadId enabled:(BOOL)isEnabled durationSeconds:(uint32_t)seconds;
|
||||
//
|
||||
//@property (nonatomic, getter=isEnabled) BOOL enabled;
|
||||
//@property (nonatomic) uint32_t durationSeconds;
|
||||
//@property (nonatomic, readonly) NSUInteger durationIndex;
|
||||
//@property (nonatomic, readonly) NSString *durationString;
|
||||
//@property (nonatomic, readonly) BOOL dictionaryValueDidChange;
|
||||
//@property (readonly, getter=isNewRecord) BOOL newRecord;
|
||||
//
|
||||
//+ (instancetype)fetchOrBuildDefaultWithThreadId:(NSString *)threadId
|
||||
// transaction:(YapDatabaseReadTransaction *)transaction;
|
||||
//
|
||||
//+ (NSArray<NSNumber *> *)validDurationsSeconds;
|
||||
//+ (uint32_t)maxDurationSeconds;
|
||||
//
|
||||
//@end
|
||||
//
|
||||
//NS_ASSUME_NONNULL_END
|
||||
|
|
|
@ -1,130 +1,130 @@
|
|||
////
|
||||
//// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
////
|
||||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//#import "OWSDisappearingMessagesConfiguration.h"
|
||||
//#import <SignalCoreKit/NSDate+OWS.h>
|
||||
//#import <SignalCoreKit/NSString+OWS.h>
|
||||
//
|
||||
|
||||
#import "OWSDisappearingMessagesConfiguration.h"
|
||||
#import <SignalCoreKit/NSDate+OWS.h>
|
||||
#import <SignalCoreKit/NSString+OWS.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface OWSDisappearingMessagesConfiguration ()
|
||||
|
||||
// Transient record lifecycle attributes.
|
||||
@property (atomic) NSDictionary *originalDictionaryValue;
|
||||
@property (atomic, getter=isNewRecord) BOOL newRecord;
|
||||
|
||||
@end
|
||||
|
||||
@implementation OWSDisappearingMessagesConfiguration
|
||||
|
||||
- (instancetype)initDefaultWithThreadId:(NSString *)threadId
|
||||
{
|
||||
return [self initWithThreadId:threadId
|
||||
enabled:NO
|
||||
durationSeconds:(NSTimeInterval)OWSDisappearingMessagesConfigurationDefaultExpirationDuration];
|
||||
}
|
||||
|
||||
- (nullable instancetype)initWithCoder:(NSCoder *)coder
|
||||
{
|
||||
self = [super initWithCoder:coder];
|
||||
|
||||
_originalDictionaryValue = [self dictionaryValue];
|
||||
_newRecord = NO;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithThreadId:(NSString *)threadId enabled:(BOOL)isEnabled durationSeconds:(uint32_t)seconds
|
||||
{
|
||||
self = [super initWithUniqueId:threadId];
|
||||
if (!self) {
|
||||
return self;
|
||||
}
|
||||
|
||||
_enabled = isEnabled;
|
||||
_durationSeconds = seconds;
|
||||
_newRecord = YES;
|
||||
_originalDictionaryValue = self.dictionaryValue;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (instancetype)fetchOrBuildDefaultWithThreadId:(NSString *)threadId
|
||||
transaction:(YapDatabaseReadTransaction *)transaction
|
||||
{
|
||||
OWSDisappearingMessagesConfiguration *savedConfiguration =
|
||||
[self fetchObjectWithUniqueID:threadId transaction:transaction];
|
||||
if (savedConfiguration) {
|
||||
return savedConfiguration;
|
||||
} else {
|
||||
return [[self alloc] initDefaultWithThreadId:threadId];
|
||||
}
|
||||
}
|
||||
|
||||
+ (NSArray<NSNumber *> *)validDurationsSeconds
|
||||
{
|
||||
return @[
|
||||
@(5 * kSecondInterval),
|
||||
@(10 * kSecondInterval),
|
||||
@(30 * kSecondInterval),
|
||||
@(1 * kMinuteInterval),
|
||||
@(5 * kMinuteInterval),
|
||||
@(30 * kMinuteInterval),
|
||||
@(1 * kHourInterval),
|
||||
@(6 * kHourInterval),
|
||||
@(12 * kHourInterval),
|
||||
@(24 * kHourInterval),
|
||||
@(1 * kWeekInterval)
|
||||
];
|
||||
}
|
||||
|
||||
+ (uint32_t)maxDurationSeconds
|
||||
{
|
||||
static uint32_t max;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
max = [[self.validDurationsSeconds valueForKeyPath:@"@max.intValue"] unsignedIntValue];
|
||||
});
|
||||
|
||||
return max;
|
||||
}
|
||||
|
||||
- (NSUInteger)durationIndex
|
||||
{
|
||||
return [[self.class validDurationsSeconds] indexOfObject:@(self.durationSeconds)];
|
||||
}
|
||||
|
||||
- (NSString *)durationString
|
||||
{
|
||||
return [NSString formatDurationSeconds:self.durationSeconds useShortFormat:NO];
|
||||
}
|
||||
|
||||
#pragma mark - Dirty Tracking
|
||||
|
||||
+ (MTLPropertyStorage)storageBehaviorForPropertyWithKey:(NSString *)propertyKey
|
||||
{
|
||||
// Don't persist transient properties
|
||||
if ([propertyKey isEqualToString:@"originalDictionaryValue"]
|
||||
||[propertyKey isEqualToString:@"newRecord"]) {
|
||||
return MTLPropertyStorageNone;
|
||||
} else {
|
||||
return [super storageBehaviorForPropertyWithKey:propertyKey];
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)dictionaryValueDidChange
|
||||
{
|
||||
return ![self.originalDictionaryValue isEqual:[self dictionaryValue]];
|
||||
}
|
||||
|
||||
- (void)saveWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
|
||||
{
|
||||
[super saveWithTransaction:transaction];
|
||||
self.originalDictionaryValue = [self dictionaryValue];
|
||||
self.newRecord = NO;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
//NS_ASSUME_NONNULL_BEGIN
|
||||
//
|
||||
//@interface OWSDisappearingMessagesConfiguration ()
|
||||
//
|
||||
//// Transient record lifecycle attributes.
|
||||
//@property (atomic) NSDictionary *originalDictionaryValue;
|
||||
//@property (atomic, getter=isNewRecord) BOOL newRecord;
|
||||
//
|
||||
//@end
|
||||
//
|
||||
//@implementation OWSDisappearingMessagesConfiguration
|
||||
//
|
||||
//- (instancetype)initDefaultWithThreadId:(NSString *)threadId
|
||||
//{
|
||||
// return [self initWithThreadId:threadId
|
||||
// enabled:NO
|
||||
// durationSeconds:(NSTimeInterval)OWSDisappearingMessagesConfigurationDefaultExpirationDuration];
|
||||
//}
|
||||
//
|
||||
//- (nullable instancetype)initWithCoder:(NSCoder *)coder
|
||||
//{
|
||||
// self = [super initWithCoder:coder];
|
||||
//
|
||||
// _originalDictionaryValue = [self dictionaryValue];
|
||||
// _newRecord = NO;
|
||||
//
|
||||
// return self;
|
||||
//}
|
||||
//
|
||||
//- (instancetype)initWithThreadId:(NSString *)threadId enabled:(BOOL)isEnabled durationSeconds:(uint32_t)seconds
|
||||
//{
|
||||
// self = [super initWithUniqueId:threadId];
|
||||
// if (!self) {
|
||||
// return self;
|
||||
// }
|
||||
//
|
||||
// _enabled = isEnabled;
|
||||
// _durationSeconds = seconds;
|
||||
// _newRecord = YES;
|
||||
// _originalDictionaryValue = self.dictionaryValue;
|
||||
//
|
||||
// return self;
|
||||
//}
|
||||
//
|
||||
//+ (instancetype)fetchOrBuildDefaultWithThreadId:(NSString *)threadId
|
||||
// transaction:(YapDatabaseReadTransaction *)transaction
|
||||
//{
|
||||
// OWSDisappearingMessagesConfiguration *savedConfiguration =
|
||||
// [self fetchObjectWithUniqueID:threadId transaction:transaction];
|
||||
// if (savedConfiguration) {
|
||||
// return savedConfiguration;
|
||||
// } else {
|
||||
// return [[self alloc] initDefaultWithThreadId:threadId];
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//+ (NSArray<NSNumber *> *)validDurationsSeconds
|
||||
//{
|
||||
// return @[
|
||||
// @(5 * kSecondInterval),
|
||||
// @(10 * kSecondInterval),
|
||||
// @(30 * kSecondInterval),
|
||||
// @(1 * kMinuteInterval),
|
||||
// @(5 * kMinuteInterval),
|
||||
// @(30 * kMinuteInterval),
|
||||
// @(1 * kHourInterval),
|
||||
// @(6 * kHourInterval),
|
||||
// @(12 * kHourInterval),
|
||||
// @(24 * kHourInterval),
|
||||
// @(1 * kWeekInterval)
|
||||
// ];
|
||||
//}
|
||||
//
|
||||
//+ (uint32_t)maxDurationSeconds
|
||||
//{
|
||||
// static uint32_t max;
|
||||
// static dispatch_once_t onceToken;
|
||||
// dispatch_once(&onceToken, ^{
|
||||
// max = [[self.validDurationsSeconds valueForKeyPath:@"@max.intValue"] unsignedIntValue];
|
||||
// });
|
||||
//
|
||||
// return max;
|
||||
//}
|
||||
//
|
||||
//- (NSUInteger)durationIndex
|
||||
//{
|
||||
// return [[self.class validDurationsSeconds] indexOfObject:@(self.durationSeconds)];
|
||||
//}
|
||||
//
|
||||
//- (NSString *)durationString
|
||||
//{
|
||||
// return [NSString formatDurationSeconds:self.durationSeconds useShortFormat:NO];
|
||||
//}
|
||||
//
|
||||
//#pragma mark - Dirty Tracking
|
||||
//
|
||||
//+ (MTLPropertyStorage)storageBehaviorForPropertyWithKey:(NSString *)propertyKey
|
||||
//{
|
||||
// // Don't persist transient properties
|
||||
// if ([propertyKey isEqualToString:@"originalDictionaryValue"]
|
||||
// ||[propertyKey isEqualToString:@"newRecord"]) {
|
||||
// return MTLPropertyStorageNone;
|
||||
// } else {
|
||||
// return [super storageBehaviorForPropertyWithKey:propertyKey];
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//- (BOOL)dictionaryValueDidChange
|
||||
//{
|
||||
// return ![self.originalDictionaryValue isEqual:[self dictionaryValue]];
|
||||
//}
|
||||
//
|
||||
//- (void)saveWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
|
||||
//{
|
||||
// [super saveWithTransaction:transaction];
|
||||
// self.originalDictionaryValue = [self dictionaryValue];
|
||||
// self.newRecord = NO;
|
||||
//}
|
||||
//
|
||||
//@end
|
||||
//
|
||||
//NS_ASSUME_NONNULL_END
|
||||
|
|
|
@ -188,8 +188,7 @@ void AssertIsOnDisappearingMessagesQueue()
|
|||
|
||||
NSString *_Nullable remoteContactName = nil;
|
||||
if (remoteRecipientId) {
|
||||
SNContactContext context = [SNContact contextForThread:thread];
|
||||
remoteContactName = [[LKStorage.shared getContactWithSessionID:remoteRecipientId] displayNameFor:context] ?: remoteRecipientId;
|
||||
remoteContactName = [SMKProfile displayNameWithId:remoteRecipientId thread:thread];
|
||||
}
|
||||
|
||||
// Become eventually consistent in the case that the remote changed their settings at the same time.
|
||||
|
@ -198,9 +197,9 @@ void AssertIsOnDisappearingMessagesQueue()
|
|||
[thread disappearingMessagesConfigurationWithTransaction:transaction];
|
||||
|
||||
if (duration == 0) {
|
||||
disappearingMessagesConfiguration.enabled = NO;
|
||||
disappearingMessagesConfiguration.isEnabled = NO;
|
||||
} else {
|
||||
disappearingMessagesConfiguration.enabled = YES;
|
||||
disappearingMessagesConfiguration.isEnabled = YES;
|
||||
disappearingMessagesConfiguration.durationSeconds = duration;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import AFNetworking
|
||||
import Foundation
|
||||
import PromiseKit
|
||||
import SignalCoreKit
|
||||
|
||||
@objc
|
||||
public enum LinkPreviewError: Int, Error {
|
||||
|
|
|
@ -33,9 +33,9 @@ public final class MentionsManager : NSObject {
|
|||
let openGroupV2 = Storage.shared.getV2OpenGroup(for: threadID)
|
||||
storage.dbReadConnection.read { transaction in
|
||||
candidates = cache.compactMap { publicKey in
|
||||
let context: Contact.Context = (openGroupV2 != nil) ? .openGroup : .regular
|
||||
let displayNameOrNil = Storage.shared.getContact(with: publicKey)?.displayName(for: context)
|
||||
guard let displayName = displayNameOrNil else { return nil }
|
||||
guard let displayName: String = Profile.displayNameNoFallback(for: publicKey, context: (openGroupV2 != nil ? .openGroup : .regular)) else {
|
||||
return nil
|
||||
}
|
||||
guard !displayName.hasPrefix("Anonymous") else { return nil }
|
||||
return Mention(publicKey: publicKey, displayName: displayName)
|
||||
}
|
||||
|
|
|
@ -8,9 +8,9 @@ import SessionUtilitiesKit
|
|||
|
||||
extension MessageReceiver {
|
||||
|
||||
internal static func decryptWithSessionProtocol(ciphertext: Data, using x25519KeyPair: ECKeyPair) throws -> (plaintext: Data, senderX25519PublicKey: String) {
|
||||
let recipientX25519PrivateKey = x25519KeyPair.privateKey
|
||||
let recipientX25519PublicKey = Data(hex: x25519KeyPair.hexEncodedPublicKey.removing05PrefixIfNeeded())
|
||||
internal static func decryptWithSessionProtocol(ciphertext: Data, using x25519KeyPair: Box.KeyPair) throws -> (plaintext: Data, senderX25519PublicKey: String) {
|
||||
let recipientX25519PrivateKey = x25519KeyPair.secretKey
|
||||
let recipientX25519PublicKey = x25519KeyPair.publicKey
|
||||
let sodium = Sodium()
|
||||
let signatureSize = sodium.sign.Bytes
|
||||
let ed25519PublicKeySize = sodium.sign.PublicKeyBytes
|
||||
|
|
|
@ -1,23 +1,25 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import GRDB
|
||||
import Sodium
|
||||
import Curve25519Kit
|
||||
import SignalCoreKit
|
||||
import SessionSnodeKit
|
||||
|
||||
extension MessageReceiver {
|
||||
|
||||
public static func handle(_ message: Message, associatedWithProto proto: SNProtoContent, openGroupID: String?, isBackgroundPoll: Bool, using transaction: Any) throws {
|
||||
public static func handle(_ db: Database, _ message: Message, associatedWithProto proto: SNProtoContent, openGroupID: String?, isBackgroundPoll: Bool, using transaction: Any) throws {
|
||||
switch message {
|
||||
case let message as ReadReceipt: handleReadReceipt(message, using: transaction)
|
||||
case let message as TypingIndicator: handleTypingIndicator(message, using: transaction)
|
||||
case let message as ClosedGroupControlMessage: handleClosedGroupControlMessage(message, using: transaction)
|
||||
case let message as ClosedGroupControlMessage: handleClosedGroupControlMessage(db, message, using: transaction)
|
||||
case let message as DataExtractionNotification: handleDataExtractionNotification(message, using: transaction)
|
||||
case let message as ExpirationTimerUpdate: handleExpirationTimerUpdate(message, using: transaction)
|
||||
case let message as ConfigurationMessage: handleConfigurationMessage(message, using: transaction)
|
||||
case let message as ConfigurationMessage: handleConfigurationMessage(db, message, using: transaction)
|
||||
case let message as UnsendRequest: handleUnsendRequest(message, using: transaction)
|
||||
case let message as MessageRequestResponse: handleMessageRequestResponse(message, using: transaction)
|
||||
case let message as VisibleMessage: try handleVisibleMessage(message, associatedWithProto: proto, openGroupID: openGroupID, isBackgroundPoll: isBackgroundPoll, using: transaction)
|
||||
case let message as MessageRequestResponse: handleMessageRequestResponse(db, message, using: transaction)
|
||||
case let message as VisibleMessage: try handleVisibleMessage(db, message, associatedWithProto: proto, openGroupID: openGroupID, isBackgroundPoll: isBackgroundPoll, using: transaction)
|
||||
default: fatalError()
|
||||
}
|
||||
|
||||
|
@ -145,11 +147,11 @@ extension MessageReceiver {
|
|||
threadOrNil = TSContactThread.getWithContactSessionID(syncTarget ?? senderPublicKey, transaction: transaction)
|
||||
}
|
||||
guard let thread = threadOrNil else { return }
|
||||
let configuration = OWSDisappearingMessagesConfiguration(threadId: thread.uniqueId!, enabled: true, durationSeconds: duration)
|
||||
let configuration = Legacy.DisappearingMessagesConfiguration(threadId: thread.uniqueId!, enabled: true, durationSeconds: duration)
|
||||
configuration.save(with: transaction)
|
||||
var senderDisplayName: String? = nil
|
||||
if senderPublicKey != getUserHexEncodedPublicKey() {
|
||||
senderDisplayName = Storage.shared.getContact(with: senderPublicKey)?.displayName(for: .regular) ?? senderPublicKey
|
||||
senderDisplayName = Profile.displayName(for: senderPublicKey)
|
||||
}
|
||||
let message = OWSDisappearingConfigurationUpdateInfoMessage(timestamp: messageSentTimestamp, thread: thread,
|
||||
configuration: configuration, createdByRemoteName: senderDisplayName, createdInExistingGroup: false)
|
||||
|
@ -168,11 +170,11 @@ extension MessageReceiver {
|
|||
threadOrNil = TSContactThread.getWithContactSessionID(syncTarget ?? senderPublicKey, transaction: transaction)
|
||||
}
|
||||
guard let thread = threadOrNil else { return }
|
||||
let configuration = OWSDisappearingMessagesConfiguration(threadId: thread.uniqueId!, enabled: false, durationSeconds: 24 * 60 * 60)
|
||||
let configuration = Legacy.DisappearingMessagesConfiguration(threadId: thread.uniqueId!, enabled: false, durationSeconds: 24 * 60 * 60)
|
||||
configuration.save(with: transaction)
|
||||
var senderDisplayName: String? = nil
|
||||
if senderPublicKey != getUserHexEncodedPublicKey() {
|
||||
senderDisplayName = Storage.shared.getContact(with: senderPublicKey)?.displayName(for: .regular) ?? senderPublicKey
|
||||
senderDisplayName = Profile.displayName(for: senderPublicKey)
|
||||
}
|
||||
let message = OWSDisappearingConfigurationUpdateInfoMessage(timestamp: messageSentTimestamp, thread: thread,
|
||||
configuration: configuration, createdByRemoteName: senderDisplayName, createdInExistingGroup: false)
|
||||
|
@ -184,7 +186,7 @@ extension MessageReceiver {
|
|||
|
||||
// MARK: - Configuration Messages
|
||||
|
||||
private static func handleConfigurationMessage(_ message: ConfigurationMessage, using transaction: Any) {
|
||||
private static func handleConfigurationMessage(_ db: Database, _ message: ConfigurationMessage, using transaction: Any) {
|
||||
let userPublicKey = getUserHexEncodedPublicKey()
|
||||
guard message.sender == userPublicKey else { return }
|
||||
SNLog("Configuration message received.")
|
||||
|
@ -195,10 +197,14 @@ extension MessageReceiver {
|
|||
let lastConfigTimestamp: TimeInterval = (UserDefaults.standard[.lastConfigurationSync]?.timeIntervalSince1970 ?? Date(timeIntervalSince1970: 0).timeIntervalSince1970)
|
||||
|
||||
// Profile
|
||||
var userProfileKey: OWSAES256Key? = nil
|
||||
if let profileKey = message.profileKey { userProfileKey = OWSAES256Key(data: profileKey) }
|
||||
updateProfileIfNeeded(publicKey: userPublicKey, name: message.displayName, profilePictureURL: message.profilePictureURL,
|
||||
profileKey: userProfileKey, sentTimestamp: message.sentTimestamp!, transaction: transaction)
|
||||
updateProfileIfNeeded(
|
||||
publicKey: userPublicKey,
|
||||
name: message.displayName,
|
||||
profilePictureURL: message.profilePictureURL,
|
||||
profileKey: OWSAES256Key(data: message.profileKey),
|
||||
sentTimestamp: message.sentTimestamp!,
|
||||
transaction: transaction
|
||||
)
|
||||
|
||||
if isInitialSync || messageSentTimestamp > lastConfigTimestamp {
|
||||
if isInitialSync {
|
||||
|
@ -211,11 +217,18 @@ extension MessageReceiver {
|
|||
// Contacts
|
||||
for contactInfo in message.contacts {
|
||||
let sessionID = contactInfo.publicKey!
|
||||
let contact = (Storage.shared.getContact(with: sessionID, using: transaction) ?? Contact(sessionID: sessionID))
|
||||
let contactWasBlocked: Bool = contact.isBlocked
|
||||
if let profileKey = contactInfo.profileKey { contact.profileEncryptionKey = OWSAES256Key(data: profileKey) }
|
||||
contact.profilePictureURL = contactInfo.profilePictureURL
|
||||
contact.name = contactInfo.displayName
|
||||
let contact: Contact = Contact.fetchOrCreate(db, id: sessionID)
|
||||
let profile: Profile = Profile.fetchOrCreate(db, id: sessionID)
|
||||
|
||||
try? profile
|
||||
.with(
|
||||
name: contactInfo.displayName,
|
||||
profilePictureUrl: .updateIf(contactInfo.profilePictureURL),
|
||||
profileEncryptionKey: .updateIf(
|
||||
contactInfo.profileKey.map { OWSAES256Key(data: $0) }
|
||||
)
|
||||
)
|
||||
.save(db)
|
||||
|
||||
// Note: We only update these values if the proto actually has values for them (this is to
|
||||
// prevent an edge case where an old client could override the values with default values
|
||||
|
@ -225,12 +238,22 @@ extension MessageReceiver {
|
|||
// config message setting *isApproved* and *didApproveMe* to true. This may prevent some
|
||||
// weird edge cases where a config message swapping *isApproved* and *didApproveMe* to
|
||||
// false.
|
||||
if contactInfo.hasIsApproved && contactInfo.isApproved { contact.isApproved = true }
|
||||
if contactInfo.hasDidApproveMe && contactInfo.didApproveMe { contact.didApproveMe = true }
|
||||
|
||||
if contactInfo.hasIsBlocked { contact.isBlocked = contactInfo.isBlocked }
|
||||
|
||||
Storage.shared.setContact(contact, using: transaction)
|
||||
try? contact
|
||||
.with(
|
||||
isApproved: (contactInfo.hasIsApproved && contactInfo.isApproved ?
|
||||
.existing :
|
||||
true
|
||||
),
|
||||
isBlocked: (contactInfo.hasIsBlocked && contactInfo.isBlocked ?
|
||||
.existing :
|
||||
true
|
||||
),
|
||||
didApproveMe: (contactInfo.hasDidApproveMe && contactInfo.didApproveMe ?
|
||||
.existing :
|
||||
true
|
||||
)
|
||||
)
|
||||
.save(db)
|
||||
|
||||
// If the contact is blocked
|
||||
if contactInfo.hasIsBlocked && contactInfo.isBlocked {
|
||||
|
@ -238,7 +261,7 @@ extension MessageReceiver {
|
|||
// associated with them that is a message request thread then delete it (assume
|
||||
// that the current user had deleted that message request)
|
||||
if
|
||||
contactInfo.isBlocked != contactWasBlocked,
|
||||
contactInfo.isBlocked != contact.isBlocked,
|
||||
let thread: TSContactThread = TSContactThread.getWithContactSessionID(sessionID, transaction: transaction),
|
||||
thread.isMessageRequest(using: transaction)
|
||||
{
|
||||
|
@ -258,7 +281,11 @@ extension MessageReceiver {
|
|||
let allClosedGroupPublicKeys = storage.getUserClosedGroupPublicKeys()
|
||||
for closedGroup in message.closedGroups {
|
||||
guard !allClosedGroupPublicKeys.contains(closedGroup.publicKey) else { continue }
|
||||
handleNewClosedGroup(groupPublicKey: closedGroup.publicKey, name: closedGroup.name, encryptionKeyPair: closedGroup.encryptionKeyPair,
|
||||
let keyPair: Box.KeyPair = Box.KeyPair(
|
||||
publicKey: closedGroup.encryptionKeyPair.publicKey.bytes,
|
||||
secretKey: closedGroup.encryptionKeyPair.privateKey.bytes
|
||||
)
|
||||
handleNewClosedGroup(db, groupPublicKey: closedGroup.publicKey, name: closedGroup.name, encryptionKeyPair: keyPair,
|
||||
members: [String](closedGroup.members), admins: [String](closedGroup.admins), expirationTimer: closedGroup.expirationTimer,
|
||||
messageSentTimestamp: message.sentTimestamp!, using: transaction)
|
||||
}
|
||||
|
@ -313,7 +340,8 @@ extension MessageReceiver {
|
|||
// MARK: - Visible Messages
|
||||
|
||||
@discardableResult
|
||||
public static func handleVisibleMessage(_ message: VisibleMessage, associatedWithProto proto: SNProtoContent, openGroupID: String?, isBackgroundPoll: Bool, using transaction: Any) throws -> String {
|
||||
public static func handleVisibleMessage(_ db: Database, _ message: VisibleMessage, associatedWithProto proto: SNProtoContent, openGroupID: String?, isBackgroundPoll: Bool, using transaction: Any) throws -> String {
|
||||
let sender: String = message.sender!
|
||||
let storage = SNMessagingKitConfiguration.shared.storage
|
||||
let transaction = transaction as! YapDatabaseReadWriteTransaction
|
||||
var isMainAppAndActive = false
|
||||
|
@ -337,7 +365,7 @@ extension MessageReceiver {
|
|||
profileKey: contactProfileKey, sentTimestamp: message.sentTimestamp!, transaction: transaction)
|
||||
}
|
||||
// Get or create thread
|
||||
guard let threadID = storage.getOrCreateThread(for: message.syncTarget ?? message.sender!, groupPublicKey: message.groupPublicKey, openGroupID: openGroupID, using: transaction) else { throw Error.noThread }
|
||||
guard let threadID = storage.getOrCreateThread(for: message.syncTarget ?? sender, groupPublicKey: message.groupPublicKey, openGroupID: openGroupID, using: transaction) else { throw Error.noThread }
|
||||
// Parse quote if needed
|
||||
var tsQuotedMessage: TSQuotedMessage? = nil
|
||||
if message.quote != nil && proto.dataMessage?.quote != nil, let thread = TSThread.fetch(uniqueId: threadID, transaction: transaction) {
|
||||
|
@ -359,7 +387,9 @@ extension MessageReceiver {
|
|||
groupPublicKey: message.groupPublicKey, openGroupID: openGroupID, using: transaction) else { throw Error.duplicateMessage }
|
||||
message.threadID = threadID
|
||||
// Start attachment downloads if needed
|
||||
let isContactTrusted = Storage.shared.getContact(with: message.sender!)?.isTrusted ?? false
|
||||
// TODO: Swap this back
|
||||
let isContactTrusted: Bool = (GRDBStorage.shared.read({ db in try Contact.fetchOne(db, id: sender) })?.isTrusted ?? false)
|
||||
// let isContactTrusted: Bool = ((try? Contact.fetchOne(db, id: sender))?.isTrusted ?? false)
|
||||
let isGroup = message.groupPublicKey != nil || openGroupID != nil
|
||||
attachmentsToDownload.forEach { attachmentID in
|
||||
let downloadJob = AttachmentDownloadJob(attachmentID: attachmentID, tsMessageID: tsMessageID, threadID: threadID)
|
||||
|
@ -403,10 +433,10 @@ extension MessageReceiver {
|
|||
// by using the approval process
|
||||
if !isGroup, let senderSessionId: String = message.sender {
|
||||
updateContactApprovalStatusIfNeeded(
|
||||
db,
|
||||
senderSessionId: senderSessionId,
|
||||
threadId: message.threadID,
|
||||
forceConfigSync: false,
|
||||
using: transaction
|
||||
forceConfigSync: false
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -427,9 +457,10 @@ extension MessageReceiver {
|
|||
profileKey: OWSAES256Key?, sentTimestamp: UInt64, transaction: YapDatabaseReadWriteTransaction) {
|
||||
let isCurrentUser = (publicKey == getUserHexEncodedPublicKey())
|
||||
let userDefaults = UserDefaults.standard
|
||||
let contact = Storage.shared.getContact(with: publicKey) ?? Contact(sessionID: publicKey) // New API
|
||||
var profile: Profile = Profile.fetchOrCreate(id: publicKey)
|
||||
|
||||
// Name
|
||||
if let name = name, name != contact.name {
|
||||
if let name = name, name != profile.name {
|
||||
let shouldUpdate: Bool
|
||||
if isCurrentUser {
|
||||
shouldUpdate = given(userDefaults[.lastDisplayNameUpdate]) { sentTimestamp > UInt64($0.timeIntervalSince1970 * 1000) } ?? true
|
||||
|
@ -440,40 +471,54 @@ extension MessageReceiver {
|
|||
if isCurrentUser {
|
||||
userDefaults[.lastDisplayNameUpdate] = Date(timeIntervalSince1970: TimeInterval(sentTimestamp / 1000))
|
||||
}
|
||||
contact.name = name
|
||||
|
||||
profile = profile.with(name: name)
|
||||
}
|
||||
}
|
||||
|
||||
// Profile picture & profile key
|
||||
if let profileKey = profileKey, let profilePictureURL = profilePictureURL,
|
||||
profileKey.keyData.count == kAES256_KeyByteLength, profileKey != contact.profileEncryptionKey {
|
||||
if
|
||||
let profileKey = profileKey,
|
||||
let profilePictureURL = profilePictureURL,
|
||||
profileKey.keyData.count == kAES256_KeyByteLength,
|
||||
profileKey != profile.profileEncryptionKey
|
||||
{
|
||||
let shouldUpdate: Bool
|
||||
if isCurrentUser {
|
||||
shouldUpdate = given(userDefaults[.lastProfilePictureUpdate]) { sentTimestamp > UInt64($0.timeIntervalSince1970 * 1000) } ?? true
|
||||
} else {
|
||||
shouldUpdate = true
|
||||
}
|
||||
|
||||
if shouldUpdate {
|
||||
if isCurrentUser {
|
||||
userDefaults[.lastProfilePictureUpdate] = Date(timeIntervalSince1970: TimeInterval(sentTimestamp / 1000))
|
||||
}
|
||||
contact.profilePictureURL = profilePictureURL
|
||||
contact.profileEncryptionKey = profileKey
|
||||
|
||||
profile = profile.with(
|
||||
profilePictureUrl: .update(profilePictureURL),
|
||||
profileEncryptionKey: .update(profileKey)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Persist changes
|
||||
Storage.shared.setContact(contact, using: transaction)
|
||||
GRDBStorage.shared.write { db in
|
||||
try profile.save(db)
|
||||
}
|
||||
|
||||
// Download the profile picture if needed
|
||||
transaction.addCompletionQueue(DispatchQueue.main) {
|
||||
SSKEnvironment.shared.profileManager.downloadAvatar(forUserProfile: contact)
|
||||
ProfileManager.downloadAvatar(for: profile)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// MARK: - Closed Groups
|
||||
public static func handleClosedGroupControlMessage(_ message: ClosedGroupControlMessage, using transaction: Any) {
|
||||
public static func handleClosedGroupControlMessage(_ db: Database, _ message: ClosedGroupControlMessage, using transaction: Any) {
|
||||
switch message.kind! {
|
||||
case .new: handleNewClosedGroup(message, using: transaction)
|
||||
case .new: handleNewClosedGroup(db, message, using: transaction)
|
||||
case .encryptionKeyPair: handleClosedGroupEncryptionKeyPair(message, using: transaction)
|
||||
case .nameChange: handleClosedGroupNameChanged(message, using: transaction)
|
||||
case .membersAdded: handleClosedGroupMembersAdded(message, using: transaction)
|
||||
|
@ -483,16 +528,16 @@ extension MessageReceiver {
|
|||
}
|
||||
}
|
||||
|
||||
private static func handleNewClosedGroup(_ message: ClosedGroupControlMessage, using transaction: Any) {
|
||||
private static func handleNewClosedGroup(_ db: Database, _ message: ClosedGroupControlMessage, using transaction: Any) {
|
||||
guard case let .new(publicKeyAsData, name, encryptionKeyPair, membersAsData, adminsAsData, expirationTimer) = message.kind else { return }
|
||||
let groupPublicKey = publicKeyAsData.toHexString()
|
||||
let members = membersAsData.map { $0.toHexString() }
|
||||
let admins = adminsAsData.map { $0.toHexString() }
|
||||
handleNewClosedGroup(groupPublicKey: groupPublicKey, name: name, encryptionKeyPair: encryptionKeyPair,
|
||||
handleNewClosedGroup(db, groupPublicKey: groupPublicKey, name: name, encryptionKeyPair: encryptionKeyPair,
|
||||
members: members, admins: admins, expirationTimer: expirationTimer, messageSentTimestamp: message.sentTimestamp!, using: transaction)
|
||||
}
|
||||
|
||||
private static func handleNewClosedGroup(groupPublicKey: String, name: String, encryptionKeyPair: ECKeyPair, members: [String], admins: [String], expirationTimer: UInt32, messageSentTimestamp: UInt64, using transaction: Any) {
|
||||
private static func handleNewClosedGroup(_ db: Database, groupPublicKey: String, name: String, encryptionKeyPair: Box.KeyPair, members: [String], admins: [String], expirationTimer: UInt32, messageSentTimestamp: UInt64, using transaction: Any) {
|
||||
let transaction = transaction as! YapDatabaseReadWriteTransaction
|
||||
|
||||
// With new closed groups we only want to create them if the admin creating the closed group is an
|
||||
|
@ -501,7 +546,7 @@ extension MessageReceiver {
|
|||
var hasApprovedAdmin: Bool = false
|
||||
|
||||
for adminId in admins {
|
||||
if let contact: Contact = Storage.shared.getContact(with: adminId), contact.isApproved {
|
||||
if let contact: Contact = try? Contact.fetchOne(db, id: adminId), contact.isApproved {
|
||||
hasApprovedAdmin = true
|
||||
break
|
||||
}
|
||||
|
@ -533,7 +578,7 @@ extension MessageReceiver {
|
|||
|
||||
let isExpirationTimerEnabled = (expirationTimer > 0)
|
||||
let expirationTimerDuration = (isExpirationTimerEnabled ? expirationTimer : 24 * 60 * 60)
|
||||
let configuration = OWSDisappearingMessagesConfiguration(
|
||||
let configuration = Legacy.DisappearingMessagesConfiguration(
|
||||
threadId: thread.uniqueId!,
|
||||
enabled: isExpirationTimerEnabled,
|
||||
durationSeconds: expirationTimerDuration
|
||||
|
@ -560,7 +605,7 @@ extension MessageReceiver {
|
|||
let groupPublicKey = explicitGroupPublicKey?.toHexString() ?? message.groupPublicKey else { return }
|
||||
let transaction = transaction as! YapDatabaseReadWriteTransaction
|
||||
let userPublicKey = getUserHexEncodedPublicKey()
|
||||
guard let userKeyPair = Identity.fetchUserKeyPair() else {
|
||||
guard let userKeyPair: Box.KeyPair = Identity.fetchUserKeyPair() else {
|
||||
return SNLog("Couldn't find user X25519 key pair.")
|
||||
}
|
||||
let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey)
|
||||
|
@ -586,12 +631,10 @@ extension MessageReceiver {
|
|||
} catch {
|
||||
return SNLog("Couldn't parse closed group encryption key pair.")
|
||||
}
|
||||
let keyPair: ECKeyPair
|
||||
do {
|
||||
keyPair = try ECKeyPair(publicKeyData: proto.publicKey.removing05PrefixIfNeeded(), privateKeyData: proto.privateKey)
|
||||
} catch {
|
||||
return SNLog("Couldn't parse closed group encryption key pair.")
|
||||
}
|
||||
let keyPair: Box.KeyPair = Box.KeyPair(
|
||||
publicKey: proto.publicKey.removing05PrefixIfNeeded().bytes,
|
||||
secretKey: proto.privateKey.bytes
|
||||
)
|
||||
// Store it if needed
|
||||
let closedGroupEncryptionKeyPairs = Storage.shared.getClosedGroupEncryptionKeyPairs(for: groupPublicKey)
|
||||
guard !closedGroupEncryptionKeyPairs.contains(keyPair) else {
|
||||
|
@ -711,6 +754,8 @@ extension MessageReceiver {
|
|||
/// • Unsubscribe from PNs, delete the group public key, etc. as the group will be disbanded.
|
||||
private static func handleClosedGroupMemberLeft(_ message: ClosedGroupControlMessage, using transaction: Any) {
|
||||
guard case .memberLeft = message.kind else { return }
|
||||
|
||||
let sender: String = message.sender!
|
||||
let transaction = transaction as! YapDatabaseReadWriteTransaction
|
||||
guard let groupPublicKey = message.groupPublicKey else { return }
|
||||
performIfValid(for: message, using: transaction) { groupID, thread, group in
|
||||
|
@ -731,13 +776,16 @@ extension MessageReceiver {
|
|||
thread.setGroupModel(newGroupModel, with: transaction)
|
||||
// Notify the user if needed
|
||||
guard members != Set(group.groupMemberIds) else { return }
|
||||
let contact = Storage.shared.getContact(with: message.sender!)
|
||||
|
||||
let updateInfo: String
|
||||
if let displayName = contact?.displayName(for: Contact.Context.regular) {
|
||||
|
||||
if let displayName = Profile.displayNameNoFallback(for: sender) {
|
||||
updateInfo = String(format: NSLocalizedString("GROUP_MEMBER_LEFT", comment: ""), displayName)
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
updateInfo = NSLocalizedString("GROUP_UPDATED", comment: "")
|
||||
}
|
||||
|
||||
let infoMessage = TSInfoMessage(timestamp: message.sentTimestamp!, in: thread, messageType: .groupUpdated, customMessage: updateInfo)
|
||||
infoMessage.save(with: transaction)
|
||||
}
|
||||
|
@ -787,14 +835,12 @@ extension MessageReceiver {
|
|||
// MARK: - Message Requests
|
||||
|
||||
private static func updateContactApprovalStatusIfNeeded(
|
||||
_ db: Database,
|
||||
senderSessionId: String,
|
||||
threadId: String?,
|
||||
forceConfigSync: Bool,
|
||||
using transaction: Any
|
||||
forceConfigSync: Bool
|
||||
) {
|
||||
guard let transaction: YapDatabaseReadWriteTransaction = transaction as? YapDatabaseReadWriteTransaction else { return }
|
||||
|
||||
let userPublicKey: String = getUserHexEncodedPublicKey()
|
||||
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
||||
|
||||
// If the sender of the message was the current user
|
||||
if senderSessionId == userPublicKey {
|
||||
|
@ -824,8 +870,8 @@ extension MessageReceiver {
|
|||
MessageSender.syncConfiguration(forceSyncNow: true).retainUntilComplete()
|
||||
}
|
||||
|
||||
public static func handleMessageRequestResponse(_ message: MessageRequestResponse, using transaction: Any) {
|
||||
let userPublicKey = getUserHexEncodedPublicKey()
|
||||
public static func handleMessageRequestResponse(_ db: Database, _ message: MessageRequestResponse, using transaction: Any) {
|
||||
let userPublicKey = getUserHexEncodedPublicKey(db)
|
||||
|
||||
// Ignore messages which were sent from the current user
|
||||
guard message.sender != userPublicKey else { return }
|
||||
|
@ -842,10 +888,10 @@ extension MessageReceiver {
|
|||
}
|
||||
|
||||
updateContactApprovalStatusIfNeeded(
|
||||
db,
|
||||
senderSessionId: senderId,
|
||||
threadId: nil,
|
||||
forceConfigSync: true,
|
||||
using: transaction
|
||||
forceConfigSync: true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import GRDB
|
||||
import Sodium
|
||||
import SessionUtilitiesKit
|
||||
|
||||
public enum MessageReceiver {
|
||||
|
@ -51,7 +53,7 @@ public enum MessageReceiver {
|
|||
}
|
||||
}
|
||||
|
||||
public static func parse(_ data: Data, openGroupMessageServerID: UInt64?, isRetry: Bool = false, using transaction: Any) throws -> (Message, SNProtoContent) {
|
||||
public static func parse(_ db: Database, _ data: Data, openGroupMessageServerID: UInt64?, isRetry: Bool = false, using transaction: Any) throws -> (Message, SNProtoContent) {
|
||||
let userPublicKey = getUserHexEncodedPublicKey()
|
||||
let isOpenGroupMessage = (openGroupMessageServerID != nil)
|
||||
// Parse the envelope
|
||||
|
@ -67,7 +69,7 @@ public enum MessageReceiver {
|
|||
} else {
|
||||
switch envelope.type {
|
||||
case .sessionMessage:
|
||||
guard let userX25519KeyPair = Identity.fetchUserKeyPair() else { throw Error.noUserX25519KeyPair }
|
||||
guard let userX25519KeyPair: Box.KeyPair = Identity.fetchUserKeyPair() else { throw Error.noUserX25519KeyPair }
|
||||
(plaintext, sender) = try decryptWithSessionProtocol(ciphertext: ciphertext, using: userX25519KeyPair)
|
||||
case .closedGroupMessage:
|
||||
guard let hexEncodedGroupPublicKey = envelope.source, SNMessagingKitConfiguration.shared.storage.isClosedGroup(hexEncodedGroupPublicKey) else { throw Error.invalidGroupPublicKey }
|
||||
|
@ -111,7 +113,8 @@ public enum MessageReceiver {
|
|||
}
|
||||
|
||||
// Don't process the envelope any further if the sender is blocked
|
||||
guard Storage.shared.getContact(with: sender, using: transaction)?.isBlocked != true else {
|
||||
guard GRDBStorage.shared.read({ db in try Contact.fetchOne(db, id: sender) })?.isBlocked != true else {
|
||||
// guard (try? Contact.fetchOne(db, id: sender))?.isBlocked != true else {
|
||||
throw Error.senderBlocked
|
||||
}
|
||||
|
||||
|
@ -125,15 +128,15 @@ public enum MessageReceiver {
|
|||
}
|
||||
// Parse the message
|
||||
let message: Message? = {
|
||||
if let readReceipt = ReadReceipt.fromProto(proto) { return readReceipt }
|
||||
if let typingIndicator = TypingIndicator.fromProto(proto) { return typingIndicator }
|
||||
if let closedGroupControlMessage = ClosedGroupControlMessage.fromProto(proto) { return closedGroupControlMessage }
|
||||
if let dataExtractionNotification = DataExtractionNotification.fromProto(proto) { return dataExtractionNotification }
|
||||
if let expirationTimerUpdate = ExpirationTimerUpdate.fromProto(proto) { return expirationTimerUpdate }
|
||||
if let configurationMessage = ConfigurationMessage.fromProto(proto) { return configurationMessage }
|
||||
if let unsendRequest = UnsendRequest.fromProto(proto) { return unsendRequest }
|
||||
if let messageRequestResponse = MessageRequestResponse.fromProto(proto) { return messageRequestResponse }
|
||||
if let visibleMessage = VisibleMessage.fromProto(proto) { return visibleMessage }
|
||||
if let readReceipt = ReadReceipt.fromProto(proto, sender: sender) { return readReceipt }
|
||||
if let typingIndicator = TypingIndicator.fromProto(proto, sender: sender) { return typingIndicator }
|
||||
if let closedGroupControlMessage = ClosedGroupControlMessage.fromProto(proto, sender: sender) { return closedGroupControlMessage }
|
||||
if let dataExtractionNotification = DataExtractionNotification.fromProto(proto, sender: sender) { return dataExtractionNotification }
|
||||
if let expirationTimerUpdate = ExpirationTimerUpdate.fromProto(proto, sender: sender) { return expirationTimerUpdate }
|
||||
if let configurationMessage = ConfigurationMessage.fromProto(proto, sender: sender) { return configurationMessage }
|
||||
if let unsendRequest = UnsendRequest.fromProto(proto, sender: sender) { return unsendRequest }
|
||||
if let messageRequestResponse = MessageRequestResponse.fromProto(proto, sender: sender) { return messageRequestResponse }
|
||||
if let visibleMessage = VisibleMessage.fromProto(proto, sender: sender) { return visibleMessage }
|
||||
return nil
|
||||
}()
|
||||
if let message = message {
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import Sodium
|
||||
import Curve25519Kit
|
||||
import PromiseKit
|
||||
|
||||
extension MessageSender {
|
||||
public static var distributingClosedGroupEncryptionKeyPairs: [String: [ECKeyPair]] = [:]
|
||||
public static var distributingClosedGroupEncryptionKeyPairs: [String: [Box.KeyPair]] = [:]
|
||||
|
||||
public static func createClosedGroup(name: String, members: Set<String>, transaction: YapDatabaseReadWriteTransaction) -> Promise<TSGroupThread> {
|
||||
// Prepare
|
||||
|
@ -30,8 +31,12 @@ extension MessageSender {
|
|||
for member in members {
|
||||
let thread = TSContactThread.getOrCreateThread(withContactSessionID: member, transaction: transaction)
|
||||
thread.save(with: transaction)
|
||||
let keyPair: Box.KeyPair = Box.KeyPair(
|
||||
publicKey: encryptionKeyPair.publicKey.bytes,
|
||||
secretKey: encryptionKeyPair.privateKey.bytes
|
||||
)
|
||||
let closedGroupControlMessageKind = ClosedGroupControlMessage.Kind.new(publicKey: Data(hex: groupPublicKey), name: name,
|
||||
encryptionKeyPair: encryptionKeyPair, members: membersAsData, admins: adminsAsData, expirationTimer: 0)
|
||||
encryptionKeyPair: keyPair, members: membersAsData, admins: adminsAsData, expirationTimer: 0)
|
||||
let closedGroupControlMessage = ClosedGroupControlMessage(kind: closedGroupControlMessageKind)
|
||||
// Sending this non-durably is okay because we show a loader to the user. If they close the app while the
|
||||
// loader is still showing, it's within expectation that the group creation might be incomplete.
|
||||
|
@ -41,7 +46,11 @@ extension MessageSender {
|
|||
// Add the group to the user's set of public keys to poll for
|
||||
Storage.shared.addClosedGroupPublicKey(groupPublicKey, using: transaction)
|
||||
// Store the key pair
|
||||
Storage.shared.addClosedGroupEncryptionKeyPair(encryptionKeyPair, for: groupPublicKey, using: transaction)
|
||||
let keyPair: Box.KeyPair = Box.KeyPair(
|
||||
publicKey: encryptionKeyPair.publicKey.bytes,
|
||||
secretKey: encryptionKeyPair.privateKey.bytes
|
||||
)
|
||||
Storage.shared.addClosedGroupEncryptionKeyPair(keyPair, for: groupPublicKey, using: transaction)
|
||||
// Notify the PN server
|
||||
promises.append(PushNotificationAPI.performOperation(.subscribe, for: groupPublicKey, publicKey: userPublicKey))
|
||||
// Notify the user
|
||||
|
@ -72,10 +81,14 @@ extension MessageSender {
|
|||
return Promise(error: Error.invalidClosedGroupUpdate)
|
||||
}
|
||||
// Generate the new encryption key pair
|
||||
let newKeyPair = Curve25519.generateKeyPair()
|
||||
let newLegacyKeyPair = Curve25519.generateKeyPair()
|
||||
let newKeyPair: Box.KeyPair = Box.KeyPair(
|
||||
publicKey: newLegacyKeyPair.publicKey.bytes,
|
||||
secretKey: newLegacyKeyPair.privateKey.bytes
|
||||
)
|
||||
// Distribute it
|
||||
let proto = try! SNProtoKeyPair.builder(publicKey: newKeyPair.publicKey,
|
||||
privateKey: newKeyPair.privateKey).build()
|
||||
let proto = try! SNProtoKeyPair.builder(publicKey: Data(newKeyPair.publicKey),
|
||||
privateKey: Data(newKeyPair.secretKey)).build()
|
||||
let plaintext = try! proto.serializedData()
|
||||
let wrappers = targetMembers.compactMap { publicKey -> ClosedGroupControlMessage.KeyPairWrapper in
|
||||
let ciphertext = try! MessageSender.encryptWithSessionProtocol(plaintext, for: publicKey)
|
||||
|
@ -329,8 +342,8 @@ extension MessageSender {
|
|||
guard let encryptionKeyPair = distributingClosedGroupEncryptionKeyPairs[groupPublicKey]?.last
|
||||
?? Storage.shared.getLatestClosedGroupEncryptionKeyPair(for: groupPublicKey) else { return }
|
||||
// Send it
|
||||
guard let proto = try? SNProtoKeyPair.builder(publicKey: encryptionKeyPair.publicKey,
|
||||
privateKey: encryptionKeyPair.privateKey).build(), let plaintext = try? proto.serializedData() else { return }
|
||||
guard let proto = try? SNProtoKeyPair.builder(publicKey: Data(encryptionKeyPair.publicKey),
|
||||
privateKey: Data(encryptionKeyPair.secretKey)).build(), let plaintext = try? proto.serializedData() else { return }
|
||||
let contactThread = TSContactThread.getOrCreateThread(withContactSessionID: publicKey, transaction: transaction)
|
||||
guard let ciphertext = try? MessageSender.encryptWithSessionProtocol(plaintext, for: publicKey) else { return }
|
||||
SNLog("Sending latest encryption key pair to: \(publicKey).")
|
||||
|
|
|
@ -171,7 +171,7 @@ public final class MessageSender : NSObject {
|
|||
case .contact(let publicKey): ciphertext = try encryptWithSessionProtocol(plaintext, for: publicKey)
|
||||
case .closedGroup(let groupPublicKey):
|
||||
guard let encryptionKeyPair = Storage.shared.getLatestClosedGroupEncryptionKeyPair(for: groupPublicKey) else { throw Error.noKeyPair }
|
||||
ciphertext = try encryptWithSessionProtocol(plaintext, for: encryptionKeyPair.hexEncodedPublicKey)
|
||||
ciphertext = try encryptWithSessionProtocol(plaintext, for: "05\(encryptionKeyPair.publicKey.toHexString())")
|
||||
case .openGroup(_, _), .openGroupV2(_, _): preconditionFailure()
|
||||
}
|
||||
} catch {
|
||||
|
|
|
@ -65,20 +65,22 @@ public final class OpenGroupPollerV2 : NSObject {
|
|||
// Sorting the messages by server ID before importing them fixes an issue where messages that quote older messages can't find those older messages
|
||||
let openGroupID = "\(server).\(body.room)"
|
||||
let messages = body.messages.sorted { $0.serverID! < $1.serverID! } // Safe because messages with a nil serverID are filtered out
|
||||
storage.write { transaction in
|
||||
messages.forEach { message in
|
||||
guard let data = Data(base64Encoded: message.base64EncodedData) else {
|
||||
return SNLog("Ignoring open group message with invalid encoding.")
|
||||
}
|
||||
let envelope = SNProtoEnvelope.builder(type: .sessionMessage, timestamp: message.sentTimestamp)
|
||||
envelope.setContent(data)
|
||||
envelope.setSource(message.sender!) // Safe because messages with a nil sender are filtered out
|
||||
do {
|
||||
let data = try envelope.buildSerializedData()
|
||||
let (message, proto) = try MessageReceiver.parse(data, openGroupMessageServerID: UInt64(message.serverID!), isRetry: false, using: transaction)
|
||||
try MessageReceiver.handle(message, associatedWithProto: proto, openGroupID: openGroupID, isBackgroundPoll: isBackgroundPoll, using: transaction)
|
||||
} catch {
|
||||
SNLog("Couldn't receive open group message due to error: \(error).")
|
||||
GRDBStorage.shared.write { db in
|
||||
storage.write { transaction in
|
||||
messages.forEach { message in
|
||||
guard let data = Data(base64Encoded: message.base64EncodedData) else {
|
||||
return SNLog("Ignoring open group message with invalid encoding.")
|
||||
}
|
||||
let envelope = SNProtoEnvelope.builder(type: .sessionMessage, timestamp: message.sentTimestamp)
|
||||
envelope.setContent(data)
|
||||
envelope.setSource(message.sender!) // Safe because messages with a nil sender are filtered out
|
||||
do {
|
||||
let data = try envelope.buildSerializedData()
|
||||
let (message, proto) = try MessageReceiver.parse(db, data, openGroupMessageServerID: UInt64(message.serverID!), isRetry: false, using: transaction)
|
||||
try MessageReceiver.handle(db, message, associatedWithProto: proto, openGroupID: openGroupID, isBackgroundPoll: isBackgroundPoll, using: transaction)
|
||||
} catch {
|
||||
SNLog("Couldn't receive open group message due to error: \(error).")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,13 +12,6 @@ public protocol SessionMessagingKitStorageProtocol {
|
|||
func write(with block: @escaping (Any) -> Void, completion: @escaping () -> Void) -> Promise<Void>
|
||||
func writeSync(with block: @escaping (Any) -> Void)
|
||||
|
||||
// MARK: - General
|
||||
|
||||
func getUser() -> Contact?
|
||||
func getUser(using transaction: YapDatabaseReadTransaction?) -> Contact?
|
||||
func getAllContacts() -> Set<Contact>
|
||||
func getAllContacts(with transaction: YapDatabaseReadTransaction) -> Set<Contact>
|
||||
|
||||
// MARK: - Closed Groups
|
||||
|
||||
func getUserClosedGroupPublicKeys() -> Set<String>
|
||||
|
@ -95,4 +88,4 @@ public protocol SessionMessagingKitStorageProtocol {
|
|||
func persist(_ stream: TSAttachmentStream, associatedWith tsIncomingMessageID: String, using transaction: Any)
|
||||
}
|
||||
|
||||
extension Storage: SessionMessagingKitStorageProtocol, SessionSnodeKitStorageProtocol {}
|
||||
extension Storage: SessionMessagingKitStorageProtocol {}
|
||||
|
|
|
@ -59,7 +59,7 @@ NSString *const TSContactThreadPrefix = @"c";
|
|||
|
||||
- (BOOL)isMessageRequest {
|
||||
NSString *sessionID = self.contactSessionID;
|
||||
SNContact *contact = [LKStorage.shared getContactWithSessionID:sessionID];
|
||||
SMKContact *contact = [SMKContact fetchOrCreateWithId: sessionID];
|
||||
|
||||
return (
|
||||
self.shouldBeVisible &&
|
||||
|
@ -72,7 +72,7 @@ NSString *const TSContactThreadPrefix = @"c";
|
|||
|
||||
- (BOOL)isMessageRequestUsingTransaction:(YapDatabaseReadTransaction *)transaction {
|
||||
NSString *sessionID = self.contactSessionID;
|
||||
SNContact *contact = [LKStorage.shared getContactWithSessionID:sessionID using:transaction];
|
||||
SMKContact *contact = [SMKContact fetchOrCreateWithId: sessionID];
|
||||
|
||||
return (
|
||||
self.shouldBeVisible &&
|
||||
|
@ -85,14 +85,14 @@ NSString *const TSContactThreadPrefix = @"c";
|
|||
|
||||
- (BOOL)isBlocked {
|
||||
NSString *sessionID = self.contactSessionID;
|
||||
SNContact *contact = [LKStorage.shared getContactWithSessionID:sessionID];
|
||||
SMKContact *contact = [SMKContact fetchOrCreateWithId: sessionID];
|
||||
|
||||
return (contact.isBlocked == YES);
|
||||
}
|
||||
|
||||
- (BOOL)isBlockedUsingTransaction:(YapDatabaseReadTransaction *)transaction {
|
||||
NSString *sessionID = self.contactSessionID;
|
||||
SNContact *contact = [LKStorage.shared getContactWithSessionID:sessionID using:transaction];
|
||||
SMKContact *contact = [SMKContact fetchOrCreateWithId: sessionID];
|
||||
|
||||
return (contact.isBlocked == YES);
|
||||
}
|
||||
|
@ -105,15 +105,13 @@ NSString *const TSContactThreadPrefix = @"c";
|
|||
- (NSString *)name
|
||||
{
|
||||
NSString *sessionID = self.contactSessionID;
|
||||
SNContact *contact = [LKStorage.shared getContactWithSessionID:sessionID];
|
||||
return [contact displayNameFor:SNContactContextRegular] ?: sessionID;
|
||||
return [SMKProfile displayNameWithId:sessionID];
|
||||
}
|
||||
|
||||
- (NSString *)nameWithTransaction:(YapDatabaseReadTransaction *)transaction
|
||||
{
|
||||
NSString *sessionID = self.contactSessionID;
|
||||
SNContact *contact = [LKStorage.shared getContactWithSessionID:sessionID using:transaction];
|
||||
return [contact displayNameFor:SNContactContextRegular] ?: sessionID;
|
||||
return [SMKProfile displayNameWithId:sessionID];
|
||||
}
|
||||
|
||||
+ (NSString *)threadIDFromContactSessionID:(NSString *)contactSessionID {
|
||||
|
|
|
@ -123,8 +123,7 @@ const int32_t kGroupIdLength = 16;
|
|||
|
||||
if (removedMembersMinusSelf.count > 0) {
|
||||
NSArray *removedMemberNames = [removedMembers.allObjects map:^NSString *(NSString *publicKey) {
|
||||
SNContact *contact = [LKStorage.shared getContactWithSessionID:publicKey];
|
||||
return [contact displayNameFor:SNContactContextRegular] ?: publicKey;
|
||||
return [SMKProfile displayNameWithId:publicKey];
|
||||
}];
|
||||
NSString *format = removedMembers.count > 1 ? NSLocalizedString(@"GROUP_MEMBERS_REMOVED", @"") : NSLocalizedString(@"GROUP_MEMBER_REMOVED", @"");
|
||||
updatedGroupInfoString = [updatedGroupInfoString
|
||||
|
@ -135,8 +134,7 @@ const int32_t kGroupIdLength = 16;
|
|||
|
||||
if (addedMembers.count > 0) {
|
||||
NSArray *addedMemberNames = [[addedMembers allObjects] map:^NSString*(NSString* publicKey) {
|
||||
SNContact *contact = [LKStorage.shared getContactWithSessionID:publicKey];
|
||||
return [contact displayNameFor:SNContactContextRegular] ?: publicKey;
|
||||
return [SMKProfile displayNameWithId:publicKey];
|
||||
}];
|
||||
updatedGroupInfoString = [updatedGroupInfoString
|
||||
stringByAppendingString:[NSString
|
||||
|
|
|
@ -22,6 +22,7 @@ BOOL IsNoteToSelfEnabled(void);
|
|||
@property (nonatomic, readonly, nullable) NSDate *lastInteractionDate;
|
||||
@property (nonatomic, readonly) TSInteraction *lastInteraction;
|
||||
@property (atomic, readonly) BOOL isMuted;
|
||||
@property (nonatomic, copy, nullable) NSString *messageDraft;
|
||||
@property (atomic, readonly, nullable) NSDate *mutedUntilDate;
|
||||
|
||||
/**
|
||||
|
|
|
@ -23,7 +23,6 @@ BOOL IsNoteToSelfEnabled(void)
|
|||
@property (nonatomic) NSDate *creationDate;
|
||||
@property (nonatomic, nullable) NSDate *lastInteractionDate;
|
||||
@property (nonatomic, nullable) NSNumber *archivedAsOfMessageSortId;
|
||||
@property (nonatomic, copy, nullable) NSString *messageDraft;
|
||||
@property (atomic, nullable) NSDate *mutedUntilDate;
|
||||
|
||||
@end
|
||||
|
|
|
@ -6,9 +6,9 @@
|
|||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
extern NSString *const kNSNotificationName_LocalProfileDidChange;
|
||||
extern NSString *const kNSNotificationName_OtherUsersProfileDidChange;
|
||||
extern NSString *const kNSNotificationKey_ProfileRecipientId;
|
||||
//extern NSString *const kNSNotificationName_LocalProfileDidChange;
|
||||
//extern NSString *const kNSNotificationName_OtherUsersProfileDidChange;
|
||||
//extern NSString *const kNSNotificationKey_ProfileRecipientId;
|
||||
|
||||
@interface OWSUserProfile : TSYapDatabaseObject
|
||||
|
||||
|
@ -18,7 +18,7 @@ extern NSString *const kNSNotificationKey_ProfileRecipientId;
|
|||
+ (NSString *)sharedDataProfileAvatarsDirPath;
|
||||
+ (NSString *)profileAvatarsDirPath;
|
||||
+ (void)resetProfileStorage;
|
||||
+ (NSSet<NSString *> *)allProfileAvatarFilePaths;
|
||||
//+ (NSSet<NSString *> *)allProfileAvatarFilePaths;
|
||||
|
||||
@end
|
||||
|
||||
|
|
|
@ -18,10 +18,6 @@
|
|||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
NSString *const kNSNotificationName_LocalProfileDidChange = @"kNSNotificationName_LocalProfileDidChange";
|
||||
NSString *const kNSNotificationName_OtherUsersProfileDidChange = @"kNSNotificationName_OtherUsersProfileDidChange";
|
||||
NSString *const kNSNotificationKey_ProfileRecipientId = @"kNSNotificationKey_ProfileRecipientId";
|
||||
|
||||
@interface OWSUserProfile ()
|
||||
|
||||
@end
|
||||
|
@ -69,21 +65,21 @@ NSString *const kNSNotificationKey_ProfileRecipientId = @"kNSNotificationKey_Pro
|
|||
[[NSFileManager defaultManager] removeItemAtPath:[self profileAvatarsDirPath] error:&error];
|
||||
}
|
||||
|
||||
+ (NSSet<NSString *> *)allProfileAvatarFilePaths
|
||||
{
|
||||
NSString *profileAvatarsDirPath = self.profileAvatarsDirPath;
|
||||
NSMutableSet<NSString *> *profileAvatarFilePaths = [NSMutableSet new];
|
||||
|
||||
NSSet<SNContact *> *allContacts = [LKStorage.shared getAllContacts];
|
||||
|
||||
for (SNContact *contact in allContacts) {
|
||||
if (contact.profilePictureFileName == nil) { continue; }
|
||||
NSString *filePath = [profileAvatarsDirPath stringByAppendingPathComponent:contact.profilePictureFileName];
|
||||
[profileAvatarFilePaths addObject:filePath];
|
||||
}
|
||||
|
||||
return [profileAvatarFilePaths copy];
|
||||
}
|
||||
//+ (NSSet<NSString *> *)allProfileAvatarFilePaths
|
||||
//{
|
||||
// NSString *profileAvatarsDirPath = self.profileAvatarsDirPath;
|
||||
// NSMutableSet<NSString *> *profileAvatarFilePaths = [NSMutableSet new];
|
||||
//
|
||||
// NSSet<SNContact *> *allContacts = [LKStorage.shared getAllContacts];
|
||||
//
|
||||
// for (SNContact *contact in allContacts) {
|
||||
// if (contact.profilePictureFileName == nil) { continue; }
|
||||
// NSString *filePath = [profileAvatarsDirPath stringByAppendingPathComponent:contact.profilePictureFileName];
|
||||
// [profileAvatarFilePaths addObject:filePath];
|
||||
// }
|
||||
//
|
||||
// return [profileAvatarFilePaths copy];
|
||||
//}
|
||||
|
||||
@end
|
||||
|
||||
|
|
|
@ -1,30 +1,30 @@
|
|||
////
|
||||
//// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
////
|
||||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//@class OWSAES256Key;
|
||||
//@class TSThread;
|
||||
//@class YapDatabaseReadWriteTransaction;
|
||||
//@class SNContact;
|
||||
//
|
||||
|
||||
@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
|
||||
//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
|
||||
|
|
12
SessionMessagingKit/Utilities/BoxKeyPair+Utilities.swift
Normal file
12
SessionMessagingKit/Utilities/BoxKeyPair+Utilities.swift
Normal file
|
@ -0,0 +1,12 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import Sodium
|
||||
extension Box.KeyPair: Equatable {
|
||||
public static func == (lhs: Box.KeyPair, rhs: Box.KeyPair) -> Bool {
|
||||
return (
|
||||
lhs.publicKey == rhs.publicKey &&
|
||||
lhs.secretKey == rhs.secretKey
|
||||
)
|
||||
}
|
||||
}
|
|
@ -178,12 +178,16 @@ public class FullTextSearchFinder: NSObject {
|
|||
}
|
||||
|
||||
private static let recipientIndexer: SearchIndexer<String> = SearchIndexer { (recipientId: String, transaction: YapDatabaseReadTransaction) in
|
||||
var result = "\(recipientId)"
|
||||
if let contact = Storage.shared.getContact(with: recipientId) {
|
||||
if let name = contact.name { result += " \(name)" }
|
||||
if let nickname = contact.nickname { result += " \(nickname)" }
|
||||
}
|
||||
return result
|
||||
let profile: Profile? = GRDBStorage.shared.read { db in try Profile.fetchOne(db, id: recipientId) }
|
||||
|
||||
return [
|
||||
recipientId,
|
||||
profile?.name,
|
||||
profile?.nickname
|
||||
]
|
||||
.compactMap { $0 }
|
||||
.filter { !$0.isEmpty }
|
||||
.joined(separator: " ")
|
||||
}
|
||||
|
||||
private static let messageIndexer: SearchIndexer<TSMessage> = SearchIndexer { (message: TSMessage, transaction: YapDatabaseReadTransaction) in
|
||||
|
|
12
SessionMessagingKit/Utilities/OWSAES256Key+Utilities.swift
Normal file
12
SessionMessagingKit/Utilities/OWSAES256Key+Utilities.swift
Normal file
|
@ -0,0 +1,12 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import SignalCoreKit
|
||||
|
||||
public extension OWSAES256Key {
|
||||
convenience init?(data: Data?) {
|
||||
guard let existingData: Data = data else { return nil }
|
||||
|
||||
self.init(data: existingData)
|
||||
}
|
||||
}
|
|
@ -1,9 +1,8 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
import SignalCoreKit
|
||||
|
||||
@objc(OWSAudioActivity)
|
||||
public class AudioActivity: NSObject {
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import SignalCoreKit
|
||||
|
||||
extension OWSSound {
|
||||
|
||||
public func notificationSound(isQuiet: Bool) -> UNNotificationSound {
|
||||
|
|
351
SessionMessagingKit/Utilities/ProfileManager.swift
Normal file
351
SessionMessagingKit/Utilities/ProfileManager.swift
Normal file
|
@ -0,0 +1,351 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import GRDB
|
||||
import PromiseKit
|
||||
import SignalCoreKit
|
||||
import SessionUtilitiesKit
|
||||
|
||||
public struct ProfileManager {
|
||||
public enum Error: LocalizedError {
|
||||
case avatarImageTooLarge
|
||||
case avatarWriteFailed
|
||||
case avatarEncryptionFailed
|
||||
case avatarUploadFailed
|
||||
case avatarUploadMaxFileSizeExceeded
|
||||
|
||||
var localizedDescription: String {
|
||||
switch self {
|
||||
case .avatarImageTooLarge: return "Avatar image too large."
|
||||
case .avatarWriteFailed: return "Avatar write failed."
|
||||
case .avatarEncryptionFailed: return "Avatar encryption failed."
|
||||
case .avatarUploadFailed: return "Avatar upload failed."
|
||||
case .avatarUploadMaxFileSizeExceeded: return "Maximum file size exceeded."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The max bytes for a user's profile name, encoded in UTF8.
|
||||
// Before encrypting and submitting we NULL pad the name data to this length.
|
||||
private static let nameDataLength: UInt = 26
|
||||
public static let maxAvatarDiameter: CGFloat = 640
|
||||
|
||||
private static var profileAvatarCache: Atomic<[String: UIImage]> = Atomic([:])
|
||||
private static var currentAvatarDownloads: Atomic<Set<String>> = Atomic([])
|
||||
|
||||
// MARK: - Functions
|
||||
|
||||
public static func isToLong(profileName: String) -> Bool {
|
||||
return ((profileName.data(using: .utf8)?.count ?? 0) > nameDataLength)
|
||||
}
|
||||
|
||||
public static func profileAvatar(for id: String) -> UIImage? {
|
||||
guard let profile: Profile = GRDBStorage.shared.read({ db in try Profile.fetchOne(db, id: id) }) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if let profileFileName: String = profile.profilePictureFileName, !profileFileName.isEmpty {
|
||||
return loadProfileAvatar(for: profileFileName)
|
||||
}
|
||||
|
||||
if let profilePictureUrl: String = profile.profilePictureUrl, !profilePictureUrl.isEmpty {
|
||||
downloadAvatar(for: profile)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
private static func loadProfileAvatar(for fileName: String) -> UIImage? {
|
||||
if let cachedImage: UIImage = profileAvatarCache.wrappedValue[fileName] {
|
||||
return cachedImage
|
||||
}
|
||||
|
||||
guard
|
||||
!fileName.isEmpty,
|
||||
let data: Data = loadProfileData(with: fileName),
|
||||
data.isValidImage,
|
||||
let image: UIImage = UIImage(data: data)
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
profileAvatarCache.mutate { $0[fileName] = image }
|
||||
return image
|
||||
}
|
||||
|
||||
private static func loadProfileData(with fileName: String) -> Data? {
|
||||
let filePath: String = OWSUserProfile.profileAvatarFilepath(withFilename: fileName)
|
||||
|
||||
return try? Data(contentsOf: URL(fileURLWithPath: filePath))
|
||||
}
|
||||
|
||||
// MARK: - Profile Encryption
|
||||
|
||||
private static func encryptProfileData(data: Data, key: OWSAES256Key) -> Data? {
|
||||
guard key.keyData.count == kAES256_KeyByteLength else { return nil }
|
||||
|
||||
return Cryptography.encryptAESGCMProfileData(plainTextData: data, key: key)
|
||||
}
|
||||
|
||||
private static func decryptProfileData(data: Data, key: OWSAES256Key) -> Data? {
|
||||
guard key.keyData.count == kAES256_KeyByteLength else { return nil }
|
||||
|
||||
return Cryptography.decryptAESGCMProfileData(encryptedData: data, key: key)
|
||||
}
|
||||
|
||||
// MARK: - Other Users' Profiles
|
||||
|
||||
public static func downloadAvatar(for profile: Profile, funcName: String = #function) {
|
||||
guard !currentAvatarDownloads.wrappedValue.contains(profile.id) else {
|
||||
// Download already in flight; ignore
|
||||
return
|
||||
}
|
||||
guard
|
||||
let profileUrlStringAtStart: String = profile.profilePictureUrl,
|
||||
let profileUrlAtStart: URL = URL(string: profileUrlStringAtStart)
|
||||
else {
|
||||
SNLog("Skipping downloading avatar for \(profile.id) because url is not set")
|
||||
return
|
||||
}
|
||||
guard
|
||||
let fileId: UInt64 = UInt64(profileUrlAtStart.lastPathComponent),
|
||||
let profileKeyAtStart: OWSAES256Key = profile.profileEncryptionKey,
|
||||
profileKeyAtStart.keyData.count > 0
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
let fileName: String = UUID().uuidString.appendingFileExtension("jpg")
|
||||
let filePath: String = OWSUserProfile.profileAvatarFilepath(withFilename: fileName)
|
||||
var backgroundTask: OWSBackgroundTask? = OWSBackgroundTask(label: funcName)
|
||||
|
||||
DispatchQueue.global(qos: .default).async {
|
||||
OWSLogger.verbose("downloading profile avatar: \(profile.id)")
|
||||
currentAvatarDownloads.mutate { $0.insert(profile.id) }
|
||||
|
||||
let useOldServer: Bool = (profileUrlStringAtStart.contains(FileServerAPIV2.oldServer))
|
||||
|
||||
FileServerAPIV2
|
||||
.download(fileId, useOldServer: useOldServer)
|
||||
.done { data in
|
||||
currentAvatarDownloads.mutate { $0.remove(profile.id) }
|
||||
|
||||
GRDBStorage.shared.write { db in
|
||||
guard let latestProfile: Profile = try Profile.fetchOne(db, id: profile.id) else {
|
||||
return
|
||||
}
|
||||
|
||||
guard
|
||||
let latestProfileKey: OWSAES256Key = latestProfile.profileEncryptionKey,
|
||||
!latestProfileKey.keyData.isEmpty,
|
||||
latestProfileKey == profileKeyAtStart
|
||||
else {
|
||||
OWSLogger.warn("Ignoring avatar download for obsolete user profile.")
|
||||
return
|
||||
}
|
||||
|
||||
guard profileUrlStringAtStart == latestProfile.profilePictureUrl else {
|
||||
OWSLogger.warn("Avatar url has changed during download.")
|
||||
|
||||
if latestProfile.profilePictureUrl?.isEmpty == false {
|
||||
self.downloadAvatar(for: latestProfile)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
guard let decryptedData: Data = decryptProfileData(data: data, key: profileKeyAtStart) else {
|
||||
OWSLogger.warn("Avatar data for \(profile.id) could not be decrypted.")
|
||||
return
|
||||
}
|
||||
|
||||
try? decryptedData.write(to: URL(fileURLWithPath: filePath), options: [.atomic])
|
||||
|
||||
guard let image: UIImage = UIImage(contentsOfFile: filePath) else {
|
||||
OWSLogger.warn("Avatar image for \(profile.id) could not be loaded.")
|
||||
return
|
||||
}
|
||||
|
||||
try? latestProfile
|
||||
.with(profilePictureFileName: .update(fileName))
|
||||
.update(db)
|
||||
profileAvatarCache.mutate { $0[fileName] = image }
|
||||
}
|
||||
|
||||
// Redundant but without reading 'backgroundTask' it will warn that the variable
|
||||
// isn't used
|
||||
if backgroundTask != nil { backgroundTask = nil }
|
||||
}
|
||||
.retainUntilComplete()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Current User Profile
|
||||
|
||||
public static func updateLocal(
|
||||
profileName: String,
|
||||
avatarImage: UIImage?,
|
||||
requiredSync: Bool,
|
||||
success: (() -> ())? = nil,
|
||||
failure: ((Error) -> ())? = nil
|
||||
) {
|
||||
DispatchQueue.global(qos: .default).async {
|
||||
// If the profile avatar was updated or removed then encrypt with a new profile key
|
||||
// to ensure that other users know that our profile picture was updated
|
||||
let newProfileKey: OWSAES256Key = OWSAES256Key.generateRandom()
|
||||
|
||||
guard let avatarImage: UIImage = avatarImage else {
|
||||
// If we have no image then we need to make sure to remove it from the profile
|
||||
GRDBStorage.shared.writeAsync(
|
||||
updates: { db in
|
||||
let existingProfile: Profile = Profile.fetchOrCreateCurrentUser(db)
|
||||
|
||||
OWSLogger.verbose(existingProfile.profilePictureUrl != nil ?
|
||||
"Updating local profile on service with cleared avatar." :
|
||||
"Updating local profile on service with no avatar."
|
||||
)
|
||||
|
||||
try? existingProfile
|
||||
.with(
|
||||
name: profileName,
|
||||
profilePictureUrl: nil,
|
||||
profilePictureFileName: nil,
|
||||
profileEncryptionKey: (existingProfile.profilePictureUrl != nil ?
|
||||
.update(newProfileKey) :
|
||||
.existing
|
||||
)
|
||||
)
|
||||
.save(db)
|
||||
|
||||
// Remove any cached avatar image value
|
||||
if let fileName: String = existingProfile.profilePictureFileName {
|
||||
profileAvatarCache.mutate { $0[fileName] = nil }
|
||||
}
|
||||
},
|
||||
completion: { _, _ in
|
||||
SNLog("Successfully updated service with profile.")
|
||||
|
||||
DispatchQueue.main.async {
|
||||
success?()
|
||||
}
|
||||
}
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// If we have a new avatar image, we must first:
|
||||
//
|
||||
// * Encode it to JPEG.
|
||||
// * Write it to disk.
|
||||
// * Encrypt it
|
||||
// * Upload it to asset service
|
||||
// * Send asset service info to Signal Service
|
||||
OWSLogger.verbose("Updating local profile on service with new avatar.")
|
||||
let maxAvatarBytes: UInt = (5 * 1000 * 1000)
|
||||
var image: UIImage = avatarImage
|
||||
|
||||
if image.size.width != maxAvatarDiameter || image.size.height != maxAvatarDiameter {
|
||||
// To help ensure the user is being shown the same cropping of their avatar as
|
||||
// everyone else will see, we want to be sure that the image was resized before this point.
|
||||
SNLog("Avatar image should have been resized before trying to upload")
|
||||
image = image.resizedImage(toFillPixelSize: CGSize(width: maxAvatarDiameter, height: maxAvatarDiameter))
|
||||
}
|
||||
|
||||
guard let data: Data = image.jpegData(compressionQuality: 0.95) else {
|
||||
DispatchQueue.main.async {
|
||||
SNLog("Updating service with profile failed.")
|
||||
failure?(.avatarWriteFailed)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
guard data.count <= maxAvatarBytes else {
|
||||
// Our avatar dimensions are so small that it's incredibly unlikely we wouldn't
|
||||
// be able to fit our profile photo (eg. generating pure noise at our resolution
|
||||
// compresses to ~200k)
|
||||
DispatchQueue.main.async {
|
||||
SNLog("Suprised to find profile avatar was too large. Was it scaled properly? image: \(image)")
|
||||
SNLog("Updating service with profile failed.")
|
||||
failure?(.avatarImageTooLarge)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let fileName: String = UUID().uuidString.appendingFileExtension("jpg")
|
||||
let filePath: String = OWSUserProfile.profileAvatarFilepath(withFilename: fileName)
|
||||
|
||||
// Write the avatar to disk
|
||||
do { try data.write(to: URL(fileURLWithPath: filePath), options: [.atomic]) }
|
||||
catch {
|
||||
DispatchQueue.main.async {
|
||||
SNLog("Updating service with profile failed.")
|
||||
failure?(.avatarWriteFailed)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Encrypt the avatar for upload
|
||||
guard let encryptedAvatarData: Data = encryptProfileData(data: data, key: newProfileKey) else {
|
||||
DispatchQueue.main.async {
|
||||
SNLog("Updating service with profile failed.")
|
||||
failure?(.avatarEncryptionFailed)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Upload the avatar to the FileServer
|
||||
FileServerAPIV2
|
||||
.upload(encryptedAvatarData)
|
||||
.done { fileId in
|
||||
let downloadUrl: String = "\(FileServerAPIV2.server)/files/\(fileId)"
|
||||
UserDefaults.standard[.lastProfilePictureUpload] = Date()
|
||||
|
||||
GRDBStorage.shared.writeAsync(
|
||||
updates: { db in
|
||||
try? Profile
|
||||
.fetchOrCreateCurrentUser(db)
|
||||
.with(
|
||||
name: profileName,
|
||||
profilePictureUrl: .update(downloadUrl),
|
||||
profilePictureFileName: .update(fileName),
|
||||
profileEncryptionKey: .update(newProfileKey)
|
||||
)
|
||||
.save(db)
|
||||
},
|
||||
completion: { _, _ in
|
||||
// Update the cached avatar image value
|
||||
profileAvatarCache.mutate { $0[fileName] = avatarImage }
|
||||
|
||||
DispatchQueue.main.async {
|
||||
SNLog("Successfully updated service with profile.")
|
||||
success?()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
.recover { error in
|
||||
DispatchQueue.main.async {
|
||||
SNLog("Updating service with profile failed.")
|
||||
|
||||
let isMaxFileSizeExceeded: Bool = ((error as? FileServerAPIV2.Error) == FileServerAPIV2.Error.maxFileSizeExceeded)
|
||||
failure?(isMaxFileSizeExceeded ?
|
||||
.avatarUploadMaxFileSizeExceeded :
|
||||
.avatarUploadFailed
|
||||
)
|
||||
}
|
||||
}
|
||||
.retainUntilComplete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Objective-C Support
|
||||
@objc(SMKProfileManager)
|
||||
public class SMKProfileManager: NSObject {
|
||||
@objc public static func profileAvatar(recipientId: String) -> UIImage? {
|
||||
return ProfileManager.profileAvatar(for: recipientId)
|
||||
}
|
||||
|
||||
@objc public static func updateLocal(profileName: String, avatarImage: UIImage?, requiresSync: Bool) {
|
||||
ProfileManager.updateLocal(profileName: profileName, avatarImage: avatarImage, requiredSync: requiresSync)
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue